v0.1.0-pre.007

This commit is contained in:
2026-07-23 20:00:04 +02:00
parent 1eeae9fa4b
commit f5bd8fab7f
18 changed files with 1315 additions and 61 deletions

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder.rs
// version: 3
// version: 4
//! Consolidated decoder modules.
@@ -33,6 +33,24 @@ pub mod vesting;
pub mod wallet;
pub mod weighted;
/// Current stable Associated Token Account decoded event contract version.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
/// Exact Associated Token Account instructions published by the official interface.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
/// Maximum retained account-key text length for the Associated Token Account decoder.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
/// Maximum retained instruction account count for the Associated Token Account decoder.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
/// Maximum retained semantic diagnostic count for the Associated Token Account decoder.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
/// Maximum retained Associated Token Account instruction payload size.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
/// Maximum retained transaction account-key count for the Associated Token Account decoder.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
/// Stable protocol and surface code for the Associated Token Account program.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
/// Canonical tracing target for the Associated Token Account decoder.
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
/// Maximum payload prefix retained for bounded diagnostics.
pub(crate) use self::spl::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
@@ -65,6 +83,12 @@ pub(crate) use self::spl::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
pub(crate) use self::spl::SPL_TOKEN_SURFACE_CODE;
/// Canonical tracing target for the classic SPL Token decoder.
pub(crate) use self::spl::SPL_TOKEN_TRACING_TARGET;
/// Decodes one bounded Associated Token Account instruction.
pub(crate) use self::spl::spl_associated_token_account_decode;
/// Returns the exact entry matching an Associated Token Account wire.
pub(crate) use self::spl::spl_associated_token_account_entry;
/// Reads one bounded Associated Token Account payload.
pub(crate) use self::spl::spl_associated_token_account_payload;
/// Decodes one bounded SPL Memo instruction.
pub(crate) use self::spl::spl_memo_decode;
/// Decodes one bounded classic SPL Token instruction.
@@ -74,6 +98,8 @@ pub(crate) use self::spl::spl_token_entry_for_tag;
/// Returns the first byte of one retained classic SPL Token payload.
pub(crate) use self::spl::spl_token_payload_tag;
/// Exact decoder for the SPL Associated Token Account program.
pub use self::spl::SplAssociatedTokenAccountDecoder;
/// Exact decoder for the three registered SPL Memo generations.
pub use self::spl::SplMemoDecoder;
/// Exact decoder for the classic SPL Token program.

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder/spl.rs
// version: 3
// version: 4
//! `spl` decoder family.
@@ -13,6 +13,30 @@ pub mod stake_pool;
mod token;
pub mod token_2022;
/// Current stable Associated Token Account decoded event contract version.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
/// Exact Associated Token Account instructions published by the official interface.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
/// Maximum retained account-key text length for the Associated Token Account decoder.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
/// Maximum retained instruction account count for the Associated Token Account decoder.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
/// Maximum retained semantic diagnostic count for the Associated Token Account decoder.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
/// Maximum retained Associated Token Account instruction payload size.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
/// Maximum retained transaction account-key count for the Associated Token Account decoder.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
/// Stable protocol and surface code for the Associated Token Account program.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
/// Canonical tracing target for the Associated Token Account decoder.
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
/// Decodes one bounded Associated Token Account instruction.
pub(crate) use self::associated_token_account::spl_associated_token_account_decode;
/// Returns the exact entry matching an Associated Token Account wire.
pub(crate) use self::associated_token_account::spl_associated_token_account_entry;
/// Reads one bounded Associated Token Account payload.
pub(crate) use self::associated_token_account::spl_associated_token_account_payload;
/// Maximum payload prefix retained for bounded diagnostics.
pub(crate) use self::memo::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
@@ -54,6 +78,8 @@ pub(crate) use self::token::spl_token_entry_for_tag;
/// Returns the first byte of one retained classic SPL Token payload.
pub(crate) use self::token::spl_token_payload_tag;
/// Exact decoder for the SPL Associated Token Account program.
pub use self::associated_token_account::SplAssociatedTokenAccountDecoder;
/// Exact decoder for the three registered SPL Memo generations.
pub use self::memo::SplMemoDecoder;
/// Exact decoder for the classic SPL Token program.

