// file: kb_lib/src/dex/dexlab.rs //! DexLab Swap/Pool transaction decoder. /// Decoded DexLab create-pool event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DexlabCreatePoolDecoded { /// 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 creator / payer. pub creator: std::option::Option, /// Optional fee tier representation. pub fee_tier: std::option::Option, /// Decoded payload. pub payload_json: serde_json::Value, } /// Decoded DexLab swap event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct DexlabSwapDecoded { /// 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 DexLab event. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum DexlabDecodedEvent { /// Pool creation. CreatePool(DexlabCreatePoolDecoded), /// Swap. Swap(DexlabSwapDecoded), } /// DexLab decoder. #[derive(Debug, Clone, Default)] pub struct DexlabDecoder; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum DexlabInstructionKind { CreatePool, Swap, Unknown, } impl DexlabDecoder { /// Creates a new decoder. pub fn new() -> Self { return Self; } /// Decodes one projected transaction into zero or more DexLab 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 { 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::DEXLAB_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_kind = classify_instruction_kind(parsed_json.as_ref(), &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(), &["tokenA", "tokenAMint", "mintA", "baseMint", "token0Mint", "mint0"], ) .or_else(|| return extract_account(&accounts, 1)); let token_b_mint = extract_string_by_candidate_keys( parsed_json.as_ref(), &["tokenB", "tokenBMint", "mintB", "quoteMint", "token1Mint", "mint1"], ) .or_else(|| return extract_account(&accounts, 2)); let creator = extract_string_by_candidate_keys( parsed_json.as_ref(), &["payer", "creator", "user", "owner"], ) .or_else(|| return extract_account(&accounts, 3)); let fee_tier = extract_string_by_candidate_keys( parsed_json.as_ref(), &["feeTier", "fee_tier", "tradeFeeTier", "feeRate"], ); if instruction_kind == DexlabInstructionKind::CreatePool { let payload_json = serde_json::json!({ "decoder": "dexlab", "eventKind": "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, "creator": creator, "feeTier": fee_tier }); decoded_events.push(crate::DexlabDecodedEvent::CreatePool( crate::DexlabCreatePoolDecoded { transaction_id, instruction_id, signature: transaction.signature.clone(), program_id: program_id.clone(), pool_account, token_a_mint, token_b_mint, creator, fee_tier, payload_json, }, )); continue; } if instruction_kind == DexlabInstructionKind::Swap { let trade_side = infer_trade_side(&log_messages); let payload_json = serde_json::json!({ "decoder": "dexlab", "eventKind": "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::DexlabDecodedEvent::Swap(crate::DexlabSwapDecoded { 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>, log_messages: &[std::string::String], ) -> DexlabInstructionKind { 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("createpool") || normalized.contains("initializepool") { return DexlabInstructionKind::CreatePool; } if normalized == "swap" { return DexlabInstructionKind::Swap; } } if log_messages_contain_keyword(log_messages, "create_pool") || log_messages_contain_keyword(log_messages, "createpool") || log_messages_contain_keyword(log_messages, "initialize_pool") || log_messages_contain_keyword(log_messages, "initializepool") { return DexlabInstructionKind::CreatePool; } if log_messages_contain_keyword(log_messages, "swap") { return DexlabInstructionKind::Swap; } return DexlabInstructionKind::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 = match logs.as_array() { 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_contain_keyword: &[std::string::String], keyword: &str, ) -> bool { let keyword_normalized = normalize_text(keyword); for log_message in log_messages_contain_keyword { let log_normalized = normalize_text(log_message.as_str()); if log_normalized.contains(keyword_normalized.as_str()) { return true; } } return false; } fn normalize_text(normalize_text: &str) -> std::string::String { let mut normalized = std::string::String::new(); for character in normalize_text.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()); } } 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 extract_account( extract_account: &[std::string::String], index: usize, ) -> std::option::Option { if index >= extract_account.len() { return None; } return Some(extract_account[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-dexlab-create-1".to_string(), Some(893001), Some(1779800001), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 893001, "meta": { "logMessages": [ "Program log: Instruction: CreatePool" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(801); return dto; } fn make_create_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 801, None, 0, None, Some(crate::DEXLAB_PROGRAM_ID.to_string()), Some("dexlab".to_string()), Some(1), serde_json::json!([ "DexlabPool111", "DexlabTokenA111", crate::WSOL_MINT_ID, "DexlabCreator111" ]) .to_string(), None, None, Some( serde_json::json!({ "info": { "instruction": "create_pool", "pool": "DexlabPool111", "tokenA": "DexlabTokenA111", "tokenB": crate::WSOL_MINT_ID, "payer": "DexlabCreator111", "feeTier": "0.3%" } }) .to_string(), ), ); dto.id = Some(802); return dto; } fn make_swap_transaction() -> crate::ChainTransactionDto { let mut dto = crate::ChainTransactionDto::new( "sig-dexlab-swap-1".to_string(), Some(893002), Some(1779800002), Some("helius_primary_http".to_string()), Some("0".to_string()), None, None, serde_json::json!({ "slot": 893002, "meta": { "logMessages": [ "Program log: Instruction: Swap" ] }, "transaction": { "message": { "instructions": [] } } }) .to_string(), ); dto.id = Some(803); return dto; } fn make_swap_instruction() -> crate::ChainInstructionDto { let mut dto = crate::ChainInstructionDto::new( 803, None, 0, None, Some(crate::DEXLAB_PROGRAM_ID.to_string()), Some("dexlab".to_string()), Some(1), serde_json::json!(["DexlabSwapPool111", "DexlabSwapTokenA111", crate::WSOL_MINT_ID]) .to_string(), None, None, Some( serde_json::json!({ "info": { "instruction": "swap", "pool": "DexlabSwapPool111", "tokenA": "DexlabSwapTokenA111", "tokenB": crate::WSOL_MINT_ID } }) .to_string(), ), ); dto.id = Some(804); return dto; } #[test] fn dexlab_create_pool_is_detected() { let decoder = crate::DexlabDecoder::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::DexlabDecodedEvent::CreatePool(event) => { assert_eq!(event.transaction_id, 801); assert_eq!(event.instruction_id, 802); assert_eq!(event.pool_account, Some("DexlabPool111".to_string())); assert_eq!(event.token_a_mint, Some("DexlabTokenA111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); assert_eq!(event.fee_tier, Some("0.3%".to_string())); }, crate::DexlabDecodedEvent::Swap(_) => { panic!("unexpected swap event") }, } } #[test] fn dexlab_swap_is_detected() { let decoder = crate::DexlabDecoder::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::DexlabDecodedEvent::Swap(event) => { assert_eq!(event.transaction_id, 803); assert_eq!(event.instruction_id, 804); assert_eq!(event.pool_account, Some("DexlabSwapPool111".to_string())); assert_eq!(event.token_a_mint, Some("DexlabSwapTokenA111".to_string())); assert_eq!(event.token_b_mint, Some(crate::WSOL_MINT_ID.to_string())); }, crate::DexlabDecodedEvent::CreatePool(_) => { panic!("unexpected create event") }, } } }