// file: kb_lib/src/dex/meteora_dbc.rs //! Meteora Dynamic Bonding Curve (DBC) transaction decoder. /// Meteora DBC program id. pub const KB_METEORA_DBC_PROGRAM_ID: &str = "dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN"; /// Decoded Meteora DBC create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct KbMeteoraDbcCreatePoolDecoded { /// 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 base mint. pub token_a_mint: std::option::Option, /// Optional quote mint. pub token_b_mint: std::option::Option, /// Optional config account. pub config_account: std::option::Option, /// Optional creator. pub creator: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Meteora DBC swap event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct KbMeteoraDbcSwapDecoded { /// 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::KbSwapTradeSide, /// Optional pool account. pub pool_account: std::option::Option, /// Optional base mint. pub token_a_mint: std::option::Option, /// Optional quote mint. pub token_b_mint: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded Meteora DBC event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum KbMeteoraDbcDecodedEvent { /// Create pool / launch pool. CreatePool(KbMeteoraDbcCreatePoolDecoded), /// Swap / swap2. Swap(KbMeteoraDbcSwapDecoded), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum KbMeteoraDbcInstructionKind { CreatePool, Swap, Unknown, } /// Meteora DBC decoder. #[derive(Debug, Clone, Default)] pub struct KbMeteoraDbcDecoder; impl KbMeteoraDbcDecoder { /// Creates a new decoder. pub fn new() -> Self { Self } /// Decodes one projected transaction into zero or more Meteora DBC events. pub fn decode_transaction( &self, transaction: &crate::KbChainTransactionDto, instructions: &[crate::KbChainInstructionDto], ) -> Result, crate::KbError> { let transaction_id_option = transaction.id; let transaction_id = match transaction_id_option { Some(transaction_id) => transaction_id, None => { return Err(crate::KbError::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::KbError::Json(format!( "cannot parse transaction_json for signature '{}': {}", transaction.signature, error ))); } }; let log_messages = kb_extract_log_messages(&transaction_json); 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::KB_METEORA_DBC_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 = kb_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 = kb_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_kind = kb_classify_instruction_kind(parsed_json.as_ref(), &log_messages); let pool_account = kb_extract_string_by_candidate_keys( parsed_json.as_ref(), &["pool", "poolAccount", "poolState", "virtualPool", "poolKey"], ) .or_else(|| kb_extract_account(&accounts, 0)); let token_a_mint = kb_extract_string_by_candidate_keys( parsed_json.as_ref(), &["baseMint", "tokenAMint", "mintA", "token0Mint", "mint0"], ) .or_else(|| kb_extract_account(&accounts, 1)); let token_b_mint = kb_extract_string_by_candidate_keys( parsed_json.as_ref(), &["quoteMint", "tokenBMint", "mintB", "token1Mint", "mint1"], ) .or_else(|| kb_extract_account(&accounts, 2)); let config_account = kb_extract_string_by_candidate_keys( parsed_json.as_ref(), &["poolConfig", "config", "dbcConfig", "curveConfig"], ) .or_else(|| kb_extract_account(&accounts, 3)); let creator = kb_extract_string_by_candidate_keys( parsed_json.as_ref(), &["creator", "poolCreator", "owner", "user"], ) .or_else(|| kb_extract_account(&accounts, 4)); if instruction_kind == KbMeteoraDbcInstructionKind::CreatePool { let payload_json = serde_json::json!({ "decoder": "meteora_dbc", "eventKind": "create_pool", "classifiedInstructionKind": "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::KbMeteoraDbcDecodedEvent::CreatePool( crate::KbMeteoraDbcCreatePoolDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.clone(), pool_account, token_a_mint, token_b_mint, config_account, creator, payload_json, }, )); continue; } if instruction_kind == KbMeteoraDbcInstructionKind::Swap { let trade_side = kb_infer_trade_side(&log_messages); let payload_json = serde_json::json!({ "decoder": "meteora_dbc", "eventKind": "swap", "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::KbMeteoraDbcDecodedEvent::Swap( crate::KbMeteoraDbcSwapDecoded { 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, }, )); } } Ok(decoded_events) } } fn kb_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()); } } messages } fn kb_log_messages_contain_any_keyword( log_messages: &[std::string::String], keywords: &[&str], ) -> bool { for keyword in keywords { if kb_log_messages_contain_keyword(log_messages, keyword) { return true; } } false } fn kb_log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool { let keyword_normalized = kb_normalize_log_text(keyword); for log_message in log_messages { let log_normalized = kb_normalize_log_text(log_message.as_str()); if log_normalized.contains(keyword_normalized.as_str()) { return true; } } false } fn kb_normalize_log_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()); } } normalized } fn kb_parse_accounts_json( accounts_json: &str, ) -> Result, crate::KbError> { let values_result = serde_json::from_str::>(accounts_json); let values = match values_result { Ok(values) => values, Err(error) => { return Err(crate::KbError::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()); } } Ok(accounts) } fn kb_parse_optional_parsed_json( parsed_json: std::option::Option<&std::string::String>, ) -> Result, crate::KbError> { 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) => Ok(Some(value)), Err(error) => Err(crate::KbError::Json(format!( "cannot parse instruction parsed_json '{}': {}", parsed_json, error ))), } } fn kb_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, }; kb_extract_string_by_candidate_keys_inner(value, candidate_keys) } fn kb_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 = kb_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 = kb_extract_string_by_candidate_keys_inner(nested_value, candidate_keys); if nested_result.is_some() { return nested_result; } } } None } fn kb_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, }; kb_value_contains_any_key_inner(value, candidate_keys) } fn kb_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 kb_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 kb_value_contains_any_key_inner(nested_value, candidate_keys) { return true; } } } false } fn kb_extract_account( accounts: &[std::string::String], index: usize, ) -> std::option::Option { if index >= accounts.len() { return None; } Some(accounts[index].clone()) } fn kb_infer_trade_side(log_messages: &[std::string::String]) -> crate::KbSwapTradeSide { if kb_log_messages_contain_keyword(log_messages, "buy") { return crate::KbSwapTradeSide::BuyBase; } if kb_log_messages_contain_keyword(log_messages, "sell") { return crate::KbSwapTradeSide::SellBase; } crate::KbSwapTradeSide::Unknown } fn kb_classify_instruction_kind( parsed_json: std::option::Option<&serde_json::Value>, log_messages: &[std::string::String], ) -> KbMeteoraDbcInstructionKind { let parsed_instruction_name = kb_extract_string_by_candidate_keys( parsed_json, &["instruction", "instructionName", "type", "name"], ); if let Some(parsed_instruction_name) = parsed_instruction_name { let normalized = kb_normalize_log_text(parsed_instruction_name.as_str()); if normalized.contains("createpool") || normalized.contains("initializepool") || normalized.contains("launchpool") { return KbMeteoraDbcInstructionKind::CreatePool; } if normalized == "swap" || normalized == "swap2" { return KbMeteoraDbcInstructionKind::Swap; } } let has_create_config = kb_value_contains_any_key( parsed_json, &[ "poolConfig", "migrationQuoteThreshold", "curveConfig", "dbcConfig", ], ); if has_create_config { return KbMeteoraDbcInstructionKind::CreatePool; } if kb_log_messages_contain_any_keyword( log_messages, &[ "create_pool", "createpool", "initialize_pool", "initializepool", "launch_pool", ], ) { return KbMeteoraDbcInstructionKind::CreatePool; } if kb_log_messages_contain_any_keyword(log_messages, &["swap2", "swap"]) { return KbMeteoraDbcInstructionKind::Swap; } KbMeteoraDbcInstructionKind::Unknown } #[cfg(test)] mod tests { fn make_create_transaction() -> crate::KbChainTransactionDto { let mut dto = crate::KbChainTransactionDto::new( "sig-meteora-dbc-create-1".to_string(), Some(888001), Some(1779300001), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 888001, "meta": { "logMessages": [ "Program log: Instruction: CreatePool" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(301); dto } fn make_create_instruction() -> crate::KbChainInstructionDto { let mut dto = crate::KbChainInstructionDto::new( 301, None, 0, None, Some(crate::KB_METEORA_DBC_PROGRAM_ID.to_string()), Some("meteora-dbc".to_string()), Some(1), serde_json::json!([ "DbcPool111", "DbcTokenA111", "So11111111111111111111111111111111111111112", "DbcConfig111", "DbcCreator111" ]) .to_string(), None, None, Some( serde_json::json!({ "info": { "pool": "DbcPool111", "baseMint": "DbcTokenA111", "quoteMint": "So11111111111111111111111111111111111111112", "poolConfig": "DbcConfig111", "creator": "DbcCreator111" } }) .to_string(), ), ); dto.id = Some(302); dto } fn make_swap_transaction() -> crate::KbChainTransactionDto { let mut dto = crate::KbChainTransactionDto::new( "sig-meteora-dbc-swap-1".to_string(), Some(888002), Some(1779300002), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 888002, "meta": { "logMessages": [ "Program log: Instruction: Swap2" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(303); dto } fn make_swap_instruction() -> crate::KbChainInstructionDto { let mut dto = crate::KbChainInstructionDto::new( 303, None, 0, None, Some(crate::KB_METEORA_DBC_PROGRAM_ID.to_string()), Some("meteora-dbc".to_string()), Some(1), serde_json::json!([ "DbcPoolSwap111", "DbcSwapTokenA111", "So11111111111111111111111111111111111111112" ]) .to_string(), None, None, Some( serde_json::json!({ "info": { "pool": "DbcPoolSwap111", "baseMint": "DbcSwapTokenA111", "quoteMint": "So11111111111111111111111111111111111111112" } }) .to_string(), ), ); dto.id = Some(304); dto } #[test] fn meteora_dbc_create_pool_is_detected() { let decoder = crate::KbMeteoraDbcDecoder::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::KbMeteoraDbcDecodedEvent::CreatePool(event) => { assert_eq!(event.transaction_id, 301); assert_eq!(event.instruction_id, 302); assert_eq!(event.pool_account, Some("DbcPool111".to_string())); assert_eq!(event.token_a_mint, Some("DbcTokenA111".to_string())); assert_eq!( event.token_b_mint, Some("So11111111111111111111111111111111111111112".to_string()) ); assert_eq!(event.config_account, Some("DbcConfig111".to_string())); } crate::KbMeteoraDbcDecodedEvent::Swap(_) => { panic!("unexpected swap event") } } } #[test] fn meteora_dbc_swap_is_detected() { let decoder = crate::KbMeteoraDbcDecoder::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::KbMeteoraDbcDecodedEvent::Swap(event) => { assert_eq!(event.transaction_id, 303); assert_eq!(event.instruction_id, 304); assert_eq!(event.pool_account, Some("DbcPoolSwap111".to_string())); assert_eq!(event.token_a_mint, Some("DbcSwapTokenA111".to_string())); assert_eq!( event.token_b_mint, Some("So11111111111111111111111111111111111111112".to_string()) ); } crate::KbMeteoraDbcDecodedEvent::CreatePool(_) => { panic!("unexpected create event") } } } #[test] fn meteora_dbc_quote_mint_alone_does_not_trigger_create_pool() { let decoder = crate::KbMeteoraDbcDecoder::new(); let transaction = make_swap_transaction(); let mut instruction = make_swap_instruction(); instruction.parsed_json = Some( serde_json::json!({ "info": { "quoteMint": "So11111111111111111111111111111111111111112" } }) .to_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::KbMeteoraDbcDecodedEvent::Swap(_) => {} crate::KbMeteoraDbcDecodedEvent::CreatePool(_) => { panic!("unexpected create event") } } } #[test] fn meteora_dbc_pool_config_triggers_create_pool() { let decoder = crate::KbMeteoraDbcDecoder::new(); let transaction = make_create_transaction(); let instruction = make_create_instruction(); 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::KbMeteoraDbcDecodedEvent::CreatePool(_) => {} crate::KbMeteoraDbcDecodedEvent::Swap(_) => { panic!("unexpected swap event") } } } }