// file: kb_lib/src/dex/raydium_amm_v4.rs //! Raydium AmmV4 transaction decoder. const OBSERVED_JUPITER_V6_PROGRAM_ID: &str = "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4"; /// Decoded Raydium AmmV4 initialize2 pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RaydiumAmmV4Initialize2PoolDecoded { /// Parent transaction id. pub transaction_id: i64, /// Parent instruction id. pub instruction_id: i64, /// Transaction signature. pub signature: std::string::String, /// Program id. pub program_id: std::string::String, /// Optional pool account. pub pool_account: std::option::Option, /// Optional lp mint. pub lp_mint: std::option::Option, /// Optional token A mint. pub token_a_mint: std::option::Option, /// Optional token B mint. pub token_b_mint: std::option::Option, /// Optional market account. pub market_account: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Raydium AmmV4 swap event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RaydiumAmmV4SwapDecoded { /// Parent transaction id. pub transaction_id: i64, /// Parent instruction id. pub instruction_id: i64, /// Transaction signature. pub signature: std::string::String, /// Program id. pub program_id: std::string::String, /// Local decoded event kind derived from the AMM v4 instruction discriminator. pub event_kind: std::string::String, /// Upstream instruction name derived from the AMM v4 instruction discriminator. pub instruction_name: std::string::String, /// One-byte Raydium AMM v4 instruction discriminator in hex form. pub discriminator_hex: std::string::String, /// AMM pool/state account. pub pool_account: std::string::String, /// Raydium AMM authority account. pub authority: std::string::String, /// Base token mint after base/quote normalization. pub token_a_mint: std::string::String, /// Quote token mint after base/quote normalization. pub token_b_mint: std::string::String, /// Base-side vault after base/quote normalization. pub base_vault: std::string::String, /// Quote-side vault after base/quote normalization. pub quote_vault: std::string::String, /// Raw vault account found at the AMM instruction vault A position. pub raw_vault_a: std::string::String, /// Raw vault account found at the AMM instruction vault B position. pub raw_vault_b: std::string::String, /// Optional input vault inferred from vault balance deltas. pub input_vault: std::option::Option, /// Optional output vault inferred from vault balance deltas. pub output_vault: std::option::Option, /// Optional input user token account inferred from token balance deltas. pub input_token_account: std::option::Option, /// Optional output user token account inferred from token balance deltas. pub output_token_account: std::option::Option, /// Optional trade side inferred from vault balance deltas. pub trade_side: std::option::Option, /// Optional raw base amount inferred from vault balance deltas. pub base_amount_raw: std::option::Option, /// Optional raw quote amount inferred from vault balance deltas. pub quote_amount_raw: std::option::Option, /// Whether this instruction is an inner instruction. pub inner_instruction: bool, /// Optional top-level caller program inferred from the parent instruction. pub route_source: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Raydium AmmV4 non-swap or decoded-only instruction event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct RaydiumAmmV4InstructionDecoded { /// Parent transaction id. pub transaction_id: i64, /// Parent instruction id. pub instruction_id: i64, /// Transaction signature. pub signature: std::string::String, /// Program id. pub program_id: std::string::String, /// Local decoded event kind derived from the AMM v4 instruction discriminator. pub event_kind: std::string::String, /// Upstream instruction name derived from the AMM v4 instruction discriminator. pub instruction_name: std::string::String, /// One-byte Raydium AMM v4 instruction discriminator in hex form. pub discriminator_hex: std::string::String, /// Optional AMM pool/state account. pub pool_account: std::option::Option, /// Optional Raydium AMM authority account. pub authority: std::option::Option, /// Optional AMM open-orders account. pub open_orders: std::option::Option, /// Optional AMM target-orders account. pub target_orders: std::option::Option, /// Optional market program account. pub market_program: std::option::Option, /// Optional market account. pub market_account: std::option::Option, /// Optional LP mint account. pub lp_mint: std::option::Option, /// Optional token A mint after best-effort account layout extraction. pub token_a_mint: std::option::Option, /// Optional token B mint after best-effort account layout extraction. pub token_b_mint: std::option::Option, /// Optional AMM coin/base vault account. pub base_vault: std::option::Option, /// Optional AMM pc/quote vault account. pub quote_vault: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Raydium AmmV4 event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum RaydiumAmmV4DecodedEvent { /// `initialize2` pool creation-like event. Initialize2Pool(std::boxed::Box), /// Swap event decoded from a direct or inner Raydium AMM v4 instruction. Swap(std::boxed::Box), /// Known Raydium AMM v4 instruction decoded without direct trade materialization. Instruction(std::boxed::Box), } /// Raydium AmmV4 decoder. #[derive(Debug, Clone, Default)] pub struct RaydiumAmmV4Decoder; impl RaydiumAmmV4Decoder { /// Creates a new decoder. pub fn new() -> Self { return Self; } /// Decodes one projected transaction into zero or more Raydium AmmV4 events. pub fn decode_transaction( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, crate::Error> { let transaction_id_option = transaction.id; let transaction_id = match transaction_id_option { Some(transaction_id) => transaction_id, None => { return Err(crate::Error::InvalidState(format!( "chain transaction '{}' has no internal id", transaction.signature ))); }, }; let transaction_json_result = serde_json::from_str::(transaction.transaction_json.as_str()); let transaction_json = match transaction_json_result { Ok(transaction_json) => transaction_json, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse transaction_json for signature '{}': {}", transaction.signature, error ))); }, }; let meta_value_result = parse_transaction_meta_value(transaction, &transaction_json); let meta_value = match meta_value_result { Ok(meta_value) => meta_value, Err(error) => return Err(error), }; let account_keys = extract_transaction_account_keys(&transaction_json, meta_value.as_ref()); let token_balances = extract_token_balance_records(meta_value.as_ref(), account_keys.as_slice()); let log_messages = extract_log_messages(&transaction_json); let has_initialize2_log = log_messages_contain_initialize2(&log_messages); let mut decoded_events = std::vec::Vec::new(); for instruction in instructions { let program_id_option = &instruction.program_id; let program_id = match program_id_option { Some(program_id) => program_id, None => continue, }; if program_id.as_str() != crate::RAYDIUM_AMM_V4_PROGRAM_ID { continue; } let instruction_id_option = instruction.id; let instruction_id = match instruction_id_option { Some(instruction_id) => instruction_id, None => continue, }; let accounts_result = parse_accounts_json(instruction.accounts_json.as_str()); let accounts = match accounts_result { Ok(accounts) => accounts, Err(error) => return Err(error), }; if instruction.parent_instruction_id.is_none() && has_initialize2_log { let initialize_event = decode_initialize2_event( transaction, transaction_id, instruction, instruction_id, program_id, accounts.as_slice(), log_messages.as_slice(), ); if let Some(initialize_event) = initialize_event { decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool( std::boxed::Box::new(initialize_event), )); continue; } } let swap_result = decode_swap_event( transaction, transaction_id, instruction, instruction_id, program_id, accounts.as_slice(), instructions, token_balances.as_slice(), ); match swap_result { Ok(Some(swap)) => { decoded_events .push(crate::RaydiumAmmV4DecodedEvent::Swap(std::boxed::Box::new(swap))); continue; }, Ok(None) => {}, Err(error) => return Err(error), } let instruction_event = decode_known_instruction_event( transaction, transaction_id, instruction, instruction_id, program_id, accounts.as_slice(), ); if let Some(instruction_event) = instruction_event { decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Instruction( std::boxed::Box::new(instruction_event), )); } } return Ok(decoded_events); } } fn decode_initialize2_event( transaction: &crate::ChainTransactionDto, transaction_id: i64, instruction: &crate::ChainInstructionDto, instruction_id: i64, program_id: &str, accounts: &[std::string::String], log_messages: &[std::string::String], ) -> std::option::Option { if accounts.len() < 10 { return None; } let pool_account = extract_account(accounts, 4); let lp_mint = extract_account(accounts, 7); let token_a_mint = extract_account(accounts, 8); let token_b_mint = extract_account(accounts, 9); let market_account = extract_account(accounts, 16); let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref()); let discriminator_hex = raydium_amm_v4_instruction_discriminator_hex(data_base58.as_deref()); let payload_json = serde_json::json!({ "decoder": "raydium_amm_v4", "eventKind": "initialize2_pool", "instructionName": "initialize2", "upstreamInstructionName": "initialize2", "discriminatorHex": discriminator_hex, "instructionDiscriminatorHex": discriminator_hex, "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "accounts": accounts, "logMessages": log_messages, "poolAccount": pool_account, "lpMint": lp_mint, "tokenAMint": token_a_mint, "tokenBMint": token_b_mint, "marketAccount": market_account }); return Some(crate::RaydiumAmmV4Initialize2PoolDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.to_string(), pool_account, lp_mint, token_a_mint, token_b_mint, market_account, payload_json, }); } fn decode_swap_event( transaction: &crate::ChainTransactionDto, transaction_id: i64, instruction: &crate::ChainInstructionDto, instruction_id: i64, program_id: &str, accounts: &[std::string::String], transaction_instructions: &[crate::ChainInstructionDto], token_balances: &[TokenBalanceRecord], ) -> Result, crate::Error> { if accounts.len() < 8 { return Ok(None); } let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref()); let swap_instruction = match raydium_amm_v4_swap_instruction(data_base58.as_deref()) { Some(swap_instruction) => swap_instruction, None => return Ok(None), }; let pool_account = match extract_account(accounts, 1) { Some(pool_account) => pool_account, None => return Ok(None), }; let authority = match extract_account(accounts, 2) { Some(authority) => authority, None => return Ok(None), }; let vault_pair_option = infer_raydium_amm_v4_vault_pair(accounts, authority.as_str(), token_balances); let vault_pair = match vault_pair_option { Some(vault_pair) => vault_pair, None => return Ok(None), }; let normalized_pair = normalize_pool_pair(vault_pair); let base_amount_raw = amount_delta_abs_for_account(token_balances, normalized_pair.base_vault.as_str()); let quote_amount_raw = amount_delta_abs_for_account(token_balances, normalized_pair.quote_vault.as_str()); let trade_side = infer_trade_side_from_vault_deltas( token_balances, normalized_pair.base_vault.as_str(), normalized_pair.quote_vault.as_str(), ); if trade_side.is_none() { return Ok(None); } let token_accounts = infer_user_token_accounts( accounts, token_balances, authority.as_str(), normalized_pair.base_mint.as_str(), normalized_pair.quote_mint.as_str(), normalized_pair.base_vault.as_str(), normalized_pair.quote_vault.as_str(), trade_side.as_deref(), ); let input_output_vaults = input_output_vaults_for_trade_side( trade_side.as_deref(), normalized_pair.base_vault.as_str(), normalized_pair.quote_vault.as_str(), ); let parent_program = parent_program_id_for_instruction(instruction, transaction_instructions); let trade_candidate = base_amount_raw.is_some() && quote_amount_raw.is_some(); let payload_json = serde_json::json!({ "decoder": "raydium_amm_v4", "eventKind": swap_instruction.event_kind, "instructionName": swap_instruction.instruction_name, "upstreamInstructionName": swap_instruction.instruction_name, "discriminatorHex": swap_instruction.discriminator_hex, "instructionDiscriminatorHex": swap_instruction.discriminator_hex, "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "innerInstruction": instruction.inner_instruction_index.is_some(), "parentInstructionId": instruction.parent_instruction_id, "parentProgramId": parent_program.clone(), "routeSource": route_source_from_parent(parent_program.as_deref()), "programId": program_id, "accounts": accounts, "data": data_base58, "poolAccount": pool_account.clone(), "authority": authority.clone(), "rawVaultA": normalized_pair.raw_vault_a.clone(), "rawVaultB": normalized_pair.raw_vault_b.clone(), "baseVault": normalized_pair.base_vault.clone(), "quoteVault": normalized_pair.quote_vault.clone(), "inputVault": input_output_vaults.0.clone(), "outputVault": input_output_vaults.1.clone(), "inputTokenAccount": token_accounts.0.clone(), "outputTokenAccount": token_accounts.1.clone(), "tokenAMint": normalized_pair.base_mint.clone(), "tokenBMint": normalized_pair.quote_mint.clone(), "baseMint": normalized_pair.base_mint.clone(), "quoteMint": normalized_pair.quote_mint.clone(), "tradeSide": trade_side.clone(), "baseAmountRaw": base_amount_raw.clone(), "quoteAmountRaw": quote_amount_raw.clone(), "tradeCandidate": trade_candidate, "candleCandidate": trade_candidate, "skipTradeReason": if trade_candidate { serde_json::Value::Null } else { serde_json::Value::String("missing_vault_balance_amounts".to_string()) }, "skipCandleReason": if trade_candidate { serde_json::Value::Null } else { serde_json::Value::String("missing_vault_balance_amounts".to_string()) } }); return Ok(Some(crate::RaydiumAmmV4SwapDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.to_string(), event_kind: swap_instruction.event_kind.to_string(), instruction_name: swap_instruction.instruction_name.to_string(), discriminator_hex: swap_instruction.discriminator_hex.to_string(), pool_account, authority, token_a_mint: normalized_pair.base_mint, token_b_mint: normalized_pair.quote_mint, base_vault: normalized_pair.base_vault, quote_vault: normalized_pair.quote_vault, raw_vault_a: normalized_pair.raw_vault_a, raw_vault_b: normalized_pair.raw_vault_b, input_vault: input_output_vaults.0, output_vault: input_output_vaults.1, input_token_account: token_accounts.0, output_token_account: token_accounts.1, trade_side, base_amount_raw, quote_amount_raw, inner_instruction: instruction.inner_instruction_index.is_some(), route_source: route_source_from_parent(parent_program.as_deref()), payload_json, })); } fn decode_known_instruction_event( transaction: &crate::ChainTransactionDto, transaction_id: i64, instruction: &crate::ChainInstructionDto, instruction_id: i64, program_id: &str, accounts: &[std::string::String], ) -> std::option::Option { let data_base58 = parse_optional_data_json_as_base58(instruction.data_json.as_deref()); let data_bytes = raydium_amm_v4_instruction_data_bytes(data_base58.as_deref()); let identity = match raydium_amm_v4_instruction_identity(data_base58.as_deref()) { Some(identity) => identity, None => return None, }; let account_layout = raydium_amm_v4_account_layout(identity.discriminator_hex); let pool_account = extract_account_by_index(accounts, account_layout.pool_account_index); let authority = extract_account_by_index(accounts, account_layout.authority_index); let open_orders = extract_account_by_index(accounts, account_layout.open_orders_index); let target_orders = extract_account_by_index(accounts, account_layout.target_orders_index); let market_program = extract_account_by_index(accounts, account_layout.market_program_index); let market_account = extract_account_by_index(accounts, account_layout.market_account_index); let lp_mint = extract_account_by_index(accounts, account_layout.lp_mint_index); let token_a_mint = extract_account_by_index(accounts, account_layout.token_a_mint_index); let token_b_mint = extract_account_by_index(accounts, account_layout.token_b_mint_index); let base_vault = extract_account_by_index(accounts, account_layout.base_vault_index); let quote_vault = extract_account_by_index(accounts, account_layout.quote_vault_index); let mut payload_json = serde_json::json!({ "decoder": "raydium_amm_v4", "eventKind": identity.event_kind, "instructionName": identity.instruction_name, "upstreamInstructionName": identity.instruction_name, "discriminatorHex": identity.discriminator_hex, "instructionDiscriminatorHex": identity.discriminator_hex, "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "innerInstruction": instruction.inner_instruction_index.is_some(), "parentInstructionId": instruction.parent_instruction_id, "programId": program_id, "accounts": accounts, "data": data_base58, "instructionDataLength": data_bytes.as_ref().map(std::vec::Vec::len), "poolAccount": pool_account.clone(), "authority": authority.clone(), "openOrders": open_orders.clone(), "targetOrders": target_orders.clone(), "marketProgram": market_program.clone(), "marketAccount": market_account.clone(), "lpMint": lp_mint.clone(), "tokenAMint": token_a_mint.clone(), "tokenBMint": token_b_mint.clone(), "baseVault": base_vault.clone(), "quoteVault": quote_vault.clone(), "tradeCandidate": false, "candleCandidate": false, "skipTradeReason": "decoded_only_instruction", "skipCandleReason": "decoded_only_instruction" }); enrich_known_instruction_payload(&mut payload_json, identity.discriminator_hex, data_bytes.as_deref()); return Some(crate::RaydiumAmmV4InstructionDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.to_string(), event_kind: identity.event_kind.to_string(), instruction_name: identity.instruction_name.to_string(), discriminator_hex: identity.discriminator_hex.to_string(), pool_account, authority, open_orders, target_orders, market_program, market_account, lp_mint, token_a_mint, token_b_mint, base_vault, quote_vault, payload_json, }); } #[derive(Clone, Copy)] struct RaydiumAmmV4AccountLayout { pool_account_index: std::option::Option, authority_index: std::option::Option, open_orders_index: std::option::Option, target_orders_index: std::option::Option, market_program_index: std::option::Option, market_account_index: std::option::Option, lp_mint_index: std::option::Option, token_a_mint_index: std::option::Option, token_b_mint_index: std::option::Option, base_vault_index: std::option::Option, quote_vault_index: std::option::Option, } fn raydium_amm_v4_empty_account_layout() -> RaydiumAmmV4AccountLayout { return RaydiumAmmV4AccountLayout { pool_account_index: None, authority_index: None, open_orders_index: None, target_orders_index: None, market_program_index: None, market_account_index: None, lp_mint_index: None, token_a_mint_index: None, token_b_mint_index: None, base_vault_index: None, quote_vault_index: None, }; } fn raydium_amm_v4_account_layout(discriminator_hex: &str) -> RaydiumAmmV4AccountLayout { let mut layout = raydium_amm_v4_empty_account_layout(); match discriminator_hex { "00" => { layout.pool_account_index = Some(3); }, "01" => { layout.pool_account_index = Some(4); layout.authority_index = Some(5); layout.open_orders_index = Some(6); layout.lp_mint_index = Some(7); layout.token_a_mint_index = Some(8); layout.token_b_mint_index = Some(9); layout.base_vault_index = Some(10); layout.quote_vault_index = Some(11); layout.target_orders_index = Some(12); layout.market_program_index = Some(15); layout.market_account_index = Some(16); }, "02" => { layout.pool_account_index = Some(3); layout.authority_index = Some(4); layout.open_orders_index = Some(5); layout.target_orders_index = Some(6); layout.base_vault_index = Some(7); layout.quote_vault_index = Some(8); layout.market_program_index = Some(9); layout.market_account_index = Some(10); }, "03" => { layout.pool_account_index = Some(1); layout.authority_index = Some(2); layout.open_orders_index = Some(3); layout.target_orders_index = Some(4); layout.lp_mint_index = Some(5); layout.base_vault_index = Some(6); layout.quote_vault_index = Some(7); layout.market_account_index = Some(8); }, "04" => { layout.pool_account_index = Some(1); layout.authority_index = Some(2); layout.open_orders_index = Some(3); layout.target_orders_index = Some(4); layout.lp_mint_index = Some(5); layout.base_vault_index = Some(6); layout.quote_vault_index = Some(7); layout.market_program_index = Some(8); layout.market_account_index = Some(9); }, "05" => { layout.pool_account_index = Some(3); layout.authority_index = Some(4); layout.open_orders_index = Some(5); layout.base_vault_index = Some(6); layout.quote_vault_index = Some(7); layout.target_orders_index = Some(8); layout.market_program_index = Some(9); layout.market_account_index = Some(10); }, "06" => { layout.pool_account_index = Some(1); layout.authority_index = Some(2); layout.open_orders_index = Some(3); layout.target_orders_index = Some(4); layout.base_vault_index = Some(5); layout.quote_vault_index = Some(6); layout.market_program_index = Some(7); layout.market_account_index = Some(8); }, "07" => { layout.pool_account_index = Some(1); layout.authority_index = Some(3); layout.open_orders_index = Some(4); layout.base_vault_index = Some(5); layout.quote_vault_index = Some(6); layout.target_orders_index = Some(10); layout.market_program_index = Some(11); layout.market_account_index = Some(12); }, "08" => { layout.pool_account_index = Some(1); layout.authority_index = Some(3); }, "09" | "0b" => { layout.pool_account_index = Some(1); layout.authority_index = Some(2); layout.open_orders_index = Some(3); layout.target_orders_index = Some(4); layout.base_vault_index = Some(5); layout.quote_vault_index = Some(6); layout.market_program_index = Some(7); layout.market_account_index = Some(8); }, "0a" => { layout.pool_account_index = Some(4); }, "0c" => { layout.pool_account_index = Some(1); }, "0d" => { layout.pool_account_index = Some(1); }, "10" | "11" => { layout.pool_account_index = Some(1); layout.authority_index = Some(2); layout.base_vault_index = Some(3); layout.quote_vault_index = Some(4); }, _ => {}, } return layout; } fn extract_account_by_index( accounts: &[std::string::String], index: std::option::Option, ) -> std::option::Option { let index = match index { Some(index) => index, None => return None, }; return extract_account(accounts, index); } fn raydium_amm_v4_instruction_data_bytes( data_base58: std::option::Option<&str>, ) -> std::option::Option> { let data_base58 = match data_base58 { Some(data_base58) => data_base58, None => return None, }; let bytes_result = bs58::decode(data_base58).into_vec(); match bytes_result { Ok(bytes) => return Some(bytes), Err(_) => return None, } } fn enrich_known_instruction_payload( payload_json: &mut serde_json::Value, discriminator_hex: &str, data: std::option::Option<&[u8]>, ) { let data = match data { Some(data) => data, None => return, }; match discriminator_hex { "00" => { insert_u8(payload_json, "nonce", data, 1); insert_u64_string(payload_json, "openTime", data, 2); }, "01" => { insert_u8(payload_json, "nonce", data, 1); insert_u64_string(payload_json, "openTime", data, 2); insert_u64_string(payload_json, "initPcAmount", data, 10); insert_u64_string(payload_json, "initCoinAmount", data, 18); insert_u64_string(payload_json, "tokenAAmount", data, 18); insert_u64_string(payload_json, "tokenBAmount", data, 10); }, "02" => { insert_u16(payload_json, "planOrderLimit", data, 1); insert_u16(payload_json, "placeOrderLimit", data, 3); insert_u16(payload_json, "cancelOrderLimit", data, 5); }, "03" => { insert_u64_string(payload_json, "maxCoinAmount", data, 1); insert_u64_string(payload_json, "maxPcAmount", data, 9); insert_u64_string(payload_json, "baseSide", data, 17); insert_u64_string(payload_json, "otherAmountMin", data, 25); insert_u64_string(payload_json, "tokenAAmount", data, 1); insert_u64_string(payload_json, "tokenBAmount", data, 9); }, "04" => { insert_u64_string(payload_json, "lpAmountRaw", data, 1); insert_u64_string(payload_json, "liquidity", data, 1); insert_u64_string(payload_json, "minCoinAmount", data, 9); insert_u64_string(payload_json, "minPcAmount", data, 17); }, "06" => { insert_u8(payload_json, "configParam", data, 1); insert_u64_string(payload_json, "configValue", data, 2); insert_fixed_hex(payload_json, "configPubkeyHex", data, 2, 32); insert_u64_string(payload_json, "lastOrderNumerator", data, 2); insert_u64_string(payload_json, "lastOrderDenominator", data, 10); }, "08" => { insert_u64_string(payload_json, "amountRaw", data, 1); }, "09" | "10" => { insert_u64_string(payload_json, "amountIn", data, 1); insert_u64_string(payload_json, "minimumAmountOut", data, 9); }, "0a" => { insert_u8(payload_json, "nonce", data, 1); }, "0b" | "11" => { insert_u64_string(payload_json, "maxAmountIn", data, 1); insert_u64_string(payload_json, "amountOut", data, 9); }, "0c" => { insert_u8(payload_json, "simulateParam", data, 1); insert_u64_string(payload_json, "amountIn", data, 2); insert_u64_string(payload_json, "minimumAmountOut", data, 10); insert_u64_string(payload_json, "maxAmountIn", data, 2); insert_u64_string(payload_json, "amountOut", data, 10); }, "0d" => { insert_u16(payload_json, "orderCancelLimit", data, 1); }, "0f" => { insert_u8(payload_json, "configParam", data, 1); insert_fixed_hex(payload_json, "configOwnerHex", data, 2, 32); insert_u64_string(payload_json, "createPoolFee", data, 2); }, _ => {}, } } fn insert_u8(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) { let value = match read_u8(data, offset) { Some(value) => value, None => return, }; insert_json_value( payload_json, key, serde_json::Value::Number(serde_json::Number::from(value as u64)), ); } fn insert_u16(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) { let value = match read_u16_le(data, offset) { Some(value) => value, None => return, }; insert_json_value( payload_json, key, serde_json::Value::Number(serde_json::Number::from(value as u64)), ); } fn insert_u64_string(payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize) { let value = match read_u64_le(data, offset) { Some(value) => value, None => return, }; insert_json_value( payload_json, key, serde_json::Value::String(value.to_string()), ); } fn insert_fixed_hex( payload_json: &mut serde_json::Value, key: &str, data: &[u8], offset: usize, len: usize, ) { if data.len() < offset + len { return; } let slice = &data[offset..offset + len]; insert_json_value(payload_json, key, serde_json::Value::String(bytes_to_hex(slice))); } fn insert_json_value(payload_json: &mut serde_json::Value, key: &str, value: serde_json::Value) { let object_option = payload_json.as_object_mut(); let object = match object_option { Some(object) => object, None => return, }; object.insert(key.to_string(), value); } fn read_u8(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 1 { return None; } return Some(data[offset]); } fn read_u16_le(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 2 { return None; } let bytes = [data[offset], data[offset + 1]]; return Some(u16::from_le_bytes(bytes)); } fn read_u64_le(data: &[u8], offset: usize) -> std::option::Option { if data.len() < offset + 8 { return None; } let bytes = [ data[offset], data[offset + 1], data[offset + 2], data[offset + 3], data[offset + 4], data[offset + 5], data[offset + 6], data[offset + 7], ]; return Some(u64::from_le_bytes(bytes)); } fn bytes_to_hex(bytes: &[u8]) -> std::string::String { let mut output = std::string::String::new(); for byte in bytes { output.push_str(format!("{byte:02x}").as_str()); } return output; } #[derive(Debug, Clone)] struct TokenBalanceRecord { account_address: std::option::Option, mint: std::string::String, owner: std::option::Option, pre_amount_raw: std::option::Option, post_amount_raw: std::option::Option, } #[derive(Debug, Clone)] struct TokenBalanceAccumulator { account_index: std::option::Option, account_address: std::option::Option, mint: std::string::String, owner: std::option::Option, pre_amount_raw: std::option::Option, post_amount_raw: std::option::Option, } #[derive(Debug, Clone)] struct AccountKeyInfo { index: i64, address: std::string::String, } #[derive(Debug, Clone)] struct VaultPair { vault_a: std::string::String, vault_a_mint: std::string::String, vault_b: std::string::String, vault_b_mint: std::string::String, } #[derive(Debug, Clone)] struct NormalizedVaultPair { raw_vault_a: std::string::String, raw_vault_b: std::string::String, base_vault: std::string::String, quote_vault: std::string::String, base_mint: std::string::String, quote_mint: std::string::String, } #[derive(Clone, Copy)] struct RaydiumAmmV4InstructionIdentity { instruction_name: &'static str, event_kind: &'static str, discriminator_hex: &'static str, } fn raydium_amm_v4_swap_instruction( data_base58: std::option::Option<&str>, ) -> std::option::Option { let identity = match raydium_amm_v4_instruction_identity(data_base58) { Some(identity) => identity, None => return None, }; match identity.discriminator_hex { "09" | "0b" | "10" | "11" => return Some(identity), _ => return None, } } fn raydium_amm_v4_instruction_identity( data_base58: std::option::Option<&str>, ) -> std::option::Option { let discriminator_hex = match raydium_amm_v4_instruction_discriminator_hex(data_base58) { Some(discriminator_hex) => discriminator_hex, None => return None, }; match discriminator_hex.as_str() { "00" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "initialize", event_kind: "raydium_amm_v4.initialize", discriminator_hex: "00", }); }, "01" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "initialize2", event_kind: "raydium_amm_v4.initialize2_pool", discriminator_hex: "01", }); }, "02" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "monitor_step", event_kind: "raydium_amm_v4.monitor_step", discriminator_hex: "02", }); }, "03" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "deposit", event_kind: "raydium_amm_v4.deposit", discriminator_hex: "03", }); }, "04" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "withdraw", event_kind: "raydium_amm_v4.withdraw", discriminator_hex: "04", }); }, "05" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "migrate_to_open_book", event_kind: "raydium_amm_v4.migrate_to_open_book", discriminator_hex: "05", }); }, "06" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "set_params", event_kind: "raydium_amm_v4.set_params", discriminator_hex: "06", }); }, "07" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "withdraw_pnl", event_kind: "raydium_amm_v4.withdraw_pnl", discriminator_hex: "07", }); }, "08" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "withdraw_srm", event_kind: "raydium_amm_v4.withdraw_srm", discriminator_hex: "08", }); }, "09" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "swap_base_in", event_kind: "raydium_amm_v4.swap_base_in", discriminator_hex: "09", }); }, "0a" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "pre_initialize", event_kind: "raydium_amm_v4.pre_initialize", discriminator_hex: "0a", }); }, "0b" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "swap_base_out", event_kind: "raydium_amm_v4.swap_base_out", discriminator_hex: "0b", }); }, "0c" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "simulate_info", event_kind: "raydium_amm_v4.simulate_info", discriminator_hex: "0c", }); }, "0d" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "admin_cancel_orders", event_kind: "raydium_amm_v4.admin_cancel_orders", discriminator_hex: "0d", }); }, "0e" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "create_config_account", event_kind: "raydium_amm_v4.create_config_account", discriminator_hex: "0e", }); }, "0f" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "update_config_account", event_kind: "raydium_amm_v4.update_config_account", discriminator_hex: "0f", }); }, "10" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "swap_base_in_v2", event_kind: "raydium_amm_v4.swap_base_in_v2", discriminator_hex: "10", }); }, "11" => { return Some(RaydiumAmmV4InstructionIdentity { instruction_name: "swap_base_out_v2", event_kind: "raydium_amm_v4.swap_base_out_v2", discriminator_hex: "11", }); }, _ => return None, } } fn raydium_amm_v4_instruction_discriminator_hex( data_base58: std::option::Option<&str>, ) -> std::option::Option { let data_base58 = match data_base58 { Some(data_base58) => data_base58, None => return None, }; let bytes_result = bs58::decode(data_base58).into_vec(); let bytes = match bytes_result { Ok(bytes) => bytes, Err(_) => return None, }; let first_byte = match bytes.first() { Some(first_byte) => first_byte, None => return None, }; return Some(format!("{first_byte:02x}")); } fn parse_transaction_meta_value( transaction: &crate::ChainTransactionDto, transaction_json: &serde_json::Value, ) -> Result, crate::Error> { if let Some(meta_json) = transaction.meta_json.as_deref() { let meta_result = serde_json::from_str::(meta_json); match meta_result { Ok(meta) => return Ok(Some(meta)), Err(error) => { return Err(crate::Error::Json(format!( "cannot parse meta_json for signature '{}': {}", transaction.signature, error ))); }, } } let meta = transaction_json.get("meta"); match meta { Some(meta) => return Ok(Some(meta.clone())), None => return Ok(None), } } fn extract_transaction_account_keys( transaction: &serde_json::Value, meta: std::option::Option<&serde_json::Value>, ) -> std::vec::Vec { let mut account_keys = std::vec::Vec::new(); let values = transaction .get("transaction") .and_then(|value| return value.get("message")) .and_then(|value| return value.get("accountKeys")) .and_then(serde_json::Value::as_array); if let Some(values) = values { let mut index = 0usize; for value in values { let parsed = parse_account_key_info(value, index as i64); if let Some(parsed) = parsed { account_keys.push(parsed); } index += 1; } } append_loaded_addresses(&mut account_keys, meta, "writable"); append_loaded_addresses(&mut account_keys, meta, "readonly"); return account_keys; } fn parse_account_key_info( value: &serde_json::Value, index: i64, ) -> std::option::Option { if let Some(address) = value.as_str() { return Some(AccountKeyInfo { index, address: address.to_string() }); } let address = match value.get("pubkey").and_then(serde_json::Value::as_str) { Some(address) => address.to_string(), None => return None, }; return Some(AccountKeyInfo { index, address }); } fn append_loaded_addresses( account_keys: &mut std::vec::Vec, meta: std::option::Option<&serde_json::Value>, key: &str, ) { let addresses = meta .and_then(|value| return value.get("loadedAddresses")) .and_then(|value| return value.get(key)) .and_then(serde_json::Value::as_array); let addresses = match addresses { Some(addresses) => addresses, None => return, }; for value in addresses { let address = match value.as_str() { Some(address) => address, None => continue, }; let index = account_keys.len() as i64; account_keys.push(AccountKeyInfo { index, address: address.to_string() }); } } fn extract_token_balance_records( meta: std::option::Option<&serde_json::Value>, account_keys: &[AccountKeyInfo], ) -> std::vec::Vec { let mut accumulators = std::vec::Vec::new(); collect_token_balance_side(meta, account_keys, "preTokenBalances", true, &mut accumulators); collect_token_balance_side(meta, account_keys, "postTokenBalances", false, &mut accumulators); let mut records = std::vec::Vec::new(); for accumulator in accumulators { records.push(TokenBalanceRecord { account_address: accumulator.account_address, mint: accumulator.mint, owner: accumulator.owner, pre_amount_raw: accumulator.pre_amount_raw, post_amount_raw: accumulator.post_amount_raw, }); } return records; } fn collect_token_balance_side( meta: std::option::Option<&serde_json::Value>, account_keys: &[AccountKeyInfo], key: &str, is_pre: bool, accumulators: &mut std::vec::Vec, ) { let values = meta .and_then(|value| return value.get(key)) .and_then(serde_json::Value::as_array); let values = match values { Some(values) => values, None => return, }; for value in values { let account_index = value.get("accountIndex").and_then(serde_json::Value::as_i64); let mint = match value.get("mint").and_then(serde_json::Value::as_str) { Some(mint) => mint.to_string(), None => continue, }; let owner = value .get("owner") .and_then(serde_json::Value::as_str) .map(|text| return text.to_string()); let amount = value .get("uiTokenAmount") .and_then(|amount| return amount.get("amount")) .and_then(serde_json::Value::as_str) .map(|text| return text.to_string()); let account_address = match account_index { Some(account_index) => account_address_by_index(account_keys, account_index), None => None, }; let accumulator_index = find_token_balance_accumulator(accumulators.as_slice(), account_index, mint.as_str()); let index = match accumulator_index { Some(index) => index, None => { accumulators.push(TokenBalanceAccumulator { account_index, account_address, mint, owner: owner.clone(), pre_amount_raw: None, post_amount_raw: None, }); accumulators.len() - 1 }, }; if accumulators[index].owner.is_none() { accumulators[index].owner = owner; } if is_pre { accumulators[index].pre_amount_raw = amount; } else { accumulators[index].post_amount_raw = amount; } } } fn find_token_balance_accumulator( accumulators: &[TokenBalanceAccumulator], account_index: std::option::Option, mint: &str, ) -> std::option::Option { let mut index = 0usize; while index < accumulators.len() { let accumulator = &accumulators[index]; if accumulator.account_index == account_index && accumulator.mint == mint { return Some(index); } index += 1; } return None; } fn account_address_by_index( account_keys: &[AccountKeyInfo], account_index: i64, ) -> std::option::Option { for account_key in account_keys { if account_key.index == account_index { return Some(account_key.address.clone()); } } return None; } fn infer_raydium_amm_v4_vault_pair( accounts: &[std::string::String], authority: &str, token_balances: &[TokenBalanceRecord], ) -> std::option::Option { let positional = infer_positional_vault_pair(accounts, token_balances); if positional.is_some() { return positional; } return infer_authority_owned_vault_pair(accounts, authority, token_balances); } fn infer_positional_vault_pair( accounts: &[std::string::String], token_balances: &[TokenBalanceRecord], ) -> std::option::Option { if accounts.len() < 5 { return None; } let vault_a = accounts[3].clone(); let vault_b = accounts[4].clone(); let mint_a = mint_for_account(token_balances, vault_a.as_str()); let mint_b = mint_for_account(token_balances, vault_b.as_str()); match (mint_a, mint_b) { (Some(mint_a), Some(mint_b)) => { if mint_a == mint_b { return None; } return Some(VaultPair { vault_a, vault_a_mint: mint_a, vault_b, vault_b_mint: mint_b, }); }, _ => return None, } } fn infer_authority_owned_vault_pair( accounts: &[std::string::String], authority: &str, token_balances: &[TokenBalanceRecord], ) -> std::option::Option { let mut candidates = std::vec::Vec::new(); for account in accounts { if is_known_program_or_sysvar(account.as_str()) { continue; } let record = token_balance_record_for_account(token_balances, account.as_str()); let record = match record { Some(record) => record, None => continue, }; if record.owner.as_deref() != Some(authority) { continue; } if record.delta_raw().is_none() { continue; } if candidates .iter() .any(|candidate: &TokenBalanceRecord| return candidate.mint == record.mint) { continue; } candidates.push(record.clone()); if candidates.len() >= 2 { break; } } if candidates.len() < 2 { return None; } let first_address = match &candidates[0].account_address { Some(address) => address.clone(), None => return None, }; let second_address = match &candidates[1].account_address { Some(address) => address.clone(), None => return None, }; return Some(VaultPair { vault_a: first_address, vault_a_mint: candidates[0].mint.clone(), vault_b: second_address, vault_b_mint: candidates[1].mint.clone(), }); } impl TokenBalanceRecord { fn delta_raw(&self) -> std::option::Option { let pre = parse_i128_or_zero(self.pre_amount_raw.as_deref()); let post = parse_i128_or_zero(self.post_amount_raw.as_deref()); match (pre, post) { (Some(pre), Some(post)) => return Some(post - pre), _ => return None, } } } fn mint_for_account( token_balances: &[TokenBalanceRecord], account: &str, ) -> std::option::Option { let record = token_balance_record_for_account(token_balances, account); match record { Some(record) => return Some(record.mint.clone()), None => return None, } } fn token_balance_record_for_account<'a>( token_balances: &'a [TokenBalanceRecord], account: &str, ) -> std::option::Option<&'a TokenBalanceRecord> { for record in token_balances { if record.account_address.as_deref() == Some(account) { return Some(record); } } return None; } fn normalize_pool_pair(vault_pair: VaultPair) -> NormalizedVaultPair { let vault_a_is_quote = is_quote_mint(vault_pair.vault_a_mint.as_str()); let vault_b_is_quote = is_quote_mint(vault_pair.vault_b_mint.as_str()); if vault_a_is_quote && !vault_b_is_quote { return NormalizedVaultPair { raw_vault_a: vault_pair.vault_a.clone(), raw_vault_b: vault_pair.vault_b.clone(), base_vault: vault_pair.vault_b, quote_vault: vault_pair.vault_a, base_mint: vault_pair.vault_b_mint, quote_mint: vault_pair.vault_a_mint, }; } if vault_b_is_quote && !vault_a_is_quote { return NormalizedVaultPair { raw_vault_a: vault_pair.vault_a.clone(), raw_vault_b: vault_pair.vault_b.clone(), base_vault: vault_pair.vault_a, quote_vault: vault_pair.vault_b, base_mint: vault_pair.vault_a_mint, quote_mint: vault_pair.vault_b_mint, }; } return NormalizedVaultPair { raw_vault_a: vault_pair.vault_a.clone(), raw_vault_b: vault_pair.vault_b.clone(), base_vault: vault_pair.vault_a, quote_vault: vault_pair.vault_b, base_mint: vault_pair.vault_a_mint, quote_mint: vault_pair.vault_b_mint, }; } fn is_quote_mint(mint: &str) -> bool { return mint == crate::WSOL_MINT_ID || mint == crate::USDC_MINT_ID || mint == crate::USDT_MINT_ID; } fn amount_delta_abs_for_account( token_balances: &[TokenBalanceRecord], account: &str, ) -> std::option::Option { let record = token_balance_record_for_account(token_balances, account); let record = match record { Some(record) => record, None => return None, }; let delta = record.delta_raw(); let delta = match delta { Some(delta) => delta, None => return None, }; if delta < 0 { return Some((-delta).to_string()); } return Some(delta.to_string()); } fn infer_trade_side_from_vault_deltas( token_balances: &[TokenBalanceRecord], base_vault: &str, quote_vault: &str, ) -> std::option::Option { let base_record = token_balance_record_for_account(token_balances, base_vault); let base_record = match base_record { Some(base_record) => base_record, None => return None, }; let quote_record = token_balance_record_for_account(token_balances, quote_vault); let quote_record = match quote_record { Some(quote_record) => quote_record, None => return None, }; let base_delta = base_record.delta_raw(); let quote_delta = quote_record.delta_raw(); match (base_delta, quote_delta) { (Some(base_delta), Some(quote_delta)) => { if base_delta < 0 && quote_delta > 0 { return Some("BuyBase".to_string()); } if base_delta > 0 && quote_delta < 0 { return Some("SellBase".to_string()); } return None; }, _ => return None, } } fn infer_user_token_accounts( instruction_accounts: &[std::string::String], token_balances: &[TokenBalanceRecord], authority: &str, base_mint: &str, quote_mint: &str, base_vault: &str, quote_vault: &str, trade_side: std::option::Option<&str>, ) -> ( std::option::Option, std::option::Option, ) { let user_base = find_user_token_account( instruction_accounts, token_balances, authority, base_mint, base_vault, ); let user_quote = find_user_token_account( instruction_accounts, token_balances, authority, quote_mint, quote_vault, ); match trade_side { Some("BuyBase") => return (user_quote, user_base), Some("SellBase") => return (user_base, user_quote), _ => return (None, None), } } fn find_user_token_account( instruction_accounts: &[std::string::String], token_balances: &[TokenBalanceRecord], authority: &str, mint: &str, vault: &str, ) -> std::option::Option { for account in instruction_accounts { if account == vault { continue; } let record = token_balance_record_for_account(token_balances, account.as_str()); let record = match record { Some(record) => record, None => continue, }; if record.mint != mint { continue; } if record.owner.as_deref() == Some(authority) { continue; } return Some(account.clone()); } return None; } fn input_output_vaults_for_trade_side( trade_side: std::option::Option<&str>, base_vault: &str, quote_vault: &str, ) -> ( std::option::Option, std::option::Option, ) { match trade_side { Some("BuyBase") => return (Some(quote_vault.to_string()), Some(base_vault.to_string())), Some("SellBase") => return (Some(base_vault.to_string()), Some(quote_vault.to_string())), _ => return (None, None), } } fn parent_program_id_for_instruction( instruction: &crate::ChainInstructionDto, instructions: &[crate::ChainInstructionDto], ) -> std::option::Option { let parent_id = match instruction.parent_instruction_id { Some(parent_id) => parent_id, None => return None, }; for candidate in instructions { if candidate.id == Some(parent_id) { return candidate.program_id.clone(); } } return None; } fn route_source_from_parent( parent_program: std::option::Option<&str>, ) -> std::option::Option { let parent_program = match parent_program { Some(parent_program) => parent_program, None => return None, }; if parent_program == OBSERVED_JUPITER_V6_PROGRAM_ID { return Some("jupiter".to_string()); } return Some(parent_program.to_string()); } fn parse_i128_or_zero(value: std::option::Option<&str>) -> std::option::Option { let value = match value { Some(value) => value.trim(), None => return Some(0), }; if value.is_empty() { return Some(0); } let parsed_result = value.parse::(); match parsed_result { Ok(parsed) => return Some(parsed), Err(_) => return None, } } fn is_known_program_or_sysvar(account: &str) -> bool { if account == crate::SYSTEM_PROGRAM_ID { return true; } if account == crate::SPL_TOKEN_PROGRAM_ID { return true; } if account == crate::SPL_TOKEN_2022_PROGRAM_ID { return true; } if account == crate::ASSOCIATED_TOKEN_PROGRAM_ID { return true; } if account == crate::RAYDIUM_AMM_V4_PROGRAM_ID { return true; } if account == OBSERVED_JUPITER_V6_PROGRAM_ID { return true; } return false; } fn extract_log_messages( transaction_json: &serde_json::Value, ) -> std::vec::Vec { let mut messages = std::vec::Vec::new(); let meta_option = transaction_json.get("meta"); let meta = match meta_option { Some(meta) => meta, None => return messages, }; let logs_option = meta.get("logMessages"); let logs = match logs_option { Some(logs) => logs, None => return messages, }; let logs_array_option = logs.as_array(); let logs_array = match logs_array_option { Some(logs_array) => logs_array, None => return messages, }; for value in logs_array { let text_option = value.as_str(); if let Some(text) = text_option { messages.push(text.to_string()); } } return messages; } fn log_messages_contain_initialize2(log_messages: &[std::string::String]) -> bool { for log_message in log_messages { if log_message.contains("initialize2") { return true; } } return false; } fn parse_accounts_json( accounts_json: &str, ) -> Result, crate::Error> { let values_result = serde_json::from_str::>(accounts_json); let values = match values_result { Ok(values) => values, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse instruction accounts_json '{}': {}", accounts_json, error ))); }, }; let mut accounts = std::vec::Vec::new(); for value in values { let text_option = value.as_str(); if let Some(text) = text_option { accounts.push(text.to_string()); } } return Ok(accounts); } fn parse_optional_data_json_as_base58( data_json: std::option::Option<&str>, ) -> std::option::Option { let data_json = match data_json { Some(data_json) => data_json, None => return None, }; let json_string_result = serde_json::from_str::(data_json); if let Ok(value) = json_string_result { return Some(value); } let trimmed = data_json.trim(); if trimmed.is_empty() { return None; } let without_quotes = trimmed.trim_matches('"'); if without_quotes.is_empty() { return None; } return Some(without_quotes.to_string()); } fn extract_account( accounts: &[std::string::String], index: usize, ) -> std::option::Option { if index >= accounts.len() { return None; } return Some(accounts[index].clone()); } #[cfg(test)] mod tests { fn make_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::new( "sig-raydium-test-1".to_string(), Some(888888), Some(1778000000), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 888888, "meta": { "logMessages": [ "Program log: initialize2" ] }, "transaction": { "message": { "instructions": [], "accountKeys": [] } } }) .to_string(), ); dto.id = Some(42); return dto; } fn make_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 42, None, 0, None, Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()), Some("raydium_amm_v4".to_string()), Some(1), serde_json::json!([ "Account0", "Account1", "Account2", "Account3", "Pool111", "Account5", "Account6", "LpMint111", "TokenA111", "TokenB111", "Account10", "Account11", "Account12", "Account13", "Account14", "Account15", "Market111" ]) .to_string(), None, None, None, ); dto.id = Some(7); return dto; } fn make_swap_transaction() -> crate::ChainTransactionDto { let meta = serde_json::json!({ "err": null, "preTokenBalances": [ { "accountIndex": 3, "mint": "BaseMint111", "owner": "Authority111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "1000", "decimals": 6 } }, { "accountIndex": 4, "mint": crate::WSOL_MINT_ID, "owner": "Authority111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "2000", "decimals": 9 } }, { "accountIndex": 6, "mint": "BaseMint111", "owner": "User111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "50", "decimals": 6 } }, { "accountIndex": 7, "mint": crate::WSOL_MINT_ID, "owner": "User111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "500", "decimals": 9 } } ], "postTokenBalances": [ { "accountIndex": 3, "mint": "BaseMint111", "owner": "Authority111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "900", "decimals": 6 } }, { "accountIndex": 4, "mint": crate::WSOL_MINT_ID, "owner": "Authority111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "2100", "decimals": 9 } }, { "accountIndex": 6, "mint": "BaseMint111", "owner": "User111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "150", "decimals": 6 } }, { "accountIndex": 7, "mint": crate::WSOL_MINT_ID, "owner": "User111", "programId": crate::SPL_TOKEN_PROGRAM_ID, "uiTokenAmount": { "amount": "400", "decimals": 9 } } ], "innerInstructions": [] }); let transaction_json = serde_json::json!({ "slot": 999999, "meta": meta, "transaction": { "message": { "accountKeys": [ crate::SPL_TOKEN_PROGRAM_ID, "Pool111", "Authority111", "BaseVault111", "QuoteVault111", "AmmOpenOrders111", "UserBase111", "UserQuote111" ], "instructions": [] } } }); let mut dto = crate::ChainTransactionDto::new( "sig-raydium-swap-1".to_string(), Some(999999), Some(1778000001), Some("helius_primary_http".to_string()), Some("0".to_string()), None, Some(meta.to_string()), transaction_json.to_string(), ); dto.id = Some(100); return dto; } fn make_swap_instruction() -> crate::ChainInstructionDto { return make_swap_instruction_with_data("63SfuT4qF7xK35jRTGqxuUT"); } fn make_swap_v2_instruction() -> crate::ChainInstructionDto { return make_swap_instruction_with_data("9rj8cBJgMm4L1xvzfy5AUsy"); } fn make_swap_instruction_with_data(data_base58: &str) -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 100, Some(55), 4, Some(0), Some(crate::RAYDIUM_AMM_V4_PROGRAM_ID.to_string()), Some("raydium_amm_v4".to_string()), Some(2), serde_json::json!([ crate::SPL_TOKEN_PROGRAM_ID, "Pool111", "Authority111", "BaseVault111", "QuoteVault111", "AmmOpenOrders111", "UserBase111", "UserQuote111" ]) .to_string(), Some(serde_json::json!(data_base58).to_string()), None, None, ); dto.id = Some(200); return dto; } #[test] fn raydium_amm_v4_initialize2_logs_are_detected() { let decoder = crate::RaydiumAmmV4Decoder::new(); let transaction = make_transaction(); let instructions = vec![make_instruction()]; let decoded_result = decoder.decode_transaction(&transaction, &instructions); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => panic!("decode must succeed: {}", error), }; assert_eq!(decoded.len(), 1); match &decoded[0] { crate::RaydiumAmmV4DecodedEvent::Initialize2Pool(event) => { assert_eq!(event.transaction_id, 42); assert_eq!(event.instruction_id, 7); assert_eq!(event.pool_account, Some("Pool111".to_string())); assert_eq!(event.lp_mint, Some("LpMint111".to_string())); assert_eq!(event.token_a_mint, Some("TokenA111".to_string())); assert_eq!(event.token_b_mint, Some("TokenB111".to_string())); assert_eq!(event.market_account, Some("Market111".to_string())); }, _ => panic!("expected initialize2 event"), } } #[test] fn raydium_amm_v4_initialize2_returns_none_without_expected_log() { let decoder = crate::RaydiumAmmV4Decoder::new(); let mut transaction = make_transaction(); transaction.transaction_json = serde_json::json!({ "slot": 888888, "meta": { "logMessages": [ "Program log: swap" ] }, "transaction": { "message": { "instructions": [], "accountKeys": [] } } }) .to_string(); let instructions = vec![make_instruction()]; let decoded_result = decoder.decode_transaction(&transaction, &instructions); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => panic!("decode must succeed: {}", error), }; assert_eq!(decoded.len(), 0); } #[test] fn raydium_amm_v4_swap_is_decoded_from_inner_instruction_and_vault_deltas() { let decoder = crate::RaydiumAmmV4Decoder::new(); let transaction = make_swap_transaction(); let instructions = vec![make_swap_instruction()]; let decoded_result = decoder.decode_transaction(&transaction, &instructions); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => panic!("decode must succeed: {}", error), }; assert_eq!(decoded.len(), 1); match &decoded[0] { crate::RaydiumAmmV4DecodedEvent::Swap(event) => { assert_eq!(event.transaction_id, 100); assert_eq!(event.instruction_id, 200); assert_eq!(event.pool_account, "Pool111".to_string()); assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in".to_string()); assert_eq!(event.instruction_name, "swap_base_in".to_string()); assert_eq!(event.discriminator_hex, "09".to_string()); assert_eq!(event.token_a_mint, "BaseMint111".to_string()); assert_eq!(event.token_b_mint, crate::WSOL_MINT_ID.to_string()); assert_eq!(event.base_vault, "BaseVault111".to_string()); assert_eq!(event.quote_vault, "QuoteVault111".to_string()); assert_eq!(event.base_amount_raw, Some("100".to_string())); assert_eq!(event.quote_amount_raw, Some("100".to_string())); assert_eq!(event.trade_side, Some("BuyBase".to_string())); assert_eq!(event.input_vault, Some("QuoteVault111".to_string())); assert_eq!(event.output_vault, Some("BaseVault111".to_string())); assert_eq!(event.input_token_account, Some("UserQuote111".to_string())); assert_eq!(event.output_token_account, Some("UserBase111".to_string())); }, _ => panic!("expected swap event"), } } #[test] fn raydium_amm_v4_swap_base_in_v2_is_decoded_from_inner_instruction_and_vault_deltas() { let decoder = crate::RaydiumAmmV4Decoder::new(); let transaction = make_swap_transaction(); let instructions = vec![make_swap_v2_instruction()]; let decoded_result = decoder.decode_transaction(&transaction, &instructions); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => panic!("decode must succeed: {}", error), }; assert_eq!(decoded.len(), 1); match &decoded[0] { crate::RaydiumAmmV4DecodedEvent::Swap(event) => { assert_eq!(event.event_kind, "raydium_amm_v4.swap_base_in_v2".to_string()); assert_eq!(event.instruction_name, "swap_base_in_v2".to_string()); assert_eq!(event.discriminator_hex, "10".to_string()); assert_eq!(event.pool_account, "Pool111".to_string()); assert_eq!(event.base_amount_raw, Some("100".to_string())); assert_eq!(event.quote_amount_raw, Some("100".to_string())); assert_eq!(event.trade_side, Some("BuyBase".to_string())); }, _ => panic!("expected swap event"), } } }