v0.1.0-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# file: kb-lib/Cargo.toml
|
||||
# version: 5
|
||||
# version: 6
|
||||
|
||||
[package]
|
||||
name = "kb-lib"
|
||||
@@ -25,6 +25,7 @@ solana-stake-interface.workspace = true
|
||||
solana-system-interface.workspace = true
|
||||
solana-vote-interface.workspace = true
|
||||
solana-zk-elgamal-proof-interface.workspace = true
|
||||
spl-associated-token-account-interface.workspace = true
|
||||
tracing.workspace = true
|
||||
ts-rs.workspace = true
|
||||
wincode.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/README.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# kb-lib
|
||||
|
||||
@@ -43,11 +43,25 @@ cumulés et interdit les batches imbriqués. Le décodeur n’invente ni mint, n
|
||||
final ; les validations `M/N`, soldes et autorités existantes restent stateful. La matrice
|
||||
normative est `docs/SPL_TOKEN_MATRIX.json`.
|
||||
|
||||
## Décodeur SPL Associated Token Account
|
||||
|
||||
`SplAssociatedTokenAccountDecoder` couvre exclusivement
|
||||
`ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`. Il reconnaît les trois variantes publiées
|
||||
`Create`, `CreateIdempotent` et `RecoverNested`, ainsi que l’encodage historique vide de `Create`.
|
||||
|
||||
Le décodeur conserve les comptes dans leur ordre original, leurs flags, doublons, chemins
|
||||
outer/inner et le statut de transaction. Il dérive les PDA avec l’ordre canonique
|
||||
`[wallet, token_program, mint]`, conserve simultanément l’adresse observée et l’adresse attendue,
|
||||
puis expose tout écart comme diagnostic. SPL Token classique et Token‑2022 sont distingués par leur
|
||||
Program ID sans reconstruire leur état ni interpréter leurs extensions. La matrice normative est
|
||||
`docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json`.
|
||||
|
||||
## API publique utile
|
||||
|
||||
- `SolanaCoreDecoder` : décodeur concret natif ;
|
||||
- `SplMemoDecoder` : décodeur exact des trois générations SPL Memo ;
|
||||
- `SplTokenDecoder` : décodeur exact du programme SPL Token classique ;
|
||||
- `SplAssociatedTokenAccountDecoder` : décodeur exact du programme Associated Token Account ;
|
||||
- `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ;
|
||||
- `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ;
|
||||
- `CoreInstructionReplayInput` : input source-neutral produit par l’extraction core ;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
28
kb-lib/src/decoder/spl/associated_token_account/constants.rs
Normal file
28
kb-lib/src/decoder/spl/associated_token_account/constants.rs
Normal 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";
|
||||
600
kb-lib/src/decoder/spl/associated_token_account/decoder.rs
Normal file
600
kb-lib/src/decoder/spl/associated_token_account/decoder.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
465
kb-lib/src/decoder/spl/associated_token_account/wire.rs
Normal file
465
kb-lib/src/decoder/spl/associated_token_account/wire.rs
Normal 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();
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user