This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
# file: kb_decoder_spl_associated_token_account/Cargo.toml
# version: 4
[package]
name = "kb_decoder_spl_associated_token_account"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
base64.workspace = true
borsh.workspace = true
kb_core = { path = "../kb_core" }
kb_decoder_api = { path = "../kb_decoder_api" }
kb_model = { path = "../kb_model" }
kb_program_ids = { path = "../kb_program_ids" }
kb_store_core = { path = "../kb_store_core" }
serde_json.workspace = true
sha2.workspace = true
spl-associated-token-account-interface.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,40 @@
<!-- file: kb_decoder_spl_associated_token_account/README.md -->
<!-- version: 2 -->
# kb_decoder_spl_associated_token_account
Ce crate décode exactement le programme SPL Associated Token Account
`ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`.
## Surface décodée
- `Create`, discriminant Borsh `0`, ainsi que sa forme historique vide ;
- `CreateIdempotent`, discriminant `1` ;
- `RecoverNested`, discriminant `2` ;
- cibles SPL Token classique et Token-2022, sans décoder les extensions Token-2022.
Le décodeur conserve le chemin outer/inner, le statut committed, les comptes ordonnés avec leurs
flags et doublons, les rôles ATA, les adresses observées et les PDA canoniques attendus. Une adresse
incorrecte ou une forme de comptes incomplète reste une intention structurée accompagnée de
diagnostics bornés ; elle n'est jamais corrigée silencieusement.
La matrice `docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json` et les tests différentiels imposent
l'égalité avec `spl-associated-token-account-interface 2.0.0`, ses builders et son helper PDA.
## Rôle dans l'écosystème
Ce module fait partie du découpage strict de Khadhroony Bot2. Il doit conserver des dépendances limitées et ne pas contourner les interfaces communes du workspace.
## Matérialisation
Le décodeur ne reconstruit aucun solde ni snapshot final. Les faits commités sont possédés par
`kb_materializer_token_accounts`, tandis que `kb_materializer_risk` possède uniquement le constat
distinct de récupération d'un anti-pattern nested. Les CPI SPL Token restent propriétaires des
initialisations, transferts et fermetures qu'elles effectuent.
## Règles locales
- Les commentaires de code restent en anglais.
- La documentation Markdown reste en français.
- Les exports publics sont contrôlés depuis `lib.rs` lorsque le crate expose une bibliothèque.
- Les binaires utilisent `main.rs` avec les attributs Rust obligatoires.

View File

@@ -0,0 +1,20 @@
// file: kb_decoder_spl_associated_token_account/src/constants.rs
// version: 3
//! Local constants for the `kb_decoder_spl_associated_token_account` crate. Program identifiers live in `kb_program_ids`.
pub(crate) const SURFACE_CODE: &str = "spl_associated_token_account";
pub(crate) const EVENT_VERSION: u32 = 1;
pub(crate) const MAX_INSTRUCTION_BYTES: usize = 64;
pub(crate) const MAX_ACCOUNTS: usize = 64;
pub(crate) const MAX_TRANSACTION_KEYS: usize = 4_096;
pub(crate) const MAX_ACCOUNT_KEY_BYTES: usize = 128;
pub(crate) const MAX_DIAGNOSTICS: usize = 32;
pub(crate) const INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[
(0, "create", false),
(1, "create_idempotent", false),
(2, "recover_nested", false),
];
/// Canonical tracing target for this crate.
pub(crate) const TRACING_TARGET: &str = "kb_decoder_spl_associated_token_account";

View File

