// file: kb_lib/src/dex/meteora_damm_v1.rs //! Meteora DAMM v1 transaction decoder. const DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL: [u8; 8] = [0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28]; const DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL_WITH_CONFIG: [u8; 8] = [0x49, 0xfe, 0x76, 0xf3, 0xab, 0xc4, 0x4c, 0xd0]; const DAMM_V1_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; /// Decoded Meteora DAMM v1 create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MeteoraDammV1CreatePoolDecoded { /// 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 token A mint. pub token_a_mint: std::option::Option, /// Optional token B mint. pub token_b_mint: std::option::Option, /// Optional config account. pub config_account: std::option::Option, /// Optional creator / payer. pub creator: std::option::Option, /// Whether the create path used an explicit config. pub used_config: bool, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Meteora DAMM v1 swap event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MeteoraDammV1SwapDecoded { /// 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, /// Trade side relative to normalized base. pub trade_side: crate::SwapTradeSide, /// Optional pool account. pub pool_account: 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, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Meteora DAMM v1 event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MeteoraDammV1DecodedEvent { /// Pool creation. CreatePool(MeteoraDammV1CreatePoolDecoded), /// Swap. Swap(MeteoraDammV1SwapDecoded), } /// Meteora DAMM v1 decoder. #[derive(Debug, Clone, Default)] pub struct MeteoraDammV1Decoder; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MeteoraDammV1InstructionKind { CreatePool, CreatePoolWithConfig, Swap, Unknown, } impl MeteoraDammV1Decoder { /// Creates a new decoder. pub fn new() -> Self { return Self; } /// Decodes one projected transaction into zero or more Meteora DAMM v1 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 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::METEORA_DAMM_V1_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), }; let parsed_json_result = parse_optional_parsed_json(instruction.parsed_json.as_ref()); let parsed_json = match parsed_json_result { Ok(parsed_json) => parsed_json, Err(error) => return Err(error), }; let instruction_data_result = decode_instruction_data_json(instruction.data_json.as_ref()); let instruction_data = match instruction_data_result { Ok(instruction_data) => instruction_data, Err(error) => return Err(error), }; if instruction.parent_instruction_id.is_some() && instruction_data.is_none() { continue; } let instruction_kind = classify_instruction_kind( parsed_json.as_ref(), instruction_data.as_deref(), &log_messages, ); let pool_account = extract_string_by_candidate_keys( parsed_json.as_ref(), &["pool", "poolAddress", "poolAccount", "amm", "ammPool", "poolState"], ) .or_else(|| return extract_account(&accounts, 0)); let token_a_mint = extract_string_by_candidate_keys( parsed_json.as_ref(), &["tokenAMint", "mintA", "baseMint", "token0Mint", "mint0", "coinMint"], ) .or_else(|| return extract_account(&accounts, 1)); let token_b_mint = extract_string_by_candidate_keys( parsed_json.as_ref(), &["tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1", "pcMint"], ) .or_else(|| return extract_account(&accounts, 2)); let config_account = extract_string_by_candidate_keys( parsed_json.as_ref(), &["config", "poolConfig", "ammConfig", "tradeFeeConfig"], ) .or_else(|| return extract_account(&accounts, 3)); let creator = extract_string_by_candidate_keys( parsed_json.as_ref(), &["creator", "payer", "user", "owner"], ) .or_else(|| return extract_account(&accounts, 4)); if instruction_kind == MeteoraDammV1InstructionKind::CreatePool || instruction_kind == MeteoraDammV1InstructionKind::CreatePoolWithConfig { let used_config = instruction_kind == MeteoraDammV1InstructionKind::CreatePoolWithConfig; let payload_json = serde_json::json!({ "decoder": "meteora_damm_v1", "eventKind": "create_pool", "dataDiscriminatorHex": instruction_data .as_ref() .and_then(|data| return first_8_bytes_hex(data.as_slice())), "classifiedInstructionKind": if used_config { "create_pool_with_config" } else { "create_pool" }, "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, "accounts": accounts, "parsed": parsed_json, "logMessages": log_messages, "poolAccount": pool_account, "tokenAMint": token_a_mint, "tokenBMint": token_b_mint, "configAccount": config_account, "creator": creator }); decoded_events.push(crate::MeteoraDammV1DecodedEvent::CreatePool( crate::MeteoraDammV1CreatePoolDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.clone(), pool_account, token_a_mint, token_b_mint, config_account, creator, used_config, payload_json, }, )); continue; } if instruction_kind == MeteoraDammV1InstructionKind::Swap { let trade_side = infer_trade_side(&log_messages); let payload_json = serde_json::json!({ "decoder": "meteora_damm_v1", "eventKind": "swap", "dataDiscriminatorHex": instruction_data .as_ref() .and_then(|data| return first_8_bytes_hex(data.as_slice())), "classifiedInstructionKind": "swap", "signature": transaction.signature, "instructionId": instruction_id, "instructionIndex": instruction.instruction_index, "accounts": accounts, "parsed": parsed_json, "logMessages": log_messages, "poolAccount": pool_account, "tokenAMint": token_a_mint, "tokenBMint": token_b_mint, "tradeSide": format!("{:?}", trade_side) }); decoded_events.push(crate::MeteoraDammV1DecodedEvent::Swap( crate::MeteoraDammV1SwapDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.clone(), trade_side, pool_account, token_a_mint, token_b_mint, payload_json, }, )); } } return Ok(decoded_events); } } fn classify_instruction_kind( parsed_json: std::option::Option<&serde_json::Value>, instruction_data: std::option::Option<&[u8]>, log_messages: &[std::string::String], ) -> MeteoraDammV1InstructionKind { let data_kind = classify_instruction_kind_from_data(instruction_data); if data_kind != MeteoraDammV1InstructionKind::Unknown { return data_kind; } if instruction_data_has_full_discriminator(instruction_data) { return MeteoraDammV1InstructionKind::Unknown; } let parsed_instruction_name = extract_string_by_candidate_keys( parsed_json, &["instruction", "instructionName", "type", "name"], ); if let Some(parsed_instruction_name) = parsed_instruction_name { let normalized = normalize_text(parsed_instruction_name.as_str()); if normalized.contains("initializepoolwithconfig") { return MeteoraDammV1InstructionKind::CreatePoolWithConfig; } if normalized.contains("initializepool") { return MeteoraDammV1InstructionKind::CreatePool; } if normalized == "swap" { return MeteoraDammV1InstructionKind::Swap; } } if value_contains_any_key(parsed_json, &["poolConfig", "ammConfig", "tradeFeeConfig"]) { return MeteoraDammV1InstructionKind::CreatePoolWithConfig; } if log_messages_contain_keyword(log_messages, "initialize_pool_with_config") || log_messages_contain_keyword(log_messages, "initializepoolwithconfig") { return MeteoraDammV1InstructionKind::CreatePoolWithConfig; } if log_messages_contain_keyword(log_messages, "initialize_pool") || log_messages_contain_keyword(log_messages, "initializepool") { return MeteoraDammV1InstructionKind::CreatePool; } if log_messages_contain_keyword(log_messages, "swap") { return MeteoraDammV1InstructionKind::Swap; } return MeteoraDammV1InstructionKind::Unknown; } fn instruction_data_has_full_discriminator(instruction_data: std::option::Option<&[u8]>) -> bool { let instruction_data = match instruction_data { Some(instruction_data) => instruction_data, None => return false, }; return instruction_data.len() >= 8; } fn classify_instruction_kind_from_data( instruction_data: std::option::Option<&[u8]>, ) -> MeteoraDammV1InstructionKind { let instruction_data = match instruction_data { Some(instruction_data) => instruction_data, None => return MeteoraDammV1InstructionKind::Unknown, }; if instruction_data.len() < 8 { return MeteoraDammV1InstructionKind::Unknown; } let discriminator = [ instruction_data[0], instruction_data[1], instruction_data[2], instruction_data[3], instruction_data[4], instruction_data[5], instruction_data[6], instruction_data[7], ]; if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL_WITH_CONFIG { return MeteoraDammV1InstructionKind::CreatePoolWithConfig; } if discriminator == DAMM_V1_DISCRIMINATOR_INITIALIZE_POOL { return MeteoraDammV1InstructionKind::CreatePool; } if discriminator == DAMM_V1_DISCRIMINATOR_SWAP { return MeteoraDammV1InstructionKind::Swap; } return MeteoraDammV1InstructionKind::Unknown; } 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_keyword(log_messages: &[std::string::String], keyword: &str) -> bool { let keyword_normalized = normalize_text(keyword); for log_message in log_messages { let log_normalized = normalize_text(log_message.as_str()); if log_normalized.contains(keyword_normalized.as_str()) { return true; } } return false; } fn normalize_text(value: &str) -> std::string::String { let mut normalized = std::string::String::new(); for character in value.chars() { if character.is_ascii_alphanumeric() { normalized.push(character.to_ascii_lowercase()); } } return normalized; } 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()); continue; } if let Some(pubkey) = value.get("pubkey").and_then(|nested| return nested.as_str()) { accounts.push(pubkey.to_string()); } } return Ok(accounts); } fn parse_optional_parsed_json( parsed_json: std::option::Option<&std::string::String>, ) -> Result, crate::Error> { let parsed_json = match parsed_json { Some(parsed_json) => parsed_json, None => return Ok(None), }; let value_result = serde_json::from_str::(parsed_json.as_str()); match value_result { Ok(value) => return Ok(Some(value)), Err(error) => { return Err(crate::Error::Json(format!( "cannot parse instruction parsed_json '{}': {}", parsed_json, error ))); }, } } fn decode_instruction_data_json( data_json: std::option::Option<&std::string::String>, ) -> Result>, crate::Error> { let data_json = match data_json { Some(data_json) => data_json, None => return Ok(None), }; let value_result = serde_json::from_str::(data_json.as_str()); let value = match value_result { Ok(value) => value, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse Meteora DAMM v1 data_json: {}", error ))); }, }; if let serde_json::Value::String(base58_text) = value { let decoded_result = bs58::decode(base58_text.as_str()).into_vec(); match decoded_result { Ok(decoded) => return Ok(Some(decoded)), Err(error) => { return Err(crate::Error::Json(format!( "cannot decode Meteora DAMM v1 instruction data from base58: {}", error ))); }, } } return Ok(None); } fn first_8_bytes_hex(bytes: &[u8]) -> std::option::Option { if bytes.len() < 8 { return None; } return Some(format!( "{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}", bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], )); } fn extract_string_by_candidate_keys( value: std::option::Option<&serde_json::Value>, candidate_keys: &[&str], ) -> std::option::Option { let value = match value { Some(value) => value, None => return None, }; return extract_string_by_candidate_keys_inner(value, candidate_keys); } fn extract_string_by_candidate_keys_inner( value: &serde_json::Value, candidate_keys: &[&str], ) -> std::option::Option { if let Some(object) = value.as_object() { for candidate_key in candidate_keys { let direct_option = object.get(*candidate_key); if let Some(direct) = direct_option { let direct_text_option = direct.as_str(); if let Some(direct_text) = direct_text_option { return Some(direct_text.to_string()); } } } for nested_value in object.values() { let nested_result = extract_string_by_candidate_keys_inner(nested_value, candidate_keys); if nested_result.is_some() { return nested_result; } } return None; } if let Some(array) = value.as_array() { for nested_value in array { let nested_result = extract_string_by_candidate_keys_inner(nested_value, candidate_keys); if nested_result.is_some() { return nested_result; } } } return None; } fn value_contains_any_key( value: std::option::Option<&serde_json::Value>, candidate_keys: &[&str], ) -> bool { let value = match value { Some(value) => value, None => return false, }; return value_contains_any_key_inner(value, candidate_keys); } fn value_contains_any_key_inner(value: &serde_json::Value, candidate_keys: &[&str]) -> bool { if let Some(object) = value.as_object() { for candidate_key in candidate_keys { if object.contains_key(*candidate_key) { return true; } } for nested_value in object.values() { if value_contains_any_key_inner(nested_value, candidate_keys) { return true; } } return false; } if let Some(array) = value.as_array() { for nested_value in array { if value_contains_any_key_inner(nested_value, candidate_keys) { return true; } } } return false; } fn extract_account( accounts: &[std::string::String], index: usize, ) -> std::option::Option { if index >= accounts.len() { return None; } return Some(accounts[index].clone()); } fn infer_trade_side(log_messages: &[std::string::String]) -> crate::SwapTradeSide { if log_messages_contain_keyword(log_messages, "buy") { return crate::SwapTradeSide::BuyBase; } if log_messages_contain_keyword(log_messages, "sell") { return crate::SwapTradeSide::SellBase; } return crate::SwapTradeSide::Unknown; } #[cfg(test)] mod tests { fn make_create_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::new( "sig-meteora-damm-v1-create-1".to_string(), Some(890001), Some(1779500001), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 890001, "meta": { "logMessages": [ "Program log: Instruction: InitializePoolWithConfig" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(501); return dto; } fn make_create_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 501, None, 0, None, Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), Some("meteora-damm-v1".to_string()), Some(1), serde_json::json!([ "DammV1Pool111", "DammV1TokenA111", crate::WSOL_MINT_ID, "DammV1Config111", "DammV1Creator111" ]) .to_string(), None, None, Some( serde_json::json!({ "info": { "instruction": "initialize_pool_with_config", "pool": "DammV1Pool111", "tokenAMint": "DammV1TokenA111", "tokenBMint": crate::WSOL_MINT_ID, "config": "DammV1Config111", "creator": "DammV1Creator111" } }) .to_string(), ), ); dto.id = Some(502); return dto; } fn make_swap_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::new( "sig-meteora-damm-v1-swap-1".to_string(), Some(890002), Some(1779500002), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 890002, "meta": { "logMessages": [ "Program log: Instruction: Swap" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(503); return dto; } fn make_swap_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 503, None, 0, None, Some(crate::METEORA_DAMM_V1_PROGRAM_ID.to_string()), Some("meteora-damm-v1".to_string()), Some(1), serde_json::json!(["DammV1SwapPool111", "DammV1SwapTokenA111", crate::WSOL_MINT_ID]) .to_string(), None, None, Some( serde_json::json!({ "info": { "instruction": "swap", "pool": "DammV1SwapPool111", "tokenAMint": "DammV1SwapTokenA111", "tokenBMint": crate::WSOL_MINT_ID } }) .to_string(), ), ); dto.id = Some(504); return dto; } #[test] fn meteora_damm_v1_create_pool_is_detected() { let decoder = crate::MeteoraDammV1Decoder::new(); let transaction = make_create_transaction(); let instructions = vec![make_create_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::MeteoraDammV1DecodedEvent::CreatePool(event) => { assert_eq!(event.transaction_id, 501); assert_eq!(event.instruction_id, 502); assert_eq!(event.pool_account, Some("DammV1Pool111".to_string())); assert_eq!(event.token_a_mint, Some("DammV1TokenA111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); assert!(event.used_config); }, crate::MeteoraDammV1DecodedEvent::Swap(_) => { panic!("unexpected swap event") }, } } #[test] fn meteora_damm_v1_swap_is_detected() { let decoder = crate::MeteoraDammV1Decoder::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::MeteoraDammV1DecodedEvent::Swap(event) => { assert_eq!(event.transaction_id, 503); assert_eq!(event.instruction_id, 504); assert_eq!(event.pool_account, Some("DammV1SwapPool111".to_string())); assert_eq!(event.token_a_mint, Some("DammV1SwapTokenA111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); }, crate::MeteoraDammV1DecodedEvent::CreatePool(_) => { panic!("unexpected create event") }, } } #[test] fn meteora_damm_v1_swap_discriminator_is_detected() { let data = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8, 0x01]; let kind = super::classify_instruction_kind_from_data(Some(&data)); assert_eq!(kind, super::MeteoraDammV1InstructionKind::Swap); } #[test] fn meteora_damm_v1_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() { let data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01]; let logs = vec!["Program log: Instruction: Swap".to_string()]; let kind = super::classify_instruction_kind(None, Some(&data), &logs); assert_eq!(kind, super::MeteoraDammV1InstructionKind::Unknown); } #[test] fn meteora_damm_v1_inner_swap_instruction_with_data_is_not_skipped() { let decoder = crate::MeteoraDammV1Decoder::new(); let transaction = make_swap_transaction(); let mut instruction = make_swap_instruction(); instruction.parent_instruction_id = Some(500); instruction.data_json = Some(format!( "\"{}\"", bs58::encode(&[0xf8_u8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8, 0x01]).into_string() )); let decoded_result = decoder.decode_transaction(&transaction, &[instruction]); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => panic!("decode must succeed: {}", error), }; assert_eq!(decoded.len(), 1); match &decoded[0] { crate::MeteoraDammV1DecodedEvent::Swap(event) => { assert_eq!(event.pool_account, Some("DammV1SwapPool111".to_string())); }, crate::MeteoraDammV1DecodedEvent::CreatePool(_) => panic!("unexpected create event"), } } }