// file: kb_lib/src/dex/meteora_dbc.rs //! Meteora Dynamic Bonding Curve (DBC) transaction decoder. const DBC_DISCRIMINATOR_CREATE_POOL: [u8; 8] = [0xe9, 0x92, 0xd1, 0x8e, 0xcf, 0x68, 0x40, 0xbc]; const DBC_DISCRIMINATOR_INITIALIZE_POOL: [u8; 8] = [0x5f, 0xb4, 0x0a, 0xac, 0x54, 0xae, 0xe8, 0x28]; const DBC_DISCRIMINATOR_LAUNCH_POOL: [u8; 8] = [0xa6, 0x77, 0xd1, 0xb6, 0xd6, 0x6d, 0x3a, 0xb5]; const DBC_DISCRIMINATOR_SWAP: [u8; 8] = [0xf8, 0xc6, 0x9e, 0x91, 0xe1, 0x75, 0x87, 0xc8]; const DBC_DISCRIMINATOR_SWAP2: [u8; 8] = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88]; /// Decoded Meteora DBC create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MeteoraDbcCreatePoolDecoded { /// 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 MeteoraDbcSwapDecoded { /// 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 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 MeteoraDbcDecodedEvent { /// Create pool / launch pool. CreatePool(MeteoraDbcCreatePoolDecoded), /// Swap / swap2. Swap(MeteoraDbcSwapDecoded), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MeteoraDbcInstructionKind { CreatePool, Swap, Unknown, } /// Meteora DBC decoder. #[derive(Debug, Clone, Default)] pub struct MeteoraDbcDecoder; impl MeteoraDbcDecoder { /// Creates a new decoder. pub fn new() -> Self { return Self; } /// Decodes one projected transaction into zero or more Meteora DBC 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_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 = 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", "poolAccount", "poolState", "virtualPool", "poolKey"], ) .or_else(|| return extract_account(&accounts, 0)); let token_a_mint = extract_string_by_candidate_keys( parsed_json.as_ref(), &["baseMint", "tokenAMint", "mintA", "token0Mint", "mint0"], ) .or_else(|| return extract_account(&accounts, 1)); let token_b_mint = extract_string_by_candidate_keys( parsed_json.as_ref(), &["quoteMint", "tokenBMint", "mintB", "token1Mint", "mint1"], ) .or_else(|| return extract_account(&accounts, 2)); let config_account = extract_string_by_candidate_keys( parsed_json.as_ref(), &["poolConfig", "config", "dbcConfig", "curveConfig"], ) .or_else(|| return extract_account(&accounts, 3)); let creator = extract_string_by_candidate_keys( parsed_json.as_ref(), &["creator", "poolCreator", "owner", "user"], ) .or_else(|| return extract_account(&accounts, 4)); if instruction_kind == MeteoraDbcInstructionKind::CreatePool { let payload_json = serde_json::json!({ "decoder": "meteora_dbc", "eventKind": "create_pool", "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, "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::MeteoraDbcDecodedEvent::CreatePool( crate::MeteoraDbcCreatePoolDecoded { 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 == MeteoraDbcInstructionKind::Swap { let trade_side = infer_trade_side(&log_messages); let has_trade_amount_payload = parsed_json_has_trade_amount_or_price_payload(parsed_json.as_ref()); let event_actionability = if has_trade_amount_payload { "trade_candidate" } else { "non_actionable_trade" }; let materialization_skip_reason = if has_trade_amount_payload { serde_json::Value::Null } else { serde_json::Value::String("swap_without_amount_payload".to_string()) }; let payload_json = serde_json::json!({ "decoder": "meteora_dbc", "eventKind": "swap", "dataDiscriminatorHex": instruction_data .as_ref() .and_then(|data| return first_8_bytes_hex(data.as_slice())), "classifiedInstructionKind": "swap", "eventCategory": "trade", "eventLifecycleKind": "trade_swap", "eventActionability": event_actionability, "tradeCandidate": has_trade_amount_payload, "candleCandidate": has_trade_amount_payload, "nonTradeUseful": false, "materializationSkipReason": materialization_skip_reason, "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::MeteoraDbcDecodedEvent::Swap( crate::MeteoraDbcSwapDecoded { 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 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_any_keyword( log_messages: &[std::string::String], keywords: &[&str], ) -> bool { for keyword in keywords { if log_messages_contain_keyword(log_messages, keyword) { return true; } } return false; } fn log_messages_contain_keyword(log_messages: &[std::string::String], keyword: &str) -> bool { let keyword_normalized = normalize_log_text(keyword); for log_message in log_messages { let log_normalized = normalize_log_text(log_message.as_str()); if log_normalized.contains(keyword_normalized.as_str()) { return true; } } return false; } fn 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()); } } 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 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 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 DBC data_json: {}", error ))); }, }; if let serde_json::Value::String(base58_text) = parsed { 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 DBC 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_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; } fn classify_instruction_kind( parsed_json: std::option::Option<&serde_json::Value>, instruction_data: std::option::Option<&[u8]>, log_messages: &[std::string::String], ) -> MeteoraDbcInstructionKind { let data_kind = classify_instruction_kind_from_data(instruction_data); if data_kind != MeteoraDbcInstructionKind::Unknown { return data_kind; } if instruction_data_has_full_discriminator(instruction_data) { return MeteoraDbcInstructionKind::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_log_text(parsed_instruction_name.as_str()); if normalized.contains("createpool") || normalized.contains("initializepool") || normalized.contains("launchpool") { return MeteoraDbcInstructionKind::CreatePool; } if normalized == "swap" || normalized == "swap2" { return MeteoraDbcInstructionKind::Swap; } } let has_create_config = value_contains_any_key( parsed_json, &["poolConfig", "migrationQuoteThreshold", "curveConfig", "dbcConfig"], ); if has_create_config { return MeteoraDbcInstructionKind::CreatePool; } if log_messages_contain_any_keyword( log_messages, &["create_pool", "createpool", "initialize_pool", "initializepool", "launch_pool"], ) { return MeteoraDbcInstructionKind::CreatePool; } if log_messages_contain_any_keyword(log_messages, &["swap2", "swap"]) { return MeteoraDbcInstructionKind::Swap; } return MeteoraDbcInstructionKind::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]>, ) -> MeteoraDbcInstructionKind { let instruction_data = match instruction_data { Some(instruction_data) => instruction_data, None => return MeteoraDbcInstructionKind::Unknown, }; if instruction_data.len() < 8 { return MeteoraDbcInstructionKind::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 == DBC_DISCRIMINATOR_CREATE_POOL || discriminator == DBC_DISCRIMINATOR_INITIALIZE_POOL || discriminator == DBC_DISCRIMINATOR_LAUNCH_POOL { return MeteoraDbcInstructionKind::CreatePool; } if discriminator == DBC_DISCRIMINATOR_SWAP || discriminator == DBC_DISCRIMINATOR_SWAP2 { return MeteoraDbcInstructionKind::Swap; } return MeteoraDbcInstructionKind::Unknown; } fn parsed_json_has_trade_amount_or_price_payload( parsed_json: std::option::Option<&serde_json::Value>, ) -> bool { let parsed_json = match parsed_json { Some(parsed_json) => parsed_json, None => return false, }; return json_value_contains_any_trade_amount_or_price_key(parsed_json); } fn json_value_contains_any_trade_amount_or_price_key(value: &serde_json::Value) -> bool { match value { serde_json::Value::Object(map) => { for key in map.keys() { let normalized = normalize_log_text(key.as_str()); if is_trade_amount_or_price_key(normalized.as_str()) { return true; } } for child in map.values() { if json_value_contains_any_trade_amount_or_price_key(child) { return true; } } return false; }, serde_json::Value::Array(values) => { for child in values { if json_value_contains_any_trade_amount_or_price_key(child) { return true; } } return false; }, _ => return false, } } fn is_trade_amount_or_price_key(normalized_key: &str) -> bool { return normalized_key == "baseamountraw" || normalized_key == "quoteamountraw" || normalized_key == "baseamount" || normalized_key == "quoteamount" || normalized_key == "amountin" || normalized_key == "amountout" || normalized_key == "tokenain" || normalized_key == "tokenaout" || normalized_key == "tokenbin" || normalized_key == "tokenbout" || normalized_key == "inputamount" || normalized_key == "outputamount" || normalized_key == "swapamount" || normalized_key == "price" || normalized_key == "pricequoteperbase"; } #[cfg(test)] mod tests { fn make_create_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::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); return dto; } fn make_create_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 301, None, 0, None, Some(crate::METEORA_DBC_PROGRAM_ID.to_string()), Some("meteora-dbc".to_string()), Some(1), serde_json::json!([ "DbcPool111", "DbcTokenA111", crate::WSOL_MINT_ID, "DbcConfig111", "DbcCreator111" ]) .to_string(), None, None, Some( serde_json::json!({ "info": { "pool": "DbcPool111", "baseMint": "DbcTokenA111", "quoteMint": crate::WSOL_MINT_ID, "poolConfig": "DbcConfig111", "creator": "DbcCreator111" } }) .to_string(), ), ); dto.id = Some(302); return dto; } fn make_swap_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::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); return dto; } fn make_swap_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 303, None, 0, None, Some(crate::METEORA_DBC_PROGRAM_ID.to_string()), Some("meteora-dbc".to_string()), Some(1), serde_json::json!(["DbcPoolSwap111", "DbcSwapTokenA111", crate::WSOL_MINT_ID]) .to_string(), None, None, Some( serde_json::json!({ "info": { "pool": "DbcPoolSwap111", "baseMint": "DbcSwapTokenA111", "quoteMint": crate::WSOL_MINT_ID } }) .to_string(), ), ); dto.id = Some(304); return dto; } #[test] fn meteora_dbc_create_pool_is_detected() { let decoder = crate::MeteoraDbcDecoder::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::MeteoraDbcDecodedEvent::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(crate::WSOL_MINT_ID.to_string())); assert_eq!(event.config_account, Some("DbcConfig111".to_string())); }, crate::MeteoraDbcDecodedEvent::Swap(_) => { panic!("unexpected swap event") }, } } #[test] fn meteora_dbc_swap_is_detected() { let decoder = crate::MeteoraDbcDecoder::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::MeteoraDbcDecodedEvent::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(crate::WSOL_MINT_ID.to_string())); }, crate::MeteoraDbcDecodedEvent::CreatePool(_) => { panic!("unexpected create event") }, } } #[test] fn meteora_dbc_quote_mint_alone_does_not_trigger_create_pool() { let decoder = crate::MeteoraDbcDecoder::new(); let transaction = make_swap_transaction(); let mut instruction = make_swap_instruction(); instruction.parsed_json = Some( serde_json::json!({ "info": { "quoteMint": crate::WSOL_MINT_ID } }) .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::MeteoraDbcDecodedEvent::Swap(_) => {}, crate::MeteoraDbcDecodedEvent::CreatePool(_) => { panic!("unexpected create event") }, } } #[test] fn meteora_dbc_pool_config_triggers_create_pool() { let decoder = crate::MeteoraDbcDecoder::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::MeteoraDbcDecodedEvent::CreatePool(_) => {}, crate::MeteoraDbcDecodedEvent::Swap(_) => { panic!("unexpected swap event") }, } } #[test] fn meteora_dbc_swap2_discriminator_is_detected() { let data = [0x41, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 0x01]; let kind = super::classify_instruction_kind_from_data(Some(&data)); assert_eq!(kind, super::MeteoraDbcInstructionKind::Swap); } #[test] fn meteora_dbc_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: Swap2".to_string()]; let kind = super::classify_instruction_kind(None, Some(&data), &logs); assert_eq!(kind, super::MeteoraDbcInstructionKind::Unknown); } #[test] fn meteora_dbc_inner_swap2_instruction_with_data_is_not_skipped() { let decoder = crate::MeteoraDbcDecoder::new(); let transaction = make_swap_transaction(); let mut instruction = make_swap_instruction(); instruction.parent_instruction_id = Some(300); instruction.data_json = Some(format!( "\"{}\"", bs58::encode(&[0x41_u8, 0x4b, 0x3f, 0x4c, 0xeb, 0x5b, 0x5b, 0x88, 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::MeteoraDbcDecodedEvent::Swap(event) => { assert_eq!(event.pool_account, Some("DbcPoolSwap111".to_string())); }, crate::MeteoraDbcDecodedEvent::CreatePool(_) => panic!("unexpected create event"), } } }