// file: kb-lib/src/decoder/spl/associated_token_account/wire.rs // version: 1 //! Bounded ATA wire, ordered account and canonical PDA decoding. use base64::Engine; // rust-rules: trait-import use sha2::Digest; // rust-rules: trait-import #[derive(Clone, Debug)] struct Account { position: usize, account_index: u64, key: std::string::String, signer: bool, writable: bool, source: serde_json::Value, } pub(crate) fn entry(bytes: &[u8]) -> std::option::Option<(u8, &'static str, bool)> { if bytes.is_empty() { return std::option::Option::Some((0, "create", true)); } if bytes.len() != 1 { return std::option::Option::None; } return crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES .iter() .copied() .find(|(tag, _, _)| return *tag == bytes[0]); } pub(crate) fn payload( input: &crate::CoreInstructionReplayInput, ) -> kb_core::Result> { let value = match input.instruction_payload_json.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA instruction payload is not retained", )); }, }; let encoded = match value.get("dataBase64").and_then(serde_json::Value::as_str) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA payload does not contain dataBase64", )); }, }; if encoded.len() > 92 { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA base64 payload exceeds the bounded input limit", )); } let bytes = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA payload is not valid base64: {error}" ))); }, }; if bytes.len() > crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA decoded payload exceeds 64 bytes", )); } return std::result::Result::Ok(bytes); } pub(crate) fn decode(input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult { let bytes = match crate::spl_associated_token_account_payload(input) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return failed(std::option::Option::None, "ata_payload_unavailable", error.to_string()); }, }; let instruction = match crate::spl_associated_token_account_entry(bytes.as_slice()) { std::option::Option::Some(value) => value, std::option::Option::None => { let code = if bytes.len() > 1 { "malformed_ata_instruction" } else { "unknown_ata_instruction_tag" }; return failed( std::option::Option::None, code, format!("unrecognized ATA wire; payloadPrefixHex={}", hex(bytes.as_slice())), ); }, }; let accounts = match accounts(input) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return failed( std::option::Option::Some(instruction.1), "ata_accounts_malformed", error.to_string(), ); }, }; let diagnostics = diagnostics(instruction.1, accounts.as_slice()); let projected_accounts = project_accounts(instruction.1, accounts.as_slice()); let addresses = addresses(instruction.1, accounts.as_slice()); let hash = hash(bytes.as_slice()); let committed = !input.transaction_failed; let confidence = crate::DecoderConfidence::ManualExact; let event = crate::DecodedProtocolEvent { signature: crate::Signature(input.signature.clone()), slot: crate::Slot(input.slot), instruction_path: crate::InstructionPath(input.instruction_path.clone()), program_id: crate::ProgramId(input.program_id.clone()), protocol_code: crate::ProtocolCode( crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(), ), surface_code: crate::SurfaceCode( crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(), ), event_code: crate::EventCode(format!( "{}.{}", crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE, instruction.1 )), event_name: crate::EventName(instruction.1.to_string()), event_family: crate::EventFamily::Lifecycle, source_kind: if input.instruction_path.contains('/') { crate::EventSourceKind::InnerInstruction } else { crate::EventSourceKind::Instruction }, confidence, }; let observation = crate::DecodedObservation { event_key: format!("ata:{}:0", instruction.1), event, payload_json: serde_json::json!({ "eventVersion": crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION, "programId": input.program_id, "instruction": instruction.1, "wireTag": instruction.0, "wireTagHex": format!("{:02x}", instruction.0), "wireEncoding": if bytes.is_empty() { "legacy_empty_create" } else { "borsh_enum_discriminant" }, "instructionPath": input.instruction_path, "instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" }, "transactionSucceeded": !input.transaction_failed, "committed": committed, "accounts": projected_accounts, "addresses": addresses, "semanticDiagnostics": diagnostics, "wire": { "lengthBytes": bytes.len(), "sha256": hash, "prefixHex": hex(bytes.as_slice()), "complete": true, }, "tokenProgramExtensionsDecoded": false, "stateSnapshotReconstructed": false, }), transaction_failed: input.transaction_failed, transaction_error: input.transaction_err_json.clone(), observation_committed: committed, proof: crate::DecoderProof { kind: crate::DecoderProofKind::Manual, confidence, evidence: std::vec![ "spl_associated_token_account_interface_2_0_0_and_processor_audit".to_string(), format!("wire_sha256:{hash}"), ], }, }; return crate::DecoderExecutionResult { status: crate::DecoderOutcomeStatus::Decoded, recognized_entry_code: std::option::Option::Some(instruction.1.to_string()), observations: std::vec![observation], diagnostics: std::vec::Vec::new(), }; } fn accounts(input: &crate::CoreInstructionReplayInput) -> kb_core::Result> { let instruction_accounts = match input.instruction_accounts_json.as_array() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA instruction accounts must be a JSON array", )); }, }; let keys = match input.account_keys_json.as_array() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA transaction account keys must be a JSON array", )); }, }; if instruction_accounts.len() > crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA instruction account count exceeds 64", )); } if keys.len() > crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS { return std::result::Result::Err(kb_core::Error::invalid_state( "ATA transaction account key count exceeds 4096", )); } let mut output = std::vec::Vec::with_capacity(instruction_accounts.len()); for (position, value) in instruction_accounts.iter().enumerate() { let index = match value.get("accountIndex").and_then(serde_json::Value::as_u64) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA instruction account {position} has no accountIndex" ))); }, }; let key = match value.get("accountKey").and_then(serde_json::Value::as_str) { std::option::Option::Some(value) if !value.is_empty() && value.len() <= crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES => { value }, _ => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA instruction account {position} has no accountKey" ))); }, }; let resolved = match keys.iter().find(|candidate| { return candidate.get("accountIndex").and_then(serde_json::Value::as_u64) == std::option::Option::Some(index); }) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA account index {index} is not resolved" ))); }, }; if resolved.get("accountKey").and_then(serde_json::Value::as_str) != std::option::Option::Some(key) { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA account index {index} resolves to a different key" ))); } let signer = match resolved.get("signer").and_then(serde_json::Value::as_bool) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA account index {index} has no signer flag" ))); }, }; let writable = match resolved.get("writable").and_then(serde_json::Value::as_bool) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::invalid_state(format!( "ATA account index {index} has no writable flag" ))); }, }; output.push(Account { position, account_index: index, key: key.to_string(), signer, writable, source: resolved.get("source").cloned().unwrap_or(serde_json::Value::Null), }); } return std::result::Result::Ok(output); } fn roles(code: &str) -> &'static [(&'static str, bool, bool)] { return if code == "recover_nested" { &[ ("nested_associated_token_account", false, true), ("nested_mint", false, false), ("wallet_nested_mint_associated_token_account", false, true), ("owner_associated_token_account", false, false), ("owner_mint", false, false), ("wallet_owner", true, true), ("token_program", false, false), ] } else { &[ ("funding_account", true, true), ("associated_token_account", false, true), ("wallet_owner", false, false), ("mint", false, false), ("system_program", false, false), ("token_program", false, false), ] }; } fn project_accounts(code: &str, accounts: &[Account]) -> serde_json::Value { let roles = roles(code); return serde_json::Value::Array(accounts.iter().map(|account| return serde_json::json!({ "position": account.position, "accountIndex": account.account_index, "accountKey": account.key, "signer": account.signer, "writable": account.writable, "source": account.source, "role": roles.get(account.position).map(|value| return value.0).unwrap_or("extra_account"), })).collect()); } fn diagnostics(code: &str, accounts: &[Account]) -> serde_json::Value { let roles = roles(code); let mut values = std::vec::Vec::new(); if accounts.len() != roles.len() { values.push(serde_json::json!({"code":"unexpected_account_count","expected":roles.len(),"observed":accounts.len()})); } for (position, (role, signer, writable)) in roles.iter().enumerate() { if let std::option::Option::Some(account) = accounts.get(position) { if account.signer != *signer || account.writable != *writable { values.push(serde_json::json!({"code":"account_flags_mismatch","position":position,"role":role,"expectedSigner":signer,"observedSigner":account.signer,"expectedWritable":writable,"observedWritable":account.writable})); } } else { values.push(serde_json::json!({"code":"missing_required_account","position":position,"role":role})); } } for left in 0..accounts.len() { for right in left.saturating_add(1)..accounts.len() { if accounts[left].key == accounts[right].key { values.push(serde_json::json!({"code":"duplicate_account_key","positions":[left,right],"accountKey":accounts[left].key})); } } } let expected = addresses(code, accounts); for field in [ "associatedTokenAccount", "ownerAssociatedTokenAccount", "nestedAssociatedTokenAccount", "walletNestedMintAssociatedTokenAccount", ] { if expected .get(field) .and_then(|value| return value.get("valid")) .and_then(serde_json::Value::as_bool) == std::option::Option::Some(false) { values.push(serde_json::json!({"code":"associated_address_mismatch","field":field,"observed":expected[field]["observed"],"expected":expected[field]["expected"]})); } } let token_position = if code == "recover_nested" { 6 } else { 5 }; if accounts.get(token_position).is_some_and(|account| { return account.key != kb_program_ids::SPL_TOKEN_PROGRAM_ID && account.key != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID; }) { values.push(serde_json::json!({"code":"unsupported_token_program","observed":accounts.get(token_position).map(|value| return value.key.clone())})); } if code != "recover_nested" && accounts .get(4) .is_some_and(|account| return account.key != "11111111111111111111111111111111") { values .push(serde_json::json!({"code":"system_program_mismatch","observed":accounts[4].key})); } values.truncate(crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS); return serde_json::Value::Array(values); } fn addresses(code: &str, accounts: &[Account]) -> serde_json::Value { let token_position = if code == "recover_nested" { 6 } else { 5 }; let token = accounts.get(token_position).map(|value| return value.key.as_str()); if code == "recover_nested" { return serde_json::json!({ "tokenProgram": token_kind(token), "ownerAssociatedTokenAccount": derived(accounts.get(5), accounts.get(4), token, accounts.get(3)), "nestedAssociatedTokenAccount": derived(accounts.get(3), accounts.get(1), token, accounts.first()), "walletNestedMintAssociatedTokenAccount": derived(accounts.get(5), accounts.get(1), token, accounts.get(2)), "walletOwner": accounts.get(5).map(|value| return value.key.clone()), "ownerMint": accounts.get(4).map(|value| return value.key.clone()), "nestedMint": accounts.get(1).map(|value| return value.key.clone()), }); } return serde_json::json!({ "tokenProgram": token_kind(token), "associatedTokenAccount": derived(accounts.get(2), accounts.get(3), token, accounts.get(1)), "fundingAccount": accounts.first().map(|value| return value.key.clone()), "walletOwner": accounts.get(2).map(|value| return value.key.clone()), "mint": accounts.get(3).map(|value| return value.key.clone()), }); } fn derived( wallet: std::option::Option<&Account>, mint: std::option::Option<&Account>, token: std::option::Option<&str>, observed: std::option::Option<&Account>, ) -> serde_json::Value { let expected = match (wallet, mint, token) { ( std::option::Option::Some(wallet), std::option::Option::Some(mint), std::option::Option::Some(token), ) => { let wallet_key = wallet.key.parse(); let mint_key = mint.key.parse(); let token_key = token.parse(); match (wallet_key, mint_key, token_key) { (std::result::Result::Ok(wallet_key), std::result::Result::Ok(mint_key), std::result::Result::Ok(token_key)) => std::option::Option::Some(spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(&wallet_key, &mint_key, &token_key).to_string()), _ => std::option::Option::None, } }, _ => std::option::Option::None, }; let observed = observed.map(|value| return value.key.clone()); let valid = expected.is_some() && expected == observed; return serde_json::json!({"expected":expected,"observed":observed,"valid":valid}); } fn token_kind(token: std::option::Option<&str>) -> serde_json::Value { return match token { std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID) => { serde_json::json!({"programId":kb_program_ids::SPL_TOKEN_PROGRAM_ID,"kind":"spl_token_classic","supported":true}) }, std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID) => { serde_json::json!({"programId":kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,"kind":"token_2022","supported":true}) }, std::option::Option::Some(value) => { serde_json::json!({"programId":value,"kind":"unsupported","supported":false}) }, std::option::Option::None => { serde_json::json!({"programId":serde_json::Value::Null,"kind":"missing","supported":false}) }, }; } fn failed( entry: std::option::Option<&str>, code: &str, message: std::string::String, ) -> crate::DecoderExecutionResult { return crate::DecoderExecutionResult { status: crate::DecoderOutcomeStatus::Failed, recognized_entry_code: entry.map(|value| return value.to_string()), observations: std::vec::Vec::new(), diagnostics: std::vec![crate::DecoderDiagnostic { code: code.to_string(), message, retriable: false }], }; } fn hash(bytes: &[u8]) -> std::string::String { let digest = sha2::Sha256::digest(bytes); let mut output = std::string::String::with_capacity(64); for byte in digest { output.push_str(format!("{byte:02x}").as_str()); } return output; } fn hex(bytes: &[u8]) -> std::string::String { return bytes.iter().take(16).map(|value| return format!("{value:02x}")).collect(); }