View File

@@ -1,10 +1,36 @@
// file: kb-lib/src/decoder/spl/associated_token_account.rs
// version: 1
// version: 2
//! Migration boundary for legacy crate `kb_decoder_spl_associated_token_account`.
//! Exact SPL Associated Token Account decoder component.
/// Legacy crate name retained for migration and compatibility tracking.
pub const LEGACY_CRATE: &str = "kb_decoder_spl_associated_token_account";
mod constants;
mod decoder;
mod wire;
/// Current porting status.
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
/// Current stable decoded event contract version.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
/// Exact instructions published by the official interface.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
/// Maximum retained account-key text length.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
/// Maximum retained instruction account count.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
/// Maximum retained semantic diagnostic count.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
/// Maximum retained instruction payload size.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
/// Maximum retained transaction account-key count.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
/// Stable protocol and surface code for the Associated Token Account program.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
/// Canonical tracing target for the Associated Token Account decoder.
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
/// Decodes one bounded Associated Token Account instruction.
pub(crate) use self::wire::decode as spl_associated_token_account_decode;
/// Returns the exact entry matching an Associated Token Account wire.
pub(crate) use self::wire::entry as spl_associated_token_account_entry;
/// Reads one bounded Associated Token Account payload.
pub(crate) use self::wire::payload as spl_associated_token_account_payload;
/// Exact decoder for the SPL Associated Token Account program.
pub use self::decoder::SplAssociatedTokenAccountDecoder;

View File

@@ -0,0 +1,28 @@
// file: kb-lib/src/decoder/spl/associated_token_account/constants.rs
// version: 1
//! Local constants for the `kb-lib` SPL Associated Token Account component.
/// Stable protocol and surface code for the Associated Token Account program.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE: &str = "spl_associated_token_account";
/// Current stable decoded event contract version.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION: u32 = 1;
/// Maximum retained instruction payload size.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES: usize = 64;
/// Maximum retained instruction account count.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS: usize = 64;
/// Maximum retained transaction account-key count.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS: usize = 4_096;
/// Maximum retained account-key text length.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES: usize = 128;
/// Maximum retained semantic diagnostic count.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS: usize = 32;
/// Exact instructions published by `spl-associated-token-account-interface` 2.0.0.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[
(0, "create", false),
(1, "create_idempotent", false),
(2, "recover_nested", false),
];
/// Canonical tracing target for the Associated Token Account decoder.
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET: &str =
"kb-lib.decoder.spl.associated_token_account";

View File

