// file: kb_lib/src/dex/raydium_amm_v4.rs //! Raydium AmmV4 transaction decoder. /// 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 event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum RaydiumAmmV4DecodedEvent { /// `initialize2` pool creation-like event. Initialize2Pool(RaydiumAmmV4Initialize2PoolDecoded), } /// 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 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 { if instruction.parent_instruction_id.is_some() { continue; } 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; } if !has_initialize2_log { 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 accounts.len() < 10 { continue; } 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 payload_json = serde_json::json!({ "decoder": "raydium_amm_v4", "eventKind": "initialize2_pool", "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, "accounts": accounts, "logMessages": log_messages, "poolAccount": pool_account, "lpMint": lp_mint, "tokenAMint": token_a_mint, "tokenBMint": token_b_mint, "marketAccount": market_account }); decoded_events.push(crate::RaydiumAmmV4DecodedEvent::Initialize2Pool( crate::RaydiumAmmV4Initialize2PoolDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.clone(), pool_account, lp_mint, token_a_mint, token_b_mint, market_account, payload_json, }, )); } return Ok(decoded_events); } } 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 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": [] } } }) .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; } #[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())); }, } } #[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": [] } } }) .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); } }