// file: kb_lib/src/dex/meteora_dlmm.rs //! Meteora DLMM transaction decoder. //! //! This first decoder version is intentionally conservative. It only emits //! decoded events when the projected instruction or transaction logs expose //! clear DLMM create/swap hints. const DLMM_DISCRIMINATOR_CLAIM_FEE2: [u8; 8] = [0x70, 0xbf, 0x65, 0xab, 0x1c, 0x90, 0x7f, 0xbb]; const DLMM_DISCRIMINATOR_INITIALIZE_POSITION: [u8; 8] = [0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50]; const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR: [u8; 8] = [0x2d, 0x9a, 0xed, 0xd2, 0xdd, 0x0f, 0xa6, 0x5c]; const DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2: [u8; 8] = [0x49, 0x3b, 0x24, 0x78, 0xed, 0x53, 0x6c, 0xc6]; const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR: [u8; 8] = [0x2e, 0x27, 0x29, 0x87, 0x6f, 0xb7, 0xc8, 0x40]; const DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2: [u8; 8] = [0xf3, 0x49, 0x81, 0x7e, 0x33, 0x13, 0xf1, 0x6b]; const DLMM_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; const DLMM_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88]; const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT: [u8; 8] = [0xfa, 0x49, 0x65, 0x21, 0x26, 0xcf, 0x4b, 0xb8]; const DLMM_DISCRIMINATOR_SWAP_EXACT_OUT2: [u8; 8] = [0x2b, 0xd7, 0xf7, 0x84, 0x89, 0x3c, 0xf3, 0x51]; const DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT: [u8; 8] = [0x38, 0xad, 0xe6, 0xd0, 0xad, 0xe4, 0x9c, 0xcd]; /// Decoded Meteora DLMM create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MeteoraDlmmCreatePoolDecoded { /// 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 DLMM pair/pool account. pub pool_account: std::option::Option, /// Optional token X/base mint. pub token_a_mint: std::option::Option, /// Optional token Y/quote mint. pub token_b_mint: std::option::Option, /// Optional preset/config account. pub config_account: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Meteora DLMM swap event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MeteoraDlmmSwapDecoded { /// 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, when inferable. pub trade_side: crate::SwapTradeSide, /// Optional DLMM pair/pool account. pub pool_account: std::option::Option, /// Optional token X/base mint. pub token_a_mint: std::option::Option, /// Optional token Y/quote mint. pub token_b_mint: std::option::Option, /// Optional reserve X token account. pub reserve_x_account: std::option::Option, /// Optional reserve Y token account. pub reserve_y_account: std::option::Option, /// Optional user token-in account. pub user_token_in_account: std::option::Option, /// Optional user token-out account. pub user_token_out_account: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Meteora DLMM event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum MeteoraDlmmDecodedEvent { /// DLMM pair/pool creation. CreatePool(MeteoraDlmmCreatePoolDecoded), /// DLMM swap. Swap(MeteoraDlmmSwapDecoded), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MeteoraDlmmInstructionKind { CreatePool, Swap, Ignore, Unknown, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MeteoraDlmmInstructionName { InitializeLbPair, InitializeLbPair2, InitializeCustomizablePermissionlessLbPair, InitializeCustomizablePermissionlessLbPair2, Swap, Swap2, SwapExactOut, SwapExactOut2, SwapWithPriceImpact, ClaimFee2, InitializePosition, Unknown, } impl MeteoraDlmmInstructionName { fn as_str(&self) -> &'static str { match self { Self::InitializeLbPair => return "initialize_lb_pair", Self::InitializeLbPair2 => return "initialize_lb_pair2", Self::InitializeCustomizablePermissionlessLbPair => { return "initialize_customizable_permissionless_lb_pair"; }, Self::InitializeCustomizablePermissionlessLbPair2 => { return "initialize_customizable_permissionless_lb_pair2"; }, Self::Swap => return "swap", Self::Swap2 => return "swap2", Self::SwapExactOut => return "swap_exact_out", Self::SwapExactOut2 => return "swap_exact_out2", Self::SwapWithPriceImpact => return "swap_with_price_impact", Self::ClaimFee2 => return "claim_fee2", Self::InitializePosition => return "initialize_position", Self::Unknown => return "unknown", } } fn kind(&self) -> MeteoraDlmmInstructionKind { match self { Self::InitializeLbPair | Self::InitializeLbPair2 | Self::InitializeCustomizablePermissionlessLbPair | Self::InitializeCustomizablePermissionlessLbPair2 => { return MeteoraDlmmInstructionKind::CreatePool; }, Self::Swap | Self::Swap2 | Self::SwapExactOut | Self::SwapExactOut2 | Self::SwapWithPriceImpact => return MeteoraDlmmInstructionKind::Swap, Self::Unknown => return MeteoraDlmmInstructionKind::Unknown, Self::ClaimFee2 | Self::InitializePosition => { return MeteoraDlmmInstructionKind::Ignore; }, } } } /// Meteora DLMM decoder. #[derive(Debug, Clone, Default)] pub struct MeteoraDlmmDecoder; impl MeteoraDlmmDecoder { /// Creates a new decoder. pub fn new() -> Self { return Self; } /// Decodes one projected transaction into zero or more Meteora DLMM events. pub fn decode_transaction( &self, transaction: &crate::ChainTransactionDto, instructions: &[crate::ChainInstructionDto], ) -> Result, crate::Error> { let transaction_id = match transaction.id { 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 = match instruction.program_id.as_deref() { Some(program_id) => program_id, None => continue, }; if program_id != crate::METEORA_DLMM_PROGRAM_ID { continue; } let instruction_id = match instruction.id { 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), }; let instruction_name = classify_instruction_name( parsed_json.as_ref(), instruction.parsed_type.as_deref(), instruction_data.as_deref(), &log_messages, ); let instruction_kind = instruction_name.kind(); if instruction_kind == MeteoraDlmmInstructionKind::Unknown || instruction_kind == MeteoraDlmmInstructionKind::Ignore { continue; } let pool_account = resolve_dlmm_pool_account(instruction_name, parsed_json.as_ref(), &accounts); let token_a_mint = resolve_dlmm_token_x_mint(instruction_name, parsed_json.as_ref(), &accounts); let token_b_mint = resolve_dlmm_token_y_mint(instruction_name, parsed_json.as_ref(), &accounts); if pool_account.is_none() || token_a_mint.is_none() || token_b_mint.is_none() { continue; } let config_account = resolve_dlmm_config_account(instruction_name, parsed_json.as_ref(), &accounts); if instruction_kind == MeteoraDlmmInstructionKind::CreatePool { let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", "eventKind": "create_pool", "decodedInstructionName": instruction_name.as_str(), "dataDiscriminatorHex": instruction_data .as_ref() .and_then(|data| return first_8_bytes_hex(data.as_slice())), "classifiedInstructionKind": "create_pool", "signature": transaction.signature, "instructionId": instruction_id, "parentInstructionId": instruction.parent_instruction_id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "stackHeight": instruction.stack_height, "accounts": accounts, "parsed": parsed_json, "logMessages": log_messages, "poolAccount": pool_account, "tokenAMint": token_a_mint, "tokenBMint": token_b_mint, "configAccount": config_account }); decoded_events.push(crate::MeteoraDlmmDecodedEvent::CreatePool( crate::MeteoraDlmmCreatePoolDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.to_string(), pool_account, token_a_mint, token_b_mint, config_account, payload_json, }, )); continue; } if instruction_kind == MeteoraDlmmInstructionKind::Swap { let reserve_x_account = resolve_dlmm_reserve_x_account( instruction_name, parsed_json.as_ref(), &accounts, ); let reserve_y_account = resolve_dlmm_reserve_y_account( instruction_name, parsed_json.as_ref(), &accounts, ); let user_token_in_account = resolve_dlmm_user_token_in_account( instruction_name, parsed_json.as_ref(), &accounts, ); let user_token_out_account = resolve_dlmm_user_token_out_account( instruction_name, parsed_json.as_ref(), &accounts, ); let trade_side = infer_trade_side(parsed_json.as_ref()); let payload_json = serde_json::json!({ "decoder": "meteora_dlmm", "eventKind": "swap", "decodedInstructionName": instruction_name.as_str(), "dataDiscriminatorHex": instruction_data .as_ref() .and_then(|data| return first_8_bytes_hex(data.as_slice())), "classifiedInstructionKind": "swap", "signature": transaction.signature, "instructionId": instruction_id, "parentInstructionId": instruction.parent_instruction_id, "instructionIndex": instruction.instruction_index, "innerInstructionIndex": instruction.inner_instruction_index, "stackHeight": instruction.stack_height, "accounts": accounts, "parsed": parsed_json, "logMessages": log_messages, "poolAccount": pool_account, "tokenAMint": token_a_mint, "tokenBMint": token_b_mint, "reserveXAccount": reserve_x_account, "reserveYAccount": reserve_y_account, "userTokenInAccount": user_token_in_account, "userTokenOutAccount": user_token_out_account, "tradeSide": format!("{:?}", trade_side) }); decoded_events.push(crate::MeteoraDlmmDecodedEvent::Swap( crate::MeteoraDlmmSwapDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.to_string(), trade_side, pool_account, token_a_mint, token_b_mint, reserve_x_account, reserve_y_account, user_token_in_account, user_token_out_account, payload_json, }, )); } } return Ok(decoded_events); } } fn classify_instruction_name( parsed_json: std::option::Option<&serde_json::Value>, parsed_type: std::option::Option<&str>, instruction_data: std::option::Option<&[u8]>, log_messages: &[std::string::String], ) -> MeteoraDlmmInstructionName { let from_data = classify_instruction_name_from_data(instruction_data); if from_data != MeteoraDlmmInstructionName::Unknown { return from_data; } if instruction_data.is_some() { return MeteoraDlmmInstructionName::Unknown; } if contains_create_pool_hint(parsed_type) { return MeteoraDlmmInstructionName::InitializeLbPair; } if contains_swap_hint(parsed_type) { return MeteoraDlmmInstructionName::Swap; } if parsed_type.is_some() { return MeteoraDlmmInstructionName::Unknown; } if let Some(parsed_json) = parsed_json { if contains_create_pool_hint_in_value(parsed_json) { return MeteoraDlmmInstructionName::InitializeLbPair; } if contains_swap_hint_in_value(parsed_json) { return MeteoraDlmmInstructionName::Swap; } return MeteoraDlmmInstructionName::Unknown; } for log_message in log_messages { if contains_create_pool_hint(Some(log_message.as_str())) { return MeteoraDlmmInstructionName::InitializeLbPair; } if contains_swap_hint(Some(log_message.as_str())) { return MeteoraDlmmInstructionName::Swap; } } return MeteoraDlmmInstructionName::Unknown; } fn classify_instruction_name_from_data( instruction_data: std::option::Option<&[u8]>, ) -> MeteoraDlmmInstructionName { let instruction_data = match instruction_data { Some(instruction_data) => instruction_data, None => return MeteoraDlmmInstructionName::Unknown, }; if instruction_data.len() < 8 { return MeteoraDlmmInstructionName::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 == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR { return MeteoraDlmmInstructionName::InitializeLbPair; } if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_LB_PAIR2 { return MeteoraDlmmInstructionName::InitializeLbPair2; } if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR { return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair; } if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_CUSTOMIZABLE_PERMISSIONLESS_LB_PAIR2 { return MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2; } if discriminator == DLMM_DISCRIMINATOR_SWAP { return MeteoraDlmmInstructionName::Swap; } if discriminator == DLMM_DISCRIMINATOR_SWAP2 { return MeteoraDlmmInstructionName::Swap2; } if discriminator == DLMM_DISCRIMINATOR_SWAP_EXACT_OUT { return MeteoraDlmmInstructionName::SwapExactOut; } if discriminator == DLMM_DISCRIMINATOR_SWAP_EXACT_OUT2 { return MeteoraDlmmInstructionName::SwapExactOut2; } if discriminator == DLMM_DISCRIMINATOR_SWAP_WITH_PRICE_IMPACT { return MeteoraDlmmInstructionName::SwapWithPriceImpact; } if discriminator == DLMM_DISCRIMINATOR_CLAIM_FEE2 { return MeteoraDlmmInstructionName::ClaimFee2; } if discriminator == DLMM_DISCRIMINATOR_INITIALIZE_POSITION { return MeteoraDlmmInstructionName::InitializePosition; } return MeteoraDlmmInstructionName::Unknown; } fn resolve_dlmm_pool_account( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &[ "lbPair", "lb_pair", "lbPairAccount", "pair", "pairAccount", "pool", "poolAccount", ], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 | MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 0); }, MeteoraDlmmInstructionName::ClaimFee2 | MeteoraDlmmInstructionName::InitializePosition | MeteoraDlmmInstructionName::Unknown => return None, } } fn resolve_dlmm_token_x_mint( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &[ "tokenXMint", "token_x_mint", "tokenMintX", "token_mint_x", "mintX", "mint_x", "tokenAMint", "token_a_mint", "baseMint", ], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 => { return extract_account(accounts, 2); }, MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 6); }, MeteoraDlmmInstructionName::ClaimFee2 | MeteoraDlmmInstructionName::InitializePosition | MeteoraDlmmInstructionName::Unknown => return None, } } fn resolve_dlmm_token_y_mint( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &[ "tokenYMint", "token_y_mint", "tokenMintY", "token_mint_y", "mintY", "mint_y", "tokenBMint", "token_b_mint", "quoteMint", ], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 => { return extract_account(accounts, 3); }, MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 7); }, MeteoraDlmmInstructionName::ClaimFee2 | MeteoraDlmmInstructionName::InitializePosition | MeteoraDlmmInstructionName::Unknown => return None, } } fn resolve_dlmm_reserve_x_account( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &["reserveX", "reserve_x", "reserveXAccount", "reserve_x_account"], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 2); }, _ => return None, } } fn resolve_dlmm_reserve_y_account( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &["reserveY", "reserve_y", "reserveYAccount", "reserve_y_account"], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 3); }, _ => return None, } } fn resolve_dlmm_user_token_in_account( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &["userTokenIn", "user_token_in", "userTokenInAccount", "user_token_in_account"], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapExactOut2 | MeteoraDlmmInstructionName::SwapWithPriceImpact => { return extract_account(accounts, 4); }, _ => return None, } } fn resolve_dlmm_user_token_out_account( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &[ "userTokenOut", "user_token_out", "userTokenOutAccount", "user_token_out_account", ], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::Swap | MeteoraDlmmInstructionName::Swap2 | MeteoraDlmmInstructionName::SwapExactOut | MeteoraDlmmInstructionName::SwapWithPriceImpact | MeteoraDlmmInstructionName::SwapExactOut2 => { return extract_account(accounts, 5); }, _ => return None, } } fn resolve_dlmm_config_account( instruction_name: MeteoraDlmmInstructionName, parsed_json: std::option::Option<&serde_json::Value>, accounts: &[std::string::String], ) -> std::option::Option { let parsed_value = extract_string_by_candidate_keys( parsed_json, &[ "presetParameter", "preset_parameter", "presetParameter2", "config", "poolConfig", ], ); if parsed_value.is_some() { return parsed_value; } match instruction_name { MeteoraDlmmInstructionName::InitializeLbPair | MeteoraDlmmInstructionName::InitializeLbPair2 | MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair => { return extract_account(accounts, 7); }, MeteoraDlmmInstructionName::InitializeCustomizablePermissionlessLbPair2 => { return None; }, _ => return None, } } fn contains_create_pool_hint(value: std::option::Option<&str>) -> bool { let value = match value { Some(value) => value.to_ascii_lowercase(), None => return false, }; if value.contains("initializelbpair") { return true; } if value.contains("initialize_lb_pair") { return true; } if value.contains("initializecustomizablepermissionlesslbpair") { return true; } if value.contains("initialize_customizable_permissionless_lb_pair") { return true; } if value.contains("initializepermissionlbpair") { return true; } if value.contains("initialize_permission_lb_pair") { return true; } if value.contains("create_lb_pair") { return true; } return false; } fn contains_swap_hint(value: std::option::Option<&str>) -> bool { let value = match value { Some(value) => value.to_ascii_lowercase(), None => return false, }; if value.contains("swap") { return true; } return false; } fn contains_create_pool_hint_in_value(value: &serde_json::Value) -> bool { return contains_string_hint_in_value(value, contains_create_pool_hint); } fn contains_swap_hint_in_value(value: &serde_json::Value) -> bool { return contains_string_hint_in_value(value, contains_swap_hint); } fn contains_string_hint_in_value( value: &serde_json::Value, predicate: fn(std::option::Option<&str>) -> bool, ) -> bool { match value { serde_json::Value::String(text) => return predicate(Some(text.as_str())), serde_json::Value::Array(values) => { for nested in values { if contains_string_hint_in_value(nested, predicate) { return true; } } }, serde_json::Value::Object(object) => { for nested in object.values() { if contains_string_hint_in_value(nested, predicate) { return true; } } }, _ => {}, } return false; } fn infer_trade_side(parsed_json: std::option::Option<&serde_json::Value>) -> crate::SwapTradeSide { let side = extract_string_by_candidate_keys( parsed_json, &["tradeSide", "trade_side", "side", "swapSide", "swap_side"], ); match side.as_deref() { Some("BuyBase") => return crate::SwapTradeSide::BuyBase, Some("buy") => return crate::SwapTradeSide::BuyBase, Some("BUY") => return crate::SwapTradeSide::BuyBase, Some("SellBase") => return crate::SwapTradeSide::SellBase, Some("sell") => return crate::SwapTradeSide::SellBase, Some("SELL") => return crate::SwapTradeSide::SellBase, _ => return crate::SwapTradeSide::Unknown, } } fn extract_log_messages( transaction_json: &serde_json::Value, ) -> std::vec::Vec { let mut messages = std::vec::Vec::new(); let meta = match transaction_json.get("meta") { Some(meta) => meta, None => return messages, }; let logs = match meta.get("logMessages").and_then(|value| return value.as_array()) { Some(logs) => logs, None => return messages, }; for log in logs { if let Some(text) = log.as_str() { messages.push(text.to_string()); } } return messages; } fn parse_accounts_json( accounts_json: &str, ) -> Result, crate::Error> { let parsed_result = serde_json::from_str::(accounts_json); let parsed = match parsed_result { Ok(parsed) => parsed, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse Meteora DLMM accounts_json: {}", error ))); }, }; let array = match parsed.as_array() { Some(array) => array, None => return Ok(std::vec::Vec::new()), }; let mut accounts = std::vec::Vec::new(); for item in array { if let Some(text) = item.as_str() { accounts.push(text.to_string()); continue; } if let Some(pubkey) = item.get("pubkey").and_then(|value| return value.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 parsed_result = serde_json::from_str::(parsed_json); match parsed_result { Ok(parsed) => return Ok(Some(parsed)), Err(error) => { return Err(crate::Error::Json(format!( "cannot parse Meteora DLMM parsed_json: {}", error ))); }, } } fn extract_account( accounts: &[std::string::String], index: usize, ) -> std::option::Option { match accounts.get(index) { Some(account) => return Some(account.clone()), None => return None, } } 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, }; if let Some(object) = value.as_object() { for candidate_key in candidate_keys { if let Some(found) = object.get(*candidate_key) { if let Some(text) = found.as_str() { return Some(text.to_string()); } if let Some(number) = found.as_u64() { return Some(number.to_string()); } if let Some(number) = found.as_i64() { return Some(number.to_string()); } } } for nested in object.values() { let nested_result = extract_string_by_candidate_keys(Some(nested), candidate_keys); if nested_result.is_some() { return nested_result; } } } if let Some(array) = value.as_array() { for nested in array { let nested_result = extract_string_by_candidate_keys(Some(nested), candidate_keys); if nested_result.is_some() { return nested_result; } } } return None; } 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 parsed_result = serde_json::from_str::(data_json.as_str()); let parsed = match parsed_result { Ok(parsed) => parsed, Err(error) => { return Err(crate::Error::Json(format!( "cannot parse Meteora DLMM data_json: {}", error ))); }, }; if let serde_json::Value::String(base58_text) = parsed { let decoded_result = decode_base58(base58_text.as_str()); let decoded = match decoded_result { Ok(decoded) => decoded, Err(error) => return Err(error), }; return Ok(Some(decoded)); } return Ok(None); } fn decode_base58(input: &str) -> Result, crate::Error> { let decoded_result = bs58::decode(input).into_vec(); match decoded_result { Ok(decoded) => return Ok(decoded), Err(error) => { return Err(crate::Error::Json(format!( "cannot decode Meteora DLMM instruction data from base58: {}", error ))); }, } } 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], )); } #[cfg(test)] mod tests { fn make_create_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::new( "sig-meteora-dlmm-create-1".to_string(), Some(888101), Some(1779400001), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 888101, "meta": { "logMessages": [ "Program log: Instruction: InitializeLbPair" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(401); return dto; } fn make_create_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 401, None, 0, None, Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some("meteora-dlmm".to_string()), Some(1), serde_json::json!([ "DlmmPair111", "DlmmTokenX111", crate::WSOL_MINT_ID, "DlmmPreset111" ]) .to_string(), None, Some("initialize_lb_pair".to_string()), Some( serde_json::json!({ "info": { "lbPair": "DlmmPair111", "tokenXMint": "DlmmTokenX111", "tokenYMint": crate::WSOL_MINT_ID, "presetParameter": "DlmmPreset111" } }) .to_string(), ), ); dto.id = Some(402); return dto; } fn make_swap_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::new( "sig-meteora-dlmm-swap-1".to_string(), Some(888102), Some(1779400002), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 888102, "meta": { "logMessages": [ "Program log: Instruction: Swap" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(403); return dto; } fn make_swap_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 403, None, 0, None, Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some("meteora-dlmm".to_string()), Some(1), serde_json::json!(["DlmmPairSwap111", "DlmmSwapTokenX111", crate::WSOL_MINT_ID]) .to_string(), None, Some("swap".to_string()), Some( serde_json::json!({ "info": { "lbPair": "DlmmPairSwap111", "tokenXMint": "DlmmSwapTokenX111", "tokenYMint": crate::WSOL_MINT_ID, "tradeSide": "buy" } }) .to_string(), ), ); dto.id = Some(404); return dto; } #[test] fn meteora_dlmm_create_pool_is_detected() { let decoder = crate::MeteoraDlmmDecoder::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::MeteoraDlmmDecodedEvent::CreatePool(event) => { assert_eq!(event.transaction_id, 401); assert_eq!(event.instruction_id, 402); assert_eq!(event.pool_account, Some("DlmmPair111".to_string())); assert_eq!(event.token_a_mint, Some("DlmmTokenX111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); assert_eq!(event.config_account, Some("DlmmPreset111".to_string())); }, crate::MeteoraDlmmDecodedEvent::Swap(_) => { panic!("unexpected swap event"); }, } } #[test] fn meteora_dlmm_swap_is_detected() { let decoder = crate::MeteoraDlmmDecoder::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::MeteoraDlmmDecodedEvent::Swap(event) => { assert_eq!(event.transaction_id, 403); assert_eq!(event.instruction_id, 404); assert_eq!(event.pool_account, Some("DlmmPairSwap111".to_string())); assert_eq!(event.token_a_mint, Some("DlmmSwapTokenX111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); assert_eq!(event.trade_side, crate::SwapTradeSide::BuyBase); }, crate::MeteoraDlmmDecodedEvent::CreatePool(_) => { panic!("unexpected create event"); }, } } #[test] fn meteora_dlmm_ignores_unclear_instruction() { let decoder = crate::MeteoraDlmmDecoder::new(); let transaction = make_swap_transaction(); let mut instruction = make_swap_instruction(); instruction.parsed_type = Some("initialize_bin_array".to_string()); instruction.parsed_json = None; 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(), 0); } #[test] fn meteora_dlmm_swap2_discriminator_is_detected() { let instruction_data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01, 0x02, 0x03]; let name = super::classify_instruction_name_from_data(Some(&instruction_data)); assert_eq!(name, super::MeteoraDlmmInstructionName::Swap2); assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::Swap); } #[test] fn meteora_dlmm_swap_accounts_are_mapped_from_carbon_layout() { let accounts = vec![ "LbPair111".to_string(), "Bitmap111".to_string(), "ReserveX111".to_string(), "ReserveY111".to_string(), "UserTokenIn111".to_string(), "UserTokenOut111".to_string(), "TokenXMint111".to_string(), "TokenYMint111".to_string(), ]; let pool = super::resolve_dlmm_pool_account( super::MeteoraDlmmInstructionName::Swap2, None, &accounts, ); let token_x = super::resolve_dlmm_token_x_mint( super::MeteoraDlmmInstructionName::Swap2, None, &accounts, ); let token_y = super::resolve_dlmm_token_y_mint( super::MeteoraDlmmInstructionName::Swap2, None, &accounts, ); assert_eq!(pool, Some("LbPair111".to_string())); assert_eq!(token_x, Some("TokenXMint111".to_string())); assert_eq!(token_y, Some("TokenYMint111".to_string())); } #[test] fn meteora_dlmm_real_swap2_base58_discriminator_is_decoded() { let data_json = "\"fx9RHbGFfZ8bH9v4Jv5SQRfuGUq9kGrWfmiZ99\"".to_string(); let decoded_result = super::decode_instruction_data_json(Some(&data_json)); let decoded = match decoded_result { Ok(Some(decoded)) => decoded, Ok(None) => panic!("expected decoded data"), Err(error) => panic!("decode must succeed: {}", error), }; let name = super::classify_instruction_name_from_data(Some(decoded.as_slice())); assert_eq!(name, super::MeteoraDlmmInstructionName::Swap2); } #[test] fn meteora_dlmm_inner_swap2_instruction_is_not_skipped() { let decoder = crate::MeteoraDlmmDecoder::new(); let mut transaction = crate::ChainTransactionDto::new( "sig-meteora-dlmm-inner-swap2".to_string(), Some(888103), Some(1779400003), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 888103, "meta": { "logMessages": [] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); transaction.id = Some(405); let mut instruction = crate::ChainInstructionDto::new( 405, Some(404), 3, Some(14), Some(crate::METEORA_DLMM_PROGRAM_ID.to_string()), Some("meteora-dlmm".to_string()), Some(2), serde_json::json!([ "LbPair111", "Bitmap111", "ReserveX111", "ReserveY111", "UserTokenIn111", "UserTokenOut111", "TokenXMint111", "TokenYMint111", "Oracle111", "HostFee111", "User111", crate::SPL_TOKEN_PROGRAM_ID, crate::SPL_TOKEN_PROGRAM_ID, "EventAuthority111".to_string(), crate::METEORA_DLMM_PROGRAM_ID, "BinArray111", "BinArray222" ]) .to_string(), Some("\"fx9RHbGFfZ8bH9v4Jv5SQRfuGUq9kGrWfmiZ99\"".to_string()), None, None, ); instruction.id = Some(406); 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::MeteoraDlmmDecodedEvent::Swap(event) => { assert_eq!(event.transaction_id, 405); assert_eq!(event.instruction_id, 406); assert_eq!(event.pool_account, Some("LbPair111".to_string())); assert_eq!(event.token_a_mint, Some("TokenXMint111".to_string())); assert_eq!(event.token_b_mint, Some("TokenYMint111".to_string())); assert_eq!(event.reserve_x_account, Some("ReserveX111".to_string())); assert_eq!(event.reserve_y_account, Some("ReserveY111".to_string())); assert_eq!(event.user_token_in_account, Some("UserTokenIn111".to_string())); assert_eq!(event.user_token_out_account, Some("UserTokenOut111".to_string())); }, crate::MeteoraDlmmDecodedEvent::CreatePool(_) => { panic!("unexpected create event"); }, } } #[test] fn meteora_dlmm_initialize_position_discriminator_is_ignored() { let instruction_data = [0xdb, 0xc0, 0xea, 0x47, 0xbe, 0xbf, 0x66, 0x50, 0x01, 0x02, 0x03]; let log_messages = vec!["Program log: Instruction: Swap".to_string()]; let name = super::classify_instruction_name(None, None, Some(&instruction_data), &log_messages); assert_eq!(name, super::MeteoraDlmmInstructionName::InitializePosition); assert_eq!(name.kind(), super::MeteoraDlmmInstructionKind::Ignore); } #[test] fn meteora_dlmm_unknown_data_discriminator_does_not_fallback_to_global_swap_logs() { let instruction_data = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11, 0x01, 0x02, 0x03]; let log_messages = vec!["Program log: Instruction: Swap".to_string()]; let name = super::classify_instruction_name(None, None, Some(&instruction_data), &log_messages); assert_eq!(name, super::MeteoraDlmmInstructionName::Unknown); } }