// file: kb_lib/src/db/dtos/program_instruction_diagnostic.rs //! Program instruction diagnostic DTO. /// Diagnostic row for instructions of one Solana program. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct ProgramInstructionDiagnosticDto { /// Parent transaction id. pub transaction_id: i64, /// Transaction signature. pub signature: std::string::String, /// Optional Solana slot. pub slot: std::option::Option, /// Internal instruction id. pub instruction_id: i64, /// Optional parent instruction id. pub parent_instruction_id: std::option::Option, /// Outer instruction index. pub instruction_index: u32, /// Optional inner instruction index. pub inner_instruction_index: std::option::Option, /// Program id. pub program_id: std::option::Option, /// Optional program name. pub program_name: std::option::Option, /// Optional stack height. pub stack_height: std::option::Option, /// Number of accounts in `accounts_json`. pub accounts_count: u64, /// First account, when present. pub account_0: std::option::Option, /// Second account, when present. pub account_1: std::option::Option, /// Third account, when present. pub account_2: std::option::Option, /// Fourth account, when present. pub account_3: std::option::Option, /// Last account, when present. pub last_account: std::option::Option, /// Optional parsed instruction type. pub parsed_type: std::option::Option, /// True when `data_json` exists. pub has_data_json: bool, /// True when `parsed_json` exists. pub has_parsed_json: bool, /// Short data JSON preview. pub data_json_preview: std::option::Option, /// Short parsed JSON preview. pub parsed_json_preview: std::option::Option, /// JSON array of useful log hints. pub log_hints_json: std::string::String, } impl TryFrom for ProgramInstructionDiagnosticDto { type Error = crate::Error; fn try_from(entity: crate::ProgramInstructionDiagnosticEntity) -> Result { let slot = match entity.slot { Some(slot) => match u64::try_from(slot) { Ok(slot) => Some(slot), Err(error) => { return Err(crate::Error::Db(format!( "cannot convert program instruction diagnostic slot '{}' to u64: {}", slot, error ))); }, }, None => None, }; let instruction_index = match u32::try_from(entity.instruction_index) { Ok(instruction_index) => instruction_index, Err(error) => { return Err(crate::Error::Db(format!( "cannot convert program instruction diagnostic instruction_index '{}' to u32: {}", entity.instruction_index, error ))); }, }; let inner_instruction_index = match entity.inner_instruction_index { Some(inner_instruction_index) => match u32::try_from(inner_instruction_index) { Ok(inner_instruction_index) => Some(inner_instruction_index), Err(error) => { return Err(crate::Error::Db(format!( "cannot convert program instruction diagnostic inner_instruction_index '{}' to u32: {}", inner_instruction_index, error ))); }, }, None => None, }; let stack_height = match entity.stack_height { Some(stack_height) => match u32::try_from(stack_height) { Ok(stack_height) => Some(stack_height), Err(error) => { return Err(crate::Error::Db(format!( "cannot convert program instruction diagnostic stack_height '{}' to u32: {}", stack_height, error ))); }, }, None => None, }; let accounts = parse_accounts_json(entity.accounts_json.as_str()); let accounts_count = match u64::try_from(accounts.len()) { Ok(accounts_count) => accounts_count, Err(error) => { return Err(crate::Error::Db(format!( "cannot convert accounts count to u64: {}", error ))); }, }; let account_0 = account_at(&accounts, 0); let account_1 = account_at(&accounts, 1); let account_2 = account_at(&accounts, 2); let account_3 = account_at(&accounts, 3); let last_account = match accounts.last() { Some(last_account) => Some(last_account.clone()), None => None, }; let log_hints = collect_log_hints(entity.meta_json.as_deref(), entity.transaction_json.as_str()); let log_hints_json_result = serde_json::to_string(&log_hints); let log_hints_json = match log_hints_json_result { Ok(log_hints_json) => log_hints_json, Err(error) => { return Err(crate::Error::Json(format!( "cannot serialize program instruction log hints: {}", error ))); }, }; return Ok(Self { transaction_id: entity.transaction_id, signature: entity.signature, slot, instruction_id: entity.instruction_id, parent_instruction_id: entity.parent_instruction_id, instruction_index, inner_instruction_index, program_id: entity.program_id, program_name: entity.program_name, stack_height, accounts_count, account_0, account_1, account_2, account_3, last_account, parsed_type: entity.parsed_type, has_data_json: entity.data_json.is_some(), has_parsed_json: entity.parsed_json.is_some(), data_json_preview: preview_text(entity.data_json.as_deref(), 600), parsed_json_preview: preview_text(entity.parsed_json.as_deref(), 1200), log_hints_json, }); } } fn account_at( accounts: &[std::string::String], index: usize, ) -> std::option::Option { match accounts.get(index) { Some(account) => return Some(account.clone()), None => return None, } } fn preview_text( text: std::option::Option<&str>, max_len: usize, ) -> std::option::Option { let text = match text { Some(text) => text, None => return None, }; if text.len() <= max_len { return Some(text.to_string()); } let mut preview = text.chars().take(max_len).collect::(); preview.push_str("..."); return Some(preview); } fn parse_accounts_json(accounts_json: &str) -> std::vec::Vec { let parsed_result = serde_json::from_str::(accounts_json); let parsed = match parsed_result { Ok(parsed) => parsed, Err(_) => return std::vec::Vec::new(), }; let array = match parsed.as_array() { Some(array) => array, None => return 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 accounts; } fn collect_log_hints( meta_json: std::option::Option<&str>, transaction_json: &str, ) -> std::vec::Vec { let mut hints = std::vec::Vec::new(); if let Some(meta_json) = meta_json { collect_log_hints_from_json_text(meta_json, &mut hints); } collect_log_hints_from_json_text(transaction_json, &mut hints); hints.sort(); hints.dedup(); return hints; } fn collect_log_hints_from_json_text( json_text: &str, hints: &mut std::vec::Vec, ) { let value_result = serde_json::from_str::(json_text); let value = match value_result { Ok(value) => value, Err(_) => return, }; collect_log_hints_from_value(&value, hints); } fn collect_log_hints_from_value( value: &serde_json::Value, hints: &mut std::vec::Vec, ) { match value { serde_json::Value::String(text) => { let normalized = text.to_ascii_lowercase(); if normalized.contains("instruction:") || normalized.contains("meteora") || normalized.contains("dlmm") || normalized.contains("lb") || normalized.contains("swap") || normalized.contains("bin") { hints.push(text.clone()); } }, serde_json::Value::Array(values) => { for nested in values { collect_log_hints_from_value(nested, hints); } }, serde_json::Value::Object(object) => { for nested in object.values() { collect_log_hints_from_value(nested, hints); } }, _ => {}, } } #[cfg(test)] mod tests { #[test] fn accounts_json_extracts_string_accounts() { let accounts = super::parse_accounts_json( serde_json::json!(["A111", "B222", "C333"]).to_string().as_str(), ); assert_eq!(accounts.len(), 3); assert_eq!(accounts[0], "A111"); assert_eq!(accounts[1], "B222"); assert_eq!(accounts[2], "C333"); } #[test] fn log_hints_are_extracted_from_nested_json() { let mut hints = std::vec::Vec::new(); super::collect_log_hints_from_json_text( serde_json::json!({ "meta": { "logMessages": [ "Program log: Instruction: Swap", "irrelevant" ] } }) .to_string() .as_str(), &mut hints, ); assert_eq!(hints.len(), 1); assert_eq!(hints[0], "Program log: Instruction: Swap"); } }