@@ -0,0 +1,600 @@
// file: kb-lib/src/decoder/spl/associated_token_account/decoder.rs
// version: 1
//! Exact SPL Associated Token Account dispatch for the common decode pipeline.
const SURFACES: &[crate::DecoderSurface] = &[crate::DecoderSurface {
program_id: kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID,
surface_code: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_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 crate::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: &crate::ProgramObservation,
) -> crate::DecoderSupport {
return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
crate::DecoderSupport::Yes
} else {
crate::DecoderSupport::No
};
}
fn decode_observation(
&self,
_observation: &crate::ProgramObservation,
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl crate::InstructionDecoder for crate::SplAssociatedTokenAccountDecoder {
fn identity(&self) -> crate::DecoderIdentity {
return crate::DecoderIdentity {
name: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
return SURFACES;
}
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
return crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES
.iter()
.map(|(tag, code, historical)| {
return crate::DecoderCoverageDeclaration {
program_id: kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
surface_code: std::option::Option::Some(
crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(),
),
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
entry_code: (*code).to_string(),
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
historical: *historical,
};
})
.collect();
}
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
if input.program_id != kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID {
return crate::DecoderRecognition::incompatible();
}
let wire = crate::spl_associated_token_account_payload(input).ok();
let entry = wire
.as_ref()
.and_then(|bytes| return crate::spl_associated_token_account_entry(bytes));
return crate::DecoderRecognition::compatible(
entry.is_some(),
100,
std::option::Option::Some(crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_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: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
if input.program_id != kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID {
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
}
let result = crate::spl_associated_token_account_decode(input);
if matches!(
result.status,
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
) {
tracing::error!(
target: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET,
action = "decode_failure",
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
processor_name = crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_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::SPL_ASSOCIATED_TOKEN_ACCOUNT_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::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES
.iter()
.map(|(tag, code, _)| return (std::option::Option::Some(*tag), (*code).to_string()))
.collect::<std::vec::Vec<_>>();
let declared =
crate::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,
) -> crate::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 = crate::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!(
crate::InstructionDecoder::coverage(&crate::SplAssociatedTokenAccountDecoder).len(),
3
);
let observation = crate::ProgramObservation {
signature: crate::Signature("signature".to_string()),
slot: crate::Slot(1),
instruction_path: crate::InstructionPath("0".to_string()),
program_id: crate::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!(
crate::ProtocolDecoder::supports_observation(
&crate::SplAssociatedTokenAccountDecoder,
&observation,
),
crate::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 =
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
assert_eq!(result.status, crate::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 =
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
assert!(!result.observations[0].observation_committed);
assert_eq!(
result.observations[0].event.source_kind,
crate::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 =
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
assert_eq!(result.status, crate::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 =
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
assert_eq!(result.status, crate::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 =
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
assert_eq!(result.status, crate::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 =
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
assert_eq!(result.status, crate::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,465 @@
// 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();
}

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/lib.rs
// version: 8
// version: 9
//! Consolidated decoder, executor, materializer and shared model library.
#![warn(missing_docs)]
@@ -11,6 +11,24 @@ pub mod executor;
pub mod materializer;
pub mod model;
/// Current stable Associated Token Account decoded event contract version.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
/// Exact Associated Token Account instructions published by the official interface.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
/// Maximum retained account-key text length for the Associated Token Account decoder.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
/// Maximum retained instruction account count for the Associated Token Account decoder.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
/// Maximum retained semantic diagnostic count for the Associated Token Account decoder.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
/// Maximum retained Associated Token Account instruction payload size.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
/// Maximum retained transaction account-key count for the Associated Token Account decoder.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
/// Stable protocol and surface code for the Associated Token Account program.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
/// Canonical tracing target for the Associated Token Account decoder.
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
/// Maximum payload prefix retained for bounded Memo diagnostics.
pub(crate) use crate::decoder::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
@@ -93,6 +111,12 @@ pub(crate) use crate::decoder::solana::solana_core_config_decode;
pub(crate) use crate::decoder::solana::solana_core_config_recognize;
/// Resolves positional instruction accounts and validates their core indexes.
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
/// Decodes one bounded Associated Token Account instruction.
pub(crate) use crate::decoder::spl_associated_token_account_decode;
/// Returns the exact entry matching an Associated Token Account wire.
pub(crate) use crate::decoder::spl_associated_token_account_entry;
/// Reads one bounded Associated Token Account payload.
pub(crate) use crate::decoder::spl_associated_token_account_payload;
/// Decodes one bounded SPL Memo instruction.
pub(crate) use crate::decoder::spl_memo_decode;
/// Decodes one bounded classic SPL Token instruction.
@@ -224,6 +248,8 @@ pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_decode;
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
/// Exact decoder for the SPL Associated Token Account program.
pub use crate::decoder::SplAssociatedTokenAccountDecoder;
/// Exact decoder for the three registered SPL Memo generations.
pub use crate::decoder::SplMemoDecoder;
/// Exact decoder for the classic SPL Token program.