|
| 1 | +use solana_program::{ |
| 2 | + account_info::AccountInfo, entrypoint::ProgramResult, program::set_return_data, |
| 3 | + program_error::ProgramError, pubkey::Pubkey, |
| 4 | +}; |
| 5 | + |
| 6 | +use crate::{ |
| 7 | + error::ExecutorQuoterError, |
| 8 | + math, |
| 9 | + state::{load_account, ChainInfo, Config, QuoteBody}, |
| 10 | +}; |
| 11 | + |
| 12 | +/// Relay instruction type constants (matching EVM) |
| 13 | +const IX_TYPE_GAS: u8 = 1; |
| 14 | +const IX_TYPE_DROP_OFF: u8 = 2; |
| 15 | + |
| 16 | +/// Parses relay instructions to extract total gas limit and msg value. |
| 17 | +/// Instruction format: |
| 18 | +/// - Type 1 (Gas): 1 byte type + 16 bytes gas_limit + 16 bytes msg_value |
| 19 | +/// - Type 2 (DropOff): 1 byte type + 48 bytes (16 msg_value + 32 recipient) |
| 20 | +fn parse_relay_instructions(relay_instructions: &[u8]) -> Result<(u128, u128), ProgramError> { |
| 21 | + let mut offset = 0; |
| 22 | + let mut gas_limit: u128 = 0; |
| 23 | + let mut msg_value: u128 = 0; |
| 24 | + let mut has_drop_off = false; |
| 25 | + |
| 26 | + while offset < relay_instructions.len() { |
| 27 | + if offset >= relay_instructions.len() { |
| 28 | + return Err(ExecutorQuoterError::InvalidRelayInstructions.into()); |
| 29 | + } |
| 30 | + |
| 31 | + let ix_type = relay_instructions[offset]; |
| 32 | + offset += 1; |
| 33 | + |
| 34 | + match ix_type { |
| 35 | + IX_TYPE_GAS => { |
| 36 | + // Gas instruction: 16 bytes gas_limit + 16 bytes msg_value |
| 37 | + if offset + 32 > relay_instructions.len() { |
| 38 | + return Err(ExecutorQuoterError::InvalidRelayInstructions.into()); |
| 39 | + } |
| 40 | + |
| 41 | + let mut ix_gas_bytes = [0u8; 16]; |
| 42 | + ix_gas_bytes.copy_from_slice(&relay_instructions[offset..offset + 16]); |
| 43 | + let ix_gas_limit = u128::from_be_bytes(ix_gas_bytes); |
| 44 | + offset += 16; |
| 45 | + |
| 46 | + let mut ix_val_bytes = [0u8; 16]; |
| 47 | + ix_val_bytes.copy_from_slice(&relay_instructions[offset..offset + 16]); |
| 48 | + let ix_msg_value = u128::from_be_bytes(ix_val_bytes); |
| 49 | + offset += 16; |
| 50 | + |
| 51 | + gas_limit = gas_limit |
| 52 | + .checked_add(ix_gas_limit) |
| 53 | + .ok_or(ExecutorQuoterError::MathOverflow)?; |
| 54 | + msg_value = msg_value |
| 55 | + .checked_add(ix_msg_value) |
| 56 | + .ok_or(ExecutorQuoterError::MathOverflow)?; |
| 57 | + } |
| 58 | + IX_TYPE_DROP_OFF => { |
| 59 | + if has_drop_off { |
| 60 | + return Err(ExecutorQuoterError::MoreThanOneDropOff.into()); |
| 61 | + } |
| 62 | + has_drop_off = true; |
| 63 | + |
| 64 | + // DropOff instruction: 16 bytes msg_value + 32 bytes recipient |
| 65 | + if offset + 48 > relay_instructions.len() { |
| 66 | + return Err(ExecutorQuoterError::InvalidRelayInstructions.into()); |
| 67 | + } |
| 68 | + |
| 69 | + let mut ix_val_bytes = [0u8; 16]; |
| 70 | + ix_val_bytes.copy_from_slice(&relay_instructions[offset..offset + 16]); |
| 71 | + let ix_msg_value = u128::from_be_bytes(ix_val_bytes); |
| 72 | + offset += 48; // Skip msg_value (16) + recipient (32) |
| 73 | + |
| 74 | + msg_value = msg_value |
| 75 | + .checked_add(ix_msg_value) |
| 76 | + .ok_or(ExecutorQuoterError::MathOverflow)?; |
| 77 | + } |
| 78 | + _ => { |
| 79 | + return Err(ExecutorQuoterError::UnsupportedInstruction.into()); |
| 80 | + } |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + Ok((gas_limit, msg_value)) |
| 85 | +} |
| 86 | + |
| 87 | +/// Process RequestQuote instruction. |
| 88 | +/// Returns the required payment amount for cross-chain execution. |
| 89 | +/// |
| 90 | +/// Accounts: |
| 91 | +/// 0. `[]` config - Config PDA |
| 92 | +/// 1. `[]` chain_info - ChainInfo PDA for destination chain |
| 93 | +/// 2. `[]` quote_body - QuoteBody PDA for destination chain |
| 94 | +/// |
| 95 | +/// Instruction data layout: |
| 96 | +/// - dst_chain: u16 (offset 0) |
| 97 | +/// - dst_addr: [u8; 32] (offset 2) |
| 98 | +/// - refund_addr: [u8; 32] (offset 34) |
| 99 | +/// - request_bytes_len: u32 (offset 66) |
| 100 | +/// - request_bytes: [u8; request_bytes_len] (offset 70) |
| 101 | +/// - relay_instructions_len: u32 (offset 70 + request_bytes_len) |
| 102 | +/// - relay_instructions: [u8; relay_instructions_len] |
| 103 | +pub fn process_request_quote( |
| 104 | + program_id: &Pubkey, |
| 105 | + accounts: &[AccountInfo], |
| 106 | + data: &[u8], |
| 107 | +) -> ProgramResult { |
| 108 | + // Parse accounts |
| 109 | + let [config_account, chain_info_account, quote_body_account] = accounts else { |
| 110 | + return Err(ProgramError::NotEnoughAccountKeys); |
| 111 | + }; |
| 112 | + |
| 113 | + // Load accounts (discriminator checked inside load_account) |
| 114 | + // Config is validated but not used in requestQuote (only in requestExecutionQuote) |
| 115 | + let _config = load_account::<Config>(config_account, program_id)?; |
| 116 | + |
| 117 | + let chain_info = load_account::<ChainInfo>(chain_info_account, program_id)?; |
| 118 | + if !chain_info.is_enabled() { |
| 119 | + return Err(ExecutorQuoterError::ChainDisabled.into()); |
| 120 | + } |
| 121 | + |
| 122 | + let quote_body = load_account::<QuoteBody>(quote_body_account, program_id)?; |
| 123 | + |
| 124 | + // Parse instruction data to get relay_instructions |
| 125 | + // Skip: dst_chain (2) + dst_addr (32) + refund_addr (32) = 66 bytes |
| 126 | + if data.len() < 70 { |
| 127 | + return Err(ExecutorQuoterError::InvalidInstructionData.into()); |
| 128 | + } |
| 129 | + |
| 130 | + // Skip request_bytes |
| 131 | + let mut len_bytes = [0u8; 4]; |
| 132 | + len_bytes.copy_from_slice(&data[66..70]); |
| 133 | + let request_bytes_len = u32::from_le_bytes(len_bytes) as usize; |
| 134 | + let relay_start = 70 + request_bytes_len; |
| 135 | + |
| 136 | + if data.len() < relay_start + 4 { |
| 137 | + return Err(ExecutorQuoterError::InvalidInstructionData.into()); |
| 138 | + } |
| 139 | + |
| 140 | + let mut relay_len_bytes = [0u8; 4]; |
| 141 | + relay_len_bytes.copy_from_slice(&data[relay_start..relay_start + 4]); |
| 142 | + let relay_instructions_len = u32::from_le_bytes(relay_len_bytes) as usize; |
| 143 | + |
| 144 | + let relay_data_start = relay_start + 4; |
| 145 | + if data.len() < relay_data_start + relay_instructions_len { |
| 146 | + return Err(ExecutorQuoterError::InvalidInstructionData.into()); |
| 147 | + } |
| 148 | + |
| 149 | + let relay_instructions = &data[relay_data_start..relay_data_start + relay_instructions_len]; |
| 150 | + |
| 151 | + // Parse relay instructions |
| 152 | + let (gas_limit, msg_value) = parse_relay_instructions(relay_instructions)?; |
| 153 | + |
| 154 | + // Calculate quote using U256 math |
| 155 | + let required_payment = math::estimate_quote( |
| 156 | + quote_body.base_fee, |
| 157 | + quote_body.src_price, |
| 158 | + quote_body.dst_price, |
| 159 | + quote_body.dst_gas_price, |
| 160 | + chain_info.gas_price_decimals, |
| 161 | + chain_info.native_decimals, |
| 162 | + gas_limit, |
| 163 | + msg_value, |
| 164 | + )?; |
| 165 | + |
| 166 | + // Return the quote as big-endian U256 (32 bytes) via set_return_data. |
| 167 | + // Clients can read this via simulateTransaction or CPI callers via get_return_data. |
| 168 | + set_return_data(&required_payment.to_be_bytes()); |
| 169 | + |
| 170 | + Ok(()) |
| 171 | +} |
| 172 | + |
| 173 | +/// Process RequestExecutionQuote instruction. |
| 174 | +/// Returns the required payment, payee address, and quote body. |
| 175 | +/// |
| 176 | +/// Accounts: Same as RequestQuote |
| 177 | +pub fn process_request_execution_quote( |
| 178 | + program_id: &Pubkey, |
| 179 | + accounts: &[AccountInfo], |
| 180 | + data: &[u8], |
| 181 | +) -> ProgramResult { |
| 182 | + // Parse accounts |
| 183 | + let [config_account, chain_info_account, quote_body_account] = accounts else { |
| 184 | + return Err(ProgramError::NotEnoughAccountKeys); |
| 185 | + }; |
| 186 | + |
| 187 | + // Load accounts (discriminator checked inside load_account) |
| 188 | + let config = load_account::<Config>(config_account, program_id)?; |
| 189 | + |
| 190 | + let chain_info = load_account::<ChainInfo>(chain_info_account, program_id)?; |
| 191 | + if !chain_info.is_enabled() { |
| 192 | + return Err(ExecutorQuoterError::ChainDisabled.into()); |
| 193 | + } |
| 194 | + |
| 195 | + let quote_body = load_account::<QuoteBody>(quote_body_account, program_id)?; |
| 196 | + |
| 197 | + // Parse instruction data to get relay_instructions |
| 198 | + if data.len() < 70 { |
| 199 | + return Err(ExecutorQuoterError::InvalidInstructionData.into()); |
| 200 | + } |
| 201 | + |
| 202 | + let mut len_bytes = [0u8; 4]; |
| 203 | + len_bytes.copy_from_slice(&data[66..70]); |
| 204 | + let request_bytes_len = u32::from_le_bytes(len_bytes) as usize; |
| 205 | + let relay_start = 70 + request_bytes_len; |
| 206 | + |
| 207 | + if data.len() < relay_start + 4 { |
| 208 | + return Err(ExecutorQuoterError::InvalidInstructionData.into()); |
| 209 | + } |
| 210 | + |
| 211 | + let mut relay_len_bytes = [0u8; 4]; |
| 212 | + relay_len_bytes.copy_from_slice(&data[relay_start..relay_start + 4]); |
| 213 | + let relay_instructions_len = u32::from_le_bytes(relay_len_bytes) as usize; |
| 214 | + |
| 215 | + let relay_data_start = relay_start + 4; |
| 216 | + if data.len() < relay_data_start + relay_instructions_len { |
| 217 | + return Err(ExecutorQuoterError::InvalidInstructionData.into()); |
| 218 | + } |
| 219 | + |
| 220 | + let relay_instructions = &data[relay_data_start..relay_data_start + relay_instructions_len]; |
| 221 | + |
| 222 | + // Parse relay instructions |
| 223 | + let (gas_limit, msg_value) = parse_relay_instructions(relay_instructions)?; |
| 224 | + |
| 225 | + // Calculate quote using U256 math |
| 226 | + let required_payment = math::estimate_quote( |
| 227 | + quote_body.base_fee, |
| 228 | + quote_body.src_price, |
| 229 | + quote_body.dst_price, |
| 230 | + quote_body.dst_gas_price, |
| 231 | + chain_info.gas_price_decimals, |
| 232 | + chain_info.native_decimals, |
| 233 | + gas_limit, |
| 234 | + msg_value, |
| 235 | + )?; |
| 236 | + |
| 237 | + // Return data layout (96 bytes, matching EVM return values): |
| 238 | + // - bytes 0-31: required_payment (U256, big-endian) |
| 239 | + // - bytes 32-63: payee_address (32 bytes) |
| 240 | + // - bytes 64-95: quote_body (32 bytes, EQ01 format) |
| 241 | + let mut return_data = [0u8; 96]; |
| 242 | + return_data[0..32].copy_from_slice(&required_payment.to_be_bytes()); |
| 243 | + return_data[32..64].copy_from_slice(&config.payee_address); |
| 244 | + return_data[64..96].copy_from_slice("e_body.to_bytes32()); |
| 245 | + |
| 246 | + set_return_data(&return_data); |
| 247 | + |
| 248 | + Ok(()) |
| 249 | +} |
0 commit comments