@@ -0,0 +1,619 @@
// file: kb_decoder_spl_associated_token_account/src/decoder.rs
// version: 10
//! Exact SPL Associated Token Account dispatch for the common decode pipeline.
const SURFACES: &[kb_decoder_api::DecoderSurface] = &[kb_decoder_api::DecoderSurface {
program_id: kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID,
surface_code: crate::SURFACE_CODE,
priority: 100,
}];
const PROGRAM_IDS: &[&str] = &[kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID];
/// Exact decoder for the SPL Associated Token Account program.
#[derive(Clone, Debug, Default)]
pub struct SplAssociatedTokenAccountDecoder;
impl kb_decoder_api::ProtocolDecoder for crate::SplAssociatedTokenAccountDecoder {
fn decoder_name(&self) -> &'static str {
return "kb_decoder_spl_associated_token_account";
}
fn decoder_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn program_ids(&self) -> &'static [&'static str] {
return PROGRAM_IDS;
}
fn supports_observation(
&self,
observation: &kb_model::ProgramObservation,
) -> kb_decoder_api::DecoderSupport {
return if kb_decoder_api::ProtocolDecoder::handles_program_id(self, &observation.program_id)
{
kb_decoder_api::DecoderSupport::Yes
} else {
kb_decoder_api::DecoderSupport::No
};
}
fn decode_observation(
&self,
_observation: &kb_model::ProgramObservation,
) -> kb_core::Result<std::vec::Vec<kb_model::DecodedProtocolEvent>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl kb_decoder_api::InstructionDecoder for crate::SplAssociatedTokenAccountDecoder {
fn identity(&self) -> kb_decoder_api::DecoderIdentity {
return kb_decoder_api::DecoderIdentity {
name: crate::SURFACE_CODE.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn surfaces(&self) -> &'static [kb_decoder_api::DecoderSurface] {
return SURFACES;
}
fn coverage(&self) -> std::vec::Vec<kb_decoder_api::DecoderCoverageDeclaration> {
return crate::INSTRUCTION_ENTRIES
.iter()
.map(|(tag, code, historical)| {
return kb_decoder_api::DecoderCoverageDeclaration {
program_id: kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
surface_code: std::option::Option::Some(crate::SURFACE_CODE.to_string()),
entry_kind: kb_decoder_api::DecoderCoverageEntryKind::Instruction,
entry_code: (*code).to_string(),
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
historical: *historical,
};
})
.collect();
}
fn recognize(
&self,
input: &kb_store_core::CoreInstructionReplayInput,
) -> kb_decoder_api::DecoderRecognition {
if input.program_id != kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID {
return kb_decoder_api::DecoderRecognition::incompatible();
}
let wire = crate::payload(input).ok();
let entry = wire.as_ref().and_then(|bytes| return crate::entry(bytes));
return kb_decoder_api::DecoderRecognition::compatible(
entry.is_some(),
100,
std::option::Option::Some(crate::SURFACE_CODE.to_string()),
entry.map(|(_, code, _)| return code.to_string()),
wire.and_then(|bytes| return bytes.first().map(|tag| return format!("{tag:02x}"))),
);
}
fn decode(
&self,
input: &kb_store_core::CoreInstructionReplayInput,
) -> kb_decoder_api::DecoderExecutionResult {
if input.program_id != kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID {
return kb_decoder_api::DecoderExecutionResult::unsupported(std::option::Option::None);
}
let result = crate::decode(input);
if matches!(
result.status,
kb_decoder_api::DecoderOutcomeStatus::Failed
| kb_decoder_api::DecoderOutcomeStatus::Unsupported
) {
tracing::error!(
target: crate::TRACING_TARGET,
action = "decode_failure",
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
processor_name = crate::SURFACE_CODE,
processor_version = env!("CARGO_PKG_VERSION"),
input_key = %input.replay_input_key,
result_status = ?result.status,
diagnostics = ?result.diagnostics,
"SPL Associated Token Account instruction was not decoded successfully"
);
}
tracing::debug!(
target: crate::TRACING_TARGET,
action = "decode",
signature = %input.signature,
instruction_path = %input.instruction_path,
transaction_failed = input.transaction_failed,
result_status = ?result.status,
observation_count = result.observations.len(),
"SPL Associated Token Account instruction decode completed"
);
return result;
}
}
#[cfg(test)]
mod tests {
fn matrix() -> serde_json::Value {
return match serde_json::from_str(include_str!(
"../../docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json"
)) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("SPL ATA matrix is invalid: {error}"),
};
}
#[test]
fn published_interface_and_matrix_variants_are_equal() {
let matrix = matrix();
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("SPL ATA matrix has no instructions"),
};
let official = [
(
"create",
spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction::Create,
),
(
"create_idempotent",
spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction::CreateIdempotent,
),
(
"recover_nested",
spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction::RecoverNested,
),
];
assert_eq!(instructions.len(), official.len());
for (position, (name, variant)) in official.iter().enumerate() {
let encoded = match borsh::to_vec(variant) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("cannot encode official ATA variant: {error}")
},
};
assert_eq!(encoded, vec![position as u8]);
assert_eq!(instructions[position]["name"], *name);
assert_eq!(instructions[position]["discriminant"], position as u64);
assert_eq!(instructions[position]["canonicalEncodingHex"], format!("{position:02x}"));
assert_eq!(instructions[position]["officialBuilder"]["available"], true);
}
assert_eq!(matrix["surfaceEquality"]["matrixVariantCount"], 3);
assert_eq!(matrix["surfaceEquality"]["officialInterfaceVariantCount"], 3);
let compiled = crate::INSTRUCTION_ENTRIES
.iter()
.map(|(tag, code, _)| return (std::option::Option::Some(*tag), (*code).to_string()))
.collect::<std::vec::Vec<_>>();
let declared =
kb_decoder_api::InstructionDecoder::coverage(&crate::SplAssociatedTokenAccountDecoder)
.iter()
.map(|entry| {
let tag = entry
.discriminator_hex
.as_deref()
.and_then(|value| return u8::from_str_radix(value, 16).ok());
return (tag, entry.entry_code.clone());
})
.collect::<std::vec::Vec<_>>();
assert_eq!(declared, compiled);
}
#[test]
fn program_ids_accounts_and_pda_seed_order_are_explicit() {
let matrix = matrix();
assert_eq!(matrix["programId"], kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID);
assert_eq!(
spl_associated_token_account_interface::program::ID.to_string(),
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
);
assert_eq!(matrix["limits"]["createRequiredAccountCount"], 6);
assert_eq!(matrix["limits"]["recoverNestedRequiredAccountCount"], 7);
assert_eq!(
matrix["pdaDerivation"]["seedOrder"],
serde_json::json!([
"wallet_address_bytes[32]",
"token_program_id_bytes[32]",
"mint_address_bytes[32]"
])
);
assert_eq!(
matrix["supportedTokenPrograms"].as_array().map(std::vec::Vec::len),
std::option::Option::Some(2)
);
}
#[test]
fn official_builders_match_matrix_wire_and_account_contracts() {
let funding = match "11111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid funding fixture: {error}"),
};
let wallet = match "Vote111111111111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid wallet fixture: {error}"),
};
let owner_mint = match "So11111111111111111111111111111111111111112".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid owner mint fixture: {error}"),
};
let nested_mint = match "SysvarRent111111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid nested mint fixture: {error}"),
};
let token_program = match kb_program_ids::SPL_TOKEN_PROGRAM_ID.parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid Token fixture: {error}"),
};
let create =
spl_associated_token_account_interface::instruction::create_associated_token_account(
&funding,
&wallet,
&owner_mint,
&token_program,
);
let idempotent = spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent(
&funding,
&wallet,
&owner_mint,
&token_program,
);
assert_eq!(create.data, vec![0]);
assert_eq!(idempotent.data, vec![1]);
assert_eq!(create.accounts.len(), 6);
assert_eq!(idempotent.accounts, create.accounts);
assert_eq!(create.accounts[0].pubkey, funding);
assert!(create.accounts[0].is_signer);
assert!(create.accounts[0].is_writable);
assert!(!create.accounts[1].is_signer);
assert!(create.accounts[1].is_writable);
assert_eq!(create.accounts[2].pubkey, wallet);
assert_eq!(create.accounts[3].pubkey, owner_mint);
assert_eq!(create.accounts[4].pubkey.to_string(), "11111111111111111111111111111111");
assert_eq!(create.accounts[5].pubkey, token_program);
let recover = spl_associated_token_account_interface::instruction::recover_nested(
&wallet,
&owner_mint,
&nested_mint,
&token_program,
);
assert_eq!(recover.data, vec![2]);
assert_eq!(recover.accounts.len(), 7);
assert!(recover.accounts[0].is_writable);
assert!(recover.accounts[2].is_writable);
assert_eq!(recover.accounts[5].pubkey, wallet);
assert!(recover.accounts[5].is_signer);
assert!(recover.accounts[5].is_writable);
assert_eq!(recover.accounts[6].pubkey, token_program);
}
#[test]
fn matrix_pda_vectors_match_the_official_helper() {
let matrix = matrix();
let vectors = match matrix["pdaDerivation"]["differentialVectors"].as_array() {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("SPL ATA matrix has no PDA vectors"),
};
for vector in vectors {
let wallet = match vector["wallet"].as_str().and_then(|value| return value.parse().ok())
{
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("invalid wallet PDA fixture"),
};
let mint = match vector["mint"].as_str().and_then(|value| return value.parse().ok()) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("invalid mint PDA fixture"),
};
let token_program =
match vector["tokenProgramId"].as_str().and_then(|value| return value.parse().ok())
{
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("invalid token program PDA fixture"),
};
let (address, bump) = spl_associated_token_account_interface::address::get_associated_token_address_and_bump_seed(
&wallet,
&mint,
&spl_associated_token_account_interface::program::ID,
&token_program,
);
assert_eq!(address.to_string(), vector["expectedAta"]);
assert_eq!(u64::from(bump), vector["expectedBump"]);
}
}
fn replay(
wire: &[u8],
metas: &[(std::string::String, bool, bool)],
failed: bool,
path: &str,
) -> kb_store_core::CoreInstructionReplayInput {
use base64::Engine; // rust-rules: trait-import
let keys = metas
.iter()
.enumerate()
.map(|(position, (key, signer, writable))| {
return serde_json::json!({
"accountIndex": position,
"accountKey": key,
"signer": signer,
"writable": writable,
"source": "static",
});
})
.collect::<std::vec::Vec<_>>();
let accounts = metas
.iter()
.enumerate()
.map(|(position, (key, _, _))| {
return serde_json::json!({
"position": position,
"accountIndex": position,
"accountKey": key,
});
})
.collect::<std::vec::Vec<_>>();
let result = kb_store_core::CoreInstructionReplayInput::new(
format!("signature:{path}"),
"signature",
42,
path,
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID,
failed,
if failed {
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
} else {
std::option::Option::None
},
serde_json::Value::Array(keys),
serde_json::Value::Array(accounts),
std::option::Option::Some(serde_json::json!({
"dataBase64": base64::engine::general_purpose::STANDARD.encode(wire),
})),
std::option::Option::None,
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
serde_json::json!([]),
);
return match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("ATA replay fixture failed: {error}"),
};
}
fn create_fixture(
token_program_text: &str,
idempotent: bool,
) -> (std::vec::Vec<u8>, std::vec::Vec<(std::string::String, bool, bool)>) {
let funding = match "Vote111111111111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid funding fixture: {error}"),
};
let wallet = match "Stake11111111111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid wallet fixture: {error}"),
};
let mint = match "So11111111111111111111111111111111111111112".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid mint fixture: {error}"),
};
let token_program = match token_program_text.parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid token program fixture: {error}"),
};
let instruction = if idempotent {
spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent(
&funding,
&wallet,
&mint,
&token_program,
)
} else {
spl_associated_token_account_interface::instruction::create_associated_token_account(
&funding,
&wallet,
&mint,
&token_program,
)
};
let metas = instruction
.accounts
.iter()
.map(|meta| return (meta.pubkey.to_string(), meta.is_signer, meta.is_writable))
.collect();
return (instruction.data, metas);
}
#[test]
fn exact_dispatch_and_coverage_are_compiled() {
assert_eq!(
kb_decoder_api::InstructionDecoder::coverage(&crate::SplAssociatedTokenAccountDecoder)
.len(),
3
);
let observation = kb_model::ProgramObservation {
signature: kb_model::Signature("signature".to_string()),
slot: kb_model::Slot(1),
instruction_path: kb_model::InstructionPath("0".to_string()),
program_id: kb_model::ProgramId(
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
),
discriminator_8: std::option::Option::None,
data_len: 1,
accounts_len: 6,
failed: false,
};
assert_eq!(
kb_decoder_api::ProtocolDecoder::supports_observation(
&crate::SplAssociatedTokenAccountDecoder,
&observation,
),
kb_decoder_api::DecoderSupport::Yes
);
}
#[test]
fn create_and_legacy_empty_create_decode_with_canonical_address() {
let (wire, metas) = create_fixture(kb_program_ids::SPL_TOKEN_PROGRAM_ID, false);
for candidate in [wire.as_slice(), &[]] {
let input = replay(candidate, metas.as_slice(), false, "0");
let result = kb_decoder_api::InstructionDecoder::decode(
&crate::SplAssociatedTokenAccountDecoder,
&input,
);
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
assert_eq!(
result.recognized_entry_code.as_deref(),
std::option::Option::Some("create")
);
assert_eq!(
result.observations[0].payload_json["addresses"]["associatedTokenAccount"]["valid"],
true
);
assert_eq!(
result.observations[0].payload_json["semanticDiagnostics"],
serde_json::json!([])
);
let expected_hash = if candidate.is_empty() {
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
} else {
"6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"
};
assert_eq!(result.observations[0].payload_json["wire"]["sha256"], expected_hash);
}
}
#[test]
fn token_2022_inner_failed_idempotent_remains_an_uncommitted_intent() {
let (wire, metas) = create_fixture(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, true);
let input = replay(wire.as_slice(), metas.as_slice(), true, "2/1");
let result = kb_decoder_api::InstructionDecoder::decode(
&crate::SplAssociatedTokenAccountDecoder,
&input,
);
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
assert!(!result.observations[0].observation_committed);
assert_eq!(
result.observations[0].event.source_kind,
kb_model::EventSourceKind::InnerInstruction
);
assert_eq!(
result.observations[0].payload_json["addresses"]["tokenProgram"]["kind"],
"token_2022"
);
assert_eq!(result.observations[0].payload_json["tokenProgramExtensionsDecoded"], false);
}
#[test]
fn recover_nested_preserves_mint_roles_and_validates_all_three_pdas() {
let wallet = match "Vote111111111111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid wallet fixture: {error}"),
};
let owner_mint = match "So11111111111111111111111111111111111111112".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid owner mint fixture: {error}"),
};
let nested_mint = match "SysvarRent111111111111111111111111111111111".parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid nested mint fixture: {error}"),
};
let token_program = match kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.parse() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("invalid token program fixture: {error}"),
};
let instruction = spl_associated_token_account_interface::instruction::recover_nested(
&wallet,
&owner_mint,
&nested_mint,
&token_program,
);
let metas = instruction
.accounts
.iter()
.map(|meta| return (meta.pubkey.to_string(), meta.is_signer, meta.is_writable))
.collect::<std::vec::Vec<_>>();
let input = replay(instruction.data.as_slice(), metas.as_slice(), false, "1/0");
let result = kb_decoder_api::InstructionDecoder::decode(
&crate::SplAssociatedTokenAccountDecoder,
&input,
);
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
let addresses = &result.observations[0].payload_json["addresses"];
assert_eq!(addresses["ownerMint"], owner_mint.to_string());
assert_eq!(addresses["nestedMint"], nested_mint.to_string());
for field in [
"ownerAssociatedTokenAccount",
"nestedAssociatedTokenAccount",
"walletNestedMintAssociatedTokenAccount",
] {
assert_eq!(addresses[field]["valid"], true);
}
assert_eq!(
result.observations[0].payload_json["semanticDiagnostics"],
serde_json::json!([])
);
}
#[test]
fn wrong_address_flags_duplicates_extra_and_missing_accounts_stay_decodable() {
let (wire, mut metas) = create_fixture(kb_program_ids::SPL_TOKEN_PROGRAM_ID, false);
metas[1].0 = metas[0].0.clone();
metas[0].1 = false;
metas.push(("SysvarC1ock11111111111111111111111111111111".to_string(), false, false));
let input = replay(wire.as_slice(), metas.as_slice(), false, "0");
let result = kb_decoder_api::InstructionDecoder::decode(
&crate::SplAssociatedTokenAccountDecoder,
&input,
);
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
let diagnostics = result.observations[0].payload_json["semanticDiagnostics"].as_array();
let diagnostics = match diagnostics {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("ATA diagnostics are not an array"),
};
for code in [
"unexpected_account_count",
"account_flags_mismatch",
"duplicate_account_key",
"associated_address_mismatch",
] {
assert!(diagnostics.iter().any(|value| return value["code"] == code));
}
metas.truncate(3);
let input = replay(wire.as_slice(), metas.as_slice(), false, "0");
let result = kb_decoder_api::InstructionDecoder::decode(
&crate::SplAssociatedTokenAccountDecoder,
&input,
);
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
assert!(
result.observations[0].payload_json["semanticDiagnostics"]
.as_array()
.is_some_and(|values| return values
.iter()
.any(|value| return value["code"] == "missing_required_account"))
);
}
#[test]
fn unknown_and_suffixed_wire_fail_with_bounded_diagnostics() {
let (_, metas) = create_fixture(kb_program_ids::SPL_TOKEN_PROGRAM_ID, false);
for wire in [std::vec![9], std::vec![0, 0]] {
let input = replay(wire.as_slice(), metas.as_slice(), false, "0");
let result = kb_decoder_api::InstructionDecoder::decode(
&crate::SplAssociatedTokenAccountDecoder,
&input,
);
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Failed);
assert_eq!(result.observations.len(), 0);
assert_eq!(result.diagnostics.len(), 1);
assert!(result.diagnostics[0].message.len() < 256);
}
}
}

View File

@@ -0,0 +1,460 @@
// file: kb_decoder_spl_associated_token_account/src/instruction.rs
// version: 3
//! 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::INSTRUCTION_ENTRIES
.iter()
.copied()
.find(|(tag, _, _)| return *tag == bytes[0]);
}
pub(crate) fn payload(
input: &kb_store_core::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::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: &kb_store_core::CoreInstructionReplayInput,
) -> kb_decoder_api::DecoderExecutionResult {
let bytes = match crate::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::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 = kb_model::DecoderConfidence::ManualExact;
let event = kb_model::DecodedProtocolEvent {
signature: kb_model::Signature(input.signature.clone()),
slot: kb_model::Slot(input.slot),
instruction_path: kb_model::InstructionPath(input.instruction_path.clone()),
program_id: kb_model::ProgramId(input.program_id.clone()),
protocol_code: kb_model::ProtocolCode(crate::SURFACE_CODE.to_string()),
surface_code: kb_model::SurfaceCode(crate::SURFACE_CODE.to_string()),
event_code: kb_model::EventCode(format!("{}.{}", crate::SURFACE_CODE, instruction.1)),
event_name: kb_model::EventName(instruction.1.to_string()),
event_family: kb_model::EventFamily::Lifecycle,
source_kind: if input.instruction_path.contains('/') {
kb_model::EventSourceKind::InnerInstruction
} else {
kb_model::EventSourceKind::Instruction
},
confidence,
};
let observation = kb_decoder_api::DecodedObservation {
event_key: format!("ata:{}:0", instruction.1),
event,
payload_json: serde_json::json!({
"eventVersion": crate::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: kb_decoder_api::DecoderProof {
kind: kb_decoder_api::DecoderProofKind::Manual,
confidence,
evidence: std::vec![
"spl_associated_token_account_interface_2_0_0_and_processor_audit".to_string(),
format!("wire_sha256:{hash}"),
],
},
};
return kb_decoder_api::DecoderExecutionResult {
status: kb_decoder_api::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: &kb_store_core::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::MAX_ACCOUNTS {
return std::result::Result::Err(kb_core::Error::invalid_state(
"ATA instruction account count exceeds 64",
));
}
if keys.len() > crate::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::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::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,
) -> kb_decoder_api::DecoderExecutionResult {
return kb_decoder_api::DecoderExecutionResult {
status: kb_decoder_api::DecoderOutcomeStatus::Failed,
recognized_entry_code: entry.map(|value| return value.to_string()),
observations: std::vec::Vec::new(),
diagnostics: std::vec![kb_decoder_api::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();
}

View File

@@ -0,0 +1,39 @@
// file: kb_decoder_spl_associated_token_account/src/lib.rs
// version: 7
//! Decoder crate for `spl_associated_token_account`.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod constants;
mod decoder;
mod instruction;
/// Crate-root access to `EVENT_VERSION` from `constants`.
pub(crate) use crate::constants::EVENT_VERSION;
/// Crate-root access to `INSTRUCTION_ENTRIES` from `constants`.
pub(crate) use crate::constants::INSTRUCTION_ENTRIES;
/// Crate-root access to `MAX_ACCOUNT_KEY_BYTES` from `constants`.
pub(crate) use crate::constants::MAX_ACCOUNT_KEY_BYTES;
/// Crate-root access to `MAX_ACCOUNTS` from `constants`.
pub(crate) use crate::constants::MAX_ACCOUNTS;
/// Crate-root access to `MAX_DIAGNOSTICS` from `constants`.
pub(crate) use crate::constants::MAX_DIAGNOSTICS;
/// Crate-root access to `MAX_INSTRUCTION_BYTES` from `constants`.
pub(crate) use crate::constants::MAX_INSTRUCTION_BYTES;
/// Crate-root access to `MAX_TRANSACTION_KEYS` from `constants`.
pub(crate) use crate::constants::MAX_TRANSACTION_KEYS;
/// Crate-root access to `SURFACE_CODE` from `constants`.
pub(crate) use crate::constants::SURFACE_CODE;
/// Canonical tracing target for this crate.
pub(crate) use crate::constants::TRACING_TARGET;
/// Crate-root access to `decode` from `instruction`.
pub(crate) use crate::instruction::decode;
/// Crate-root access to `entry` from `instruction`.
pub(crate) use crate::instruction::entry;
/// Crate-root access to `payload` from `instruction`.
pub(crate) use crate::instruction::payload;
/// Exact SPL Associated Token Account decoder.
pub use crate::decoder::SplAssociatedTokenAccountDecoder;