Files
khadhroony-bot3/kb-lib/src/decoder/spl/associated_token_account/wire.rs
2026-07-23 20:00:04 +02:00

466 lines
19 KiB
Rust

// 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<std::vec::Vec<u8>> {
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<std::vec::Vec<Account>> {
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();
}