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

View File

@@ -0,0 +1,22 @@
# file: kb_materializer_token_accounts/Cargo.toml
# version: 3
[package]
name = "kb_materializer_token_accounts"
version.workspace = true
edition.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
kb_core = { path = "../kb_core" }
kb_decoder_api = { path = "../kb_decoder_api" }
kb_decoder_spl_token_2022 = { path = "../kb_decoder_spl_token_2022" }
kb_materializer_api = { path = "../kb_materializer_api" }
kb_model = { path = "../kb_model" }
kb_program_ids = { path = "../kb_program_ids" }
serde_json.workspace = true
tracing.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,29 @@
<!-- file: kb_materializer_token_accounts/README.md -->
<!-- version: 4 -->
# kb_materializer_token_accounts
Ce crate matérialise les mutations instructionnelles commitées du programme SPL Token classique et le lifecycle d'adresses associées ATA dans le journal générique `kb_sol_mat_events`.
## Projections actives
- initialisations de mint et de compte ;
- transferts, approvals/revocations, mint et burn ;
- freeze/thaw et close ;
- `SyncNative`, `InitializeImmutableOwner`, `WithdrawExcessLamports` et `UnwrapLamports`.
- `ata_created` pour `Create` ;
- `ata_created_or_reused_idempotently` pour `CreateIdempotent`, sans inventer laquelle des deux branches s'est produite ;
- `nested_ata_recovered` pour `RecoverNested`, avec wallet, mints, trois ATA et Token Program classique ou Token-2022.
Chaque sortie conserve signature, slot, chemin, comptes, autorité, mint lorsqu'il est explicite, montant brut en chaîne et decimals présents dans le wire. Le matérialiseur utilise `SuccessfulCommittedOnly` et refuse toute transaction échouée.
## Limites et déduplication
La projection décrit une mutation d'instruction, jamais le snapshot final du compte. Elle ne recopie pas les changements de balances core. Le parent ATA possède uniquement le lifecycle de l'adresse associée ; les CPI SPL Token conservent la propriété exclusive des initialisations, transferts et fermetures de comptes. Le parent `Batch`, les conversions amount/UI amount, `set_authority` et la configuration multisig sont laissés respectivement aux enfants, au decode et au matérialiseur admin.
## Corpus validé
Le replay Mainnet du 15 juillet 2026 a produit 362 sorties `token_account` et neuf constats `risk`
depuis 370 instructions décodées. Les huit refus de matérialisation correspondent exactement à des
transactions échouées ; aucune sortie non commitée, aucun parent `Batch` dupliqué et aucun faux frais
classique n'ont été persistés. Un second replay sans force a sélectionné zéro entrée.

View File

@@ -0,0 +1,11 @@
// file: kb_materializer_token_accounts/src/constants.rs
// version: 3
//! Local constants for classic SPL Token account projections.
/// Canonical tracing target for this crate.
pub(crate) const TRACING_TARGET: &str = "kb_materializer_token_accounts";
/// Stable processor name persisted by the replay ledger.
pub(crate) const PROCESSOR_NAME: &str = "spl_token_accounts";
/// Stable projection payload contract version.
pub(crate) const PROJECTION_VERSION: u32 = 3;

View File

@@ -0,0 +1,22 @@
// file: kb_materializer_token_accounts/src/lib.rs
// version: 8
//! Materializer crate for classic SPL Token mutations and ATA lifecycle facts.
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
mod constants;
mod materializer;
/// Stable processor name persisted by the replay ledger.
pub(crate) use crate::constants::PROCESSOR_NAME;
/// Stable projection payload contract version.
pub(crate) use crate::constants::PROJECTION_VERSION;
/// Canonical tracing target for this crate.
pub(crate) use crate::constants::TRACING_TARGET;
/// Stable committed classic SPL Token mutation and ATA lifecycle materializer.
pub use crate::materializer::TokenAccountsMaterializer;
/// Materialize one bounded Token-2022 Mint, Account, or Multisig state snapshot.
pub use crate::materializer::materialize_token_2022_state_snapshot;

View File

@@ -0,0 +1,835 @@
// file: kb_materializer_token_accounts/src/materializer.rs
// version: 9
//! Stable committed SPL Token mutation and ATA lifecycle projections.
const ACCEPTED_FAMILIES: &[kb_model::EventFamily] = &[
kb_model::EventFamily::TokenAccount,
kb_model::EventFamily::TokenMint,
kb_model::EventFamily::TokenBurn,
kb_model::EventFamily::Lifecycle,
];
const MUTATION_ENTRIES: &[&str] = &[
"initialize_mint",
"initialize_mint2",
"initialize_account",
"initialize_account2",
"initialize_account3",
"transfer",
"transfer_checked",
"approve",
"approve_checked",
"revoke",
"mint_to",
"mint_to_checked",
"burn",
"burn_checked",
"close_account",
"freeze_account",
"thaw_account",
"sync_native",
"initialize_immutable_owner",
"withdraw_excess_lamports",
"unwrap_lamports",
"initialize_non_transferable_mint",
"reallocate",
];
const ATA_LIFECYCLE_ENTRIES: &[(&str, &str)] = &[
("create", "ata_created"),
("create_idempotent", "ata_created_or_reused_idempotently"),
("recover_nested", "nested_ata_recovered"),
];
/// Stable committed classic SPL Token account and mint mutation materializer.
#[derive(Clone, Debug, Default)]
pub struct TokenAccountsMaterializer;
impl kb_materializer_api::Materializer for crate::TokenAccountsMaterializer {
fn materializer_name(&self) -> &'static str {
return "kb_materializer_token_accounts";
}
fn materializer_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn accepts_event(&self, event: &kb_model::DecodedProtocolEvent) -> bool {
return accepts_event(event);
}
fn materialize_event(
&self,
_event: &kb_model::DecodedProtocolEvent,
) -> kb_core::Result<std::vec::Vec<kb_model::MaterializedEvent>> {
return std::result::Result::Ok(std::vec::Vec::new());
}
}
impl kb_materializer_api::EventMaterializer for crate::TokenAccountsMaterializer {
fn identity(&self) -> kb_materializer_api::MaterializerIdentity {
return kb_materializer_api::MaterializerIdentity {
name: crate::PROCESSOR_NAME.to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn accepted_families(&self) -> &'static [kb_model::EventFamily] {
return ACCEPTED_FAMILIES;
}
fn accepts_observation(&self, observation: &kb_decoder_api::DecodedObservation) -> bool {
return accepts_event(&observation.event);
}
fn transaction_policy(
&self,
_family: kb_model::EventFamily,
) -> kb_materializer_api::MaterializationTransactionPolicy {
return kb_materializer_api::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
}
fn materialize(
&self,
observation: &kb_decoder_api::DecodedObservation,
) -> kb_materializer_api::MaterializerExecutionResult {
if !accepts_event(&observation.event) {
return kb_materializer_api::MaterializerExecutionResult::ignored();
}
if observation.transaction_failed || !observation.observation_committed {
return kb_materializer_api::MaterializerExecutionResult::refused(
"uncommitted_token_account_projection_refused",
"failed or uncommitted SPL Token or ATA observations cannot create mutable outputs",
);
}
if is_ata_event(&observation.event) {
return materialize_ata(observation);
}
let parameters = observation
.payload_json
.get("parameters")
.cloned()
.unwrap_or(serde_json::Value::Null);
let accounts = observation
.payload_json
.get("accounts")
.cloned()
.unwrap_or(serde_json::Value::Null);
let authority = observation
.payload_json
.get("authority")
.cloned()
.unwrap_or(serde_json::Value::Null);
let operation = observation.event.event_name.0.clone();
let output = kb_materializer_api::MaterializedOutput {
output_key: format!("token_account_mutation:{operation}:0"),
family: kb_model::MaterializedEventFamily::TokenAccount,
payload_json: serde_json::json!({
"projectionVersion": crate::PROJECTION_VERSION,
"domain": "spl_token_account_mutation",
"projectionSemantics": "committed_instruction_mutation_not_final_account_snapshot",
"idempotenceKey": format!(
"spl_token:{}:{}:{}",
observation.event.signature.0,
observation.event.instruction_path.0,
operation
),
"operation": operation,
"signature": observation.event.signature.0.clone(),
"slot": observation.event.slot.0,
"instructionPath": observation.event.instruction_path.0.clone(),
"programId": observation.event.program_id.0.clone(),
"mint": account_key_for_role(&accounts, "mint"),
"amountRaw": parameters.get("amountRaw").cloned().unwrap_or(serde_json::Value::Null),
"decimals": parameters.get("decimals").cloned().unwrap_or(serde_json::Value::Null),
"parameters": parameters,
"accounts": accounts,
"authority": authority,
"committed": true,
"transactionFinalAccountStateCaptured": false,
"coreBalanceChangesDuplicated": false,
"provenance": {
"processorName": crate::PROCESSOR_NAME,
"processorVersion": env!("CARGO_PKG_VERSION"),
"sourceSurface": observation.event.surface_code.0.clone(),
"sourceEventCode": observation.event.event_code.0.clone(),
"sourceEventKey": observation.event_key.clone()
}
}),
};
tracing::debug!(
target: crate::TRACING_TARGET,
action = "materialize_token_account_mutation",
signature = %observation.event.signature.0,
instruction_path = %observation.event.instruction_path.0,
operation = %observation.event.event_name.0,
"materialize committed classic SPL Token mutation"
);
return kb_materializer_api::MaterializerExecutionResult {
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
}
fn accepts_event(event: &kb_model::DecodedProtocolEvent) -> bool {
let token_program = (event.program_id.0 == kb_program_ids::SPL_TOKEN_PROGRAM_ID
&& event.surface_code.0 == "spl_token")
|| (event.program_id.0 == kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
&& event.surface_code.0 == "spl_token_2022");
let token_mutation = token_program && MUTATION_ENTRIES.contains(&event.event_name.0.as_str());
let ata = is_ata_event(event);
return ACCEPTED_FAMILIES.contains(&event.event_family) && (token_mutation || ata);
}
fn is_ata_event(event: &kb_model::DecodedProtocolEvent) -> bool {
return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
&& event.surface_code.0 == "spl_associated_token_account"
&& event.event_family == kb_model::EventFamily::Lifecycle
&& ATA_LIFECYCLE_ENTRIES
.iter()
.any(|(entry, _)| return *entry == event.event_name.0);
}
fn materialize_ata(
observation: &kb_decoder_api::DecodedObservation,
) -> kb_materializer_api::MaterializerExecutionResult {
let operation = observation.event.event_name.0.as_str();
let addresses = match observation.payload_json.get("addresses") {
std::option::Option::Some(value) if value.is_object() => value,
_ => {
return kb_materializer_api::MaterializerExecutionResult::refused(
"ata_addresses_missing",
"committed ATA lifecycle projection requires the decoded address contract",
);
},
};
let validation = validate_ata_addresses(operation, addresses);
if let std::result::Result::Err((code, message)) = validation {
return kb_materializer_api::MaterializerExecutionResult::refused(code, message);
}
let lifecycle_kind =
match ATA_LIFECYCLE_ENTRIES.iter().find(|(entry, _)| return *entry == operation) {
std::option::Option::Some((_, value)) => *value,
std::option::Option::None => {
return kb_materializer_api::MaterializerExecutionResult::ignored();
},
};
let associated_token_account = if operation == "recover_nested" {
observed_address(addresses, "nestedAssociatedTokenAccount")
} else {
observed_address(addresses, "associatedTokenAccount")
};
let token_program = addresses
.get("tokenProgram")
.and_then(|value| return value.get("programId"))
.cloned()
.unwrap_or(serde_json::Value::Null);
let wallet = addresses.get("walletOwner").cloned().unwrap_or(serde_json::Value::Null);
let mint = if operation == "recover_nested" {
addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null)
} else {
addresses.get("mint").cloned().unwrap_or(serde_json::Value::Null)
};
let output = kb_materializer_api::MaterializedOutput {
output_key: format!("ata_lifecycle:{lifecycle_kind}:0"),
family: kb_model::MaterializedEventFamily::TokenAccount,
payload_json: serde_json::json!({
"projectionVersion": crate::PROJECTION_VERSION,
"domain": "spl_associated_token_account_lifecycle",
"projectionSemantics": "committed_associated_address_lifecycle_not_token_state_snapshot",
"idempotenceKey": format!(
"ata:{}:{}:{}:{}:{}",
observation.event.signature.0,
observation.event.instruction_path.0,
observation.event.program_id.0,
lifecycle_kind,
associated_token_account.as_str().unwrap_or("")
),
"operation": operation,
"lifecycleKind": lifecycle_kind,
"associatedTokenAccount": associated_token_account,
"walletOwner": wallet,
"mint": mint,
"tokenProgramId": token_program,
"fundingAccount": addresses.get("fundingAccount").cloned().unwrap_or(serde_json::Value::Null),
"ownerMint": addresses.get("ownerMint").cloned().unwrap_or(serde_json::Value::Null),
"nestedMint": addresses.get("nestedMint").cloned().unwrap_or(serde_json::Value::Null),
"ownerAssociatedTokenAccount": observed_address(addresses, "ownerAssociatedTokenAccount"),
"nestedAssociatedTokenAccount": observed_address(addresses, "nestedAssociatedTokenAccount"),
"walletNestedMintAssociatedTokenAccount": observed_address(addresses, "walletNestedMintAssociatedTokenAccount"),
"idempotentOutcome": if operation == "create_idempotent" {
"created_or_reused_not_distinguishable_from_parent_instruction_alone"
} else {
"not_applicable"
},
"signature": observation.event.signature.0.clone(),
"slot": observation.event.slot.0,
"instructionPath": observation.event.instruction_path.0.clone(),
"programId": observation.event.program_id.0.clone(),
"committed": true,
"transactionFinalAccountStateCaptured": false,
"tokenBalanceReconstructed": false,
"splTokenCpiMutationsDuplicated": false,
"ownership": {
"ataParent": "associated_address_lifecycle_only",
"splTokenCpi": "initialize_transfer_and_close_mutations"
},
"provenance": {
"processorName": crate::PROCESSOR_NAME,
"processorVersion": env!("CARGO_PKG_VERSION"),
"sourceSurface": observation.event.surface_code.0.clone(),
"sourceEventCode": observation.event.event_code.0.clone(),
"sourceEventKey": observation.event_key.clone()
}
}),
};
tracing::debug!(
target: crate::TRACING_TARGET,
action = "materialize_ata_lifecycle",
signature = %observation.event.signature.0,
instruction_path = %observation.event.instruction_path.0,
operation,
lifecycle_kind,
"materialize committed ATA lifecycle without duplicating SPL Token CPI mutations"
);
return kb_materializer_api::MaterializerExecutionResult {
status: kb_materializer_api::MaterializerOutcomeStatus::Inserted,
outputs: std::vec![output],
diagnostics: std::vec::Vec::new(),
};
}
fn validate_ata_addresses(
operation: &str,
addresses: &serde_json::Value,
) -> std::result::Result<(), (&'static str, &'static str)> {
let token_supported = addresses
.get("tokenProgram")
.and_then(|value| return value.get("supported"))
.and_then(serde_json::Value::as_bool)
== std::option::Option::Some(true);
let token_program_id = addresses
.get("tokenProgram")
.and_then(|value| return value.get("programId"))
.and_then(serde_json::Value::as_str);
let token_program_exact = matches!(
token_program_id,
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID)
| std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID)
);
if !token_supported || !token_program_exact {
return std::result::Result::Err((
"ata_token_program_not_supported",
"ATA lifecycle projection requires classic SPL Token or Token-2022",
));
}
let fields: &[&str] = if operation == "recover_nested" {
&[
"ownerAssociatedTokenAccount",
"nestedAssociatedTokenAccount",
"walletNestedMintAssociatedTokenAccount",
]
} else {
&["associatedTokenAccount"]
};
for field in fields {
let valid = addresses
.get(*field)
.and_then(|value| return value.get("valid"))
.and_then(serde_json::Value::as_bool)
== std::option::Option::Some(true);
let observed = observed_address(addresses, field);
if !valid || observed.as_str().is_none_or(|value| return value.is_empty()) {
return std::result::Result::Err((
"ata_derived_address_invalid",
"ATA lifecycle projection requires every canonical address validation to pass",
));
}
}
let identity_fields: &[&str] = if operation == "recover_nested" {
&["walletOwner", "ownerMint", "nestedMint"]
} else {
&["walletOwner", "mint"]
};
if identity_fields.iter().any(|field| {
return addresses
.get(*field)
.and_then(serde_json::Value::as_str)
.is_none_or(|value| return value.is_empty());
}) {
return std::result::Result::Err((
"ata_identity_field_missing",
"ATA lifecycle projection requires wallet and mint identities",
));
}
return std::result::Result::Ok(());
}
fn observed_address(addresses: &serde_json::Value, field: &str) -> serde_json::Value {
return addresses
.get(field)
.and_then(|value| return value.get("observed"))
.cloned()
.unwrap_or(serde_json::Value::Null);
}
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
let value = accounts.as_array().and_then(|rows| {
return rows.iter().find_map(|row| {
if row.get("role").and_then(serde_json::Value::as_str)
!= std::option::Option::Some(role)
{
return std::option::Option::None;
}
return row.get("accountKey").and_then(serde_json::Value::as_str);
});
});
return match value {
std::option::Option::Some(account_key) => {
serde_json::Value::String(account_key.to_string())
},
std::option::Option::None => serde_json::Value::Null,
};
}
/// Materialize one already parsed Token-2022 account snapshot without inferring hidden state.
pub fn materialize_token_2022_state_snapshot(
account_key: &str,
slot: u64,
state: &kb_decoder_spl_token_2022::state::Token2022State,
) -> std::result::Result<kb_materializer_api::MaterializedOutput, String> {
if account_key.is_empty() {
return std::result::Result::Err(
"Token-2022 state snapshot requires a non-empty account key".to_string(),
);
}
let kind = match state.kind {
kb_decoder_spl_token_2022::state::Token2022StateKind::Mint => "mint",
kb_decoder_spl_token_2022::state::Token2022StateKind::Account => "account",
kb_decoder_spl_token_2022::state::Token2022StateKind::Multisig => "multisig",
};
let extensions = state
.extensions
.iter()
.map(|entry| {
return serde_json::json!({
"extensionType": entry.extension_type,
"extensionName": entry.extension_name,
"valueHex": entry.value_hex,
"valueFields": entry.value_fields,
});
})
.collect::<std::vec::Vec<_>>();
let extension_names = state
.extensions
.iter()
.map(|entry| return entry.extension_name)
.collect::<std::vec::Vec<_>>();
let contains_confidential_state = state.extensions.iter().any(|entry| {
return entry.extension_name.starts_with("confidential_")
|| entry.extension_name == "confidential_mint_burn";
});
return std::result::Result::Ok(kb_materializer_api::MaterializedOutput {
output_key: format!("token_2022_state_snapshot:{account_key}:{slot}"),
family: kb_model::MaterializedEventFamily::TokenAccount,
payload_json: serde_json::json!({
"projectionVersion": crate::PROJECTION_VERSION,
"domain": "spl_token_2022_account_state",
"projectionSemantics": "authoritative_bounded_account_snapshot",
"idempotenceKey": format!("token_2022_state:{account_key}:{slot}"),
"accountKey": account_key,
"slot": slot,
"programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"stateKind": kind,
"baseFields": state.base_fields,
"baseHex": state.base_hex,
"accountType": state.account_type,
"extensionCount": state.extensions.len(),
"extensionNames": extension_names,
"extensions": extensions,
"containsConfidentialState": contains_confidential_state,
"confidentialValuesDecrypted": false,
"finalAccountStateCaptured": true,
"provenance": {
"processorName": crate::PROCESSOR_NAME,
"processorVersion": env!("CARGO_PKG_VERSION"),
"source": "bounded_token_2022_state_parser"
}
}),
});
}
#[cfg(test)]
mod tests {
fn observation(
entry: &str,
family: kb_model::EventFamily,
failed: bool,
) -> kb_decoder_api::DecodedObservation {
return kb_decoder_api::DecodedObservation {
event_key: format!("token:{entry}:0"),
event: kb_model::DecodedProtocolEvent {
signature: kb_model::Signature("signature".to_string()),
slot: kb_model::Slot(42),
instruction_path: kb_model::InstructionPath("1/0".to_string()),
program_id: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
protocol_code: kb_model::ProtocolCode("spl_token".to_string()),
surface_code: kb_model::SurfaceCode("spl_token".to_string()),
event_code: kb_model::EventCode(format!("spl_token.{entry}")),
event_name: kb_model::EventName(entry.to_string()),
event_family: family,
source_kind: kb_model::EventSourceKind::InnerInstruction,
confidence: kb_model::DecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({
"parameters": {"amountRaw":"18446744073709551615","decimals":9},
"accounts": [{"role":"mint","accountKey":"mint111"}],
"authority": {"form":"single"}
}),
transaction_failed: failed,
transaction_error: if failed {
std::option::Option::Some(serde_json::json!({"error":"failed"}))
} else {
std::option::Option::None
},
observation_committed: !failed,
proof: kb_decoder_api::DecoderProof {
kind: kb_decoder_api::DecoderProofKind::Manual,
confidence: kb_model::DecoderConfidence::ManualExact,
evidence: std::vec!["fixture".to_string()],
},
};
}
fn token_2022_observation(
entry: &str,
family: kb_model::EventFamily,
failed: bool,
) -> kb_decoder_api::DecodedObservation {
let mut observation = observation(entry, family, failed);
observation.event.program_id =
kb_model::ProgramId(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string());
observation.event.protocol_code = kb_model::ProtocolCode("spl_token_2022".to_string());
observation.event.surface_code = kb_model::SurfaceCode("spl_token_2022".to_string());
observation.event.event_code = kb_model::EventCode(format!("spl_token_2022.{entry}"));
observation.event_key = format!("token_2022:{entry}:0");
return observation;
}
fn ata_observation(
entry: &str,
failed: bool,
valid: bool,
) -> kb_decoder_api::DecodedObservation {
let addresses = if entry == "recover_nested" {
serde_json::json!({
"tokenProgram": {
"programId": kb_program_ids::SPL_TOKEN_PROGRAM_ID,
"kind": "spl_token_classic",
"supported": true
},
"ownerAssociatedTokenAccount": {"expected":"ownerAta","observed":"ownerAta","valid":valid},
"nestedAssociatedTokenAccount": {"expected":"nestedAta","observed":"nestedAta","valid":valid},
"walletNestedMintAssociatedTokenAccount": {"expected":"destinationAta","observed":"destinationAta","valid":valid},
"walletOwner": "wallet111",
"ownerMint": "ownerMint111",
"nestedMint": "nestedMint111"
})
} else {
serde_json::json!({
"tokenProgram": {
"programId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"kind": "token_2022",
"supported": true
},
"associatedTokenAccount": {"expected":"ata111","observed":"ata111","valid":valid},
"fundingAccount": "payer111",
"walletOwner": "wallet111",
"mint": "mint111"
})
};
return kb_decoder_api::DecodedObservation {
event_key: format!("ata:{entry}:0"),
event: kb_model::DecodedProtocolEvent {
signature: kb_model::Signature("signature".to_string()),
slot: kb_model::Slot(42),
instruction_path: kb_model::InstructionPath("0".to_string()),
program_id: kb_model::ProgramId(
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
),
protocol_code: kb_model::ProtocolCode("spl_associated_token_account".to_string()),
surface_code: kb_model::SurfaceCode("spl_associated_token_account".to_string()),
event_code: kb_model::EventCode(format!("spl_associated_token_account.{entry}")),
event_name: kb_model::EventName(entry.to_string()),
event_family: kb_model::EventFamily::Lifecycle,
source_kind: kb_model::EventSourceKind::Instruction,
confidence: kb_model::DecoderConfidence::ManualExact,
},
payload_json: serde_json::json!({"addresses":addresses}),
transaction_failed: failed,
transaction_error: std::option::Option::None,
observation_committed: !failed,
proof: kb_decoder_api::DecoderProof {
kind: kb_decoder_api::DecoderProofKind::Manual,
confidence: kb_model::DecoderConfidence::ManualExact,
evidence: std::vec!["fixture".to_string()],
},
};
}
#[test]
fn committed_checked_transfer_preserves_exact_amount_mint_and_path() {
let observation =
observation("transfer_checked", kb_model::EventFamily::TokenAccount, false);
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs[0].payload_json["amountRaw"], "18446744073709551615");
assert_eq!(result.outputs[0].payload_json["mint"], "mint111");
assert_eq!(result.outputs[0].payload_json["instructionPath"], "1/0");
assert_eq!(result.outputs[0].payload_json["transactionFinalAccountStateCaptured"], false);
}
#[test]
fn committed_token_2022_transfer_has_the_same_single_projection_owner() {
let observation =
token_2022_observation("transfer_checked", kb_model::EventFamily::TokenAccount, false);
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs.len(), 1);
assert_eq!(result.outputs[0].payload_json["operation"], "transfer_checked");
assert_eq!(
result.outputs[0].payload_json["programId"],
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID
);
}
#[test]
fn failed_mutation_is_refused() {
let observation = observation("burn", kb_model::EventFamily::TokenBurn, true);
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Refused);
assert!(result.outputs.is_empty());
}
#[test]
fn batch_parent_admin_and_conversion_events_are_not_duplicated() {
for (entry, family) in [
("batch", kb_model::EventFamily::Audit),
("set_authority", kb_model::EventFamily::Admin),
("amount_to_ui_amount", kb_model::EventFamily::Audit),
] {
let observation = observation(entry, family, false);
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Ignored);
}
}
#[test]
fn every_ata_variant_has_one_exact_lifecycle_projection() {
for (entry, expected_kind, expected_ata, expected_token_program) in [
("create", "ata_created", "ata111", kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID),
(
"create_idempotent",
"ata_created_or_reused_idempotently",
"ata111",
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
),
(
"recover_nested",
"nested_ata_recovered",
"nestedAta",
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
),
] {
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&ata_observation(entry, false, true),
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Inserted);
assert_eq!(result.outputs.len(), 1);
assert_eq!(result.outputs[0].payload_json["lifecycleKind"], expected_kind);
assert_eq!(result.outputs[0].payload_json["associatedTokenAccount"], expected_ata);
assert_eq!(result.outputs[0].payload_json["tokenProgramId"], expected_token_program);
assert_eq!(result.outputs[0].payload_json["tokenBalanceReconstructed"], false);
assert_eq!(result.outputs[0].payload_json["splTokenCpiMutationsDuplicated"], false);
}
}
#[test]
fn idempotent_parent_does_not_invent_created_or_reused_branch() {
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&ata_observation("create_idempotent", false, true),
);
assert_eq!(
result.outputs[0].payload_json["idempotentOutcome"],
"created_or_reused_not_distinguishable_from_parent_instruction_alone"
);
assert!(result.outputs[0].payload_json.get("reused").is_none());
assert!(result.outputs[0].payload_json.get("created").is_none());
}
#[test]
fn uncommitted_or_invalid_ata_lifecycle_is_refused() {
for observation in [
ata_observation("create", true, true),
ata_observation("recover_nested", false, false),
] {
let result = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
assert_eq!(result.status, kb_materializer_api::MaterializerOutcomeStatus::Refused);
assert!(result.outputs.is_empty());
}
}
#[test]
fn ata_parent_and_classic_token_cpi_children_own_distinct_facts() {
let parent = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&ata_observation("recover_nested", false, true),
);
let transfer = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation("transfer", kb_model::EventFamily::TokenAccount, false),
);
let close = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation("close_account", kb_model::EventFamily::TokenAccount, false),
);
assert_eq!(parent.outputs[0].payload_json["lifecycleKind"], "nested_ata_recovered");
assert_eq!(
parent.outputs[0].payload_json["ownership"]["ataParent"],
"associated_address_lifecycle_only"
);
assert!(parent.outputs[0].payload_json.get("amountRaw").is_none());
assert_eq!(transfer.outputs[0].payload_json["operation"], "transfer");
assert_eq!(close.outputs[0].payload_json["operation"], "close_account");
assert_eq!(transfer.outputs[0].payload_json["domain"], "spl_token_account_mutation");
assert_eq!(close.outputs[0].payload_json["domain"], "spl_token_account_mutation");
}
#[test]
fn ata_matrix_and_compiled_token_account_projection_ownership_are_equal() {
let matrix: serde_json::Value = 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!("ATA matrix is invalid: {error}"),
};
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("ATA matrix instructions are absent"),
};
for (entry, fact) in super::ATA_LIFECYCLE_ENTRIES {
let row = match instructions.iter().find(|row| {
return row.get("name").and_then(serde_json::Value::as_str)
== std::option::Option::Some(*entry);
}) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("ATA matrix entry {entry} is absent"),
};
let expected = format!("token_accounts:{fact}");
assert!(row["authorizedProjections"].as_array().is_some_and(|values| {
return values.iter().any(|value| return value == expected.as_str());
}));
}
let owner = &matrix["materializationContract"]["projectionOwners"][0];
let matrix_facts = match owner["facts"].as_array() {
std::option::Option::Some(values) => values
.iter()
.filter_map(|value| return value.as_str())
.collect::<std::vec::Vec<_>>(),
std::option::Option::None => panic!("ATA token-account owner facts are absent"),
};
let compiled_facts = super::ATA_LIFECYCLE_ENTRIES
.iter()
.map(|(_, fact)| return *fact)
.collect::<std::vec::Vec<_>>();
assert_eq!(matrix_facts, compiled_facts);
}
#[test]
fn second_ata_materialization_is_byte_for_byte_idempotent() {
let observation = ata_observation("create_idempotent", false, true);
let first = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
let second = kb_materializer_api::EventMaterializer::materialize(
&crate::TokenAccountsMaterializer,
&observation,
);
assert_eq!(first, second);
assert_eq!(first.outputs[0].output_key, second.outputs[0].output_key);
assert_eq!(
first.outputs[0].payload_json["idempotenceKey"],
second.outputs[0].payload_json["idempotenceKey"]
);
}
#[test]
fn token_2022_state_snapshot_preserves_base_and_ordered_extensions() {
let state = kb_decoder_spl_token_2022::state::Token2022State {
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Mint,
base_fields: serde_json::json!({"supply":"42","decimals":9,"initialized":true}),
base_hex: "00".repeat(82),
account_type: std::option::Option::Some(1),
extensions: std::vec![
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
extension_type: 1,
extension_name: "transfer_fee_config",
value_hex: "11".repeat(108),
value_fields: serde_json::json!({"withheldAmount":"7"}),
},
kb_decoder_spl_token_2022::state::Token2022TlvEntry {
extension_type: 4,
extension_name: "confidential_transfer_mint",
value_hex: "22".repeat(65),
value_fields: serde_json::json!({"autoApproveNewAccounts":true}),
},
],
};
let output = match crate::materialize_token_2022_state_snapshot("mint111", 99, &state) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("snapshot failed: {error}"),
};
assert_eq!(output.payload_json["stateKind"], "mint");
assert_eq!(output.payload_json["extensionCount"], 2);
assert_eq!(output.payload_json["extensionNames"][0], "transfer_fee_config");
assert_eq!(output.payload_json["extensionNames"][1], "confidential_transfer_mint");
assert_eq!(output.payload_json["containsConfidentialState"], true);
assert_eq!(output.payload_json["confidentialValuesDecrypted"], false);
assert_eq!(output.payload_json["baseFields"]["supply"], "42");
}
#[test]
fn token_2022_state_snapshot_is_idempotent_and_rejects_missing_identity() {
let state = kb_decoder_spl_token_2022::state::Token2022State {
kind: kb_decoder_spl_token_2022::state::Token2022StateKind::Account,
base_fields: serde_json::json!({"mint":"mint111","owner":"wallet111","amount":"5"}),
base_hex: "00".repeat(165),
account_type: std::option::Option::None,
extensions: std::vec::Vec::new(),
};
let first = crate::materialize_token_2022_state_snapshot("account111", 7, &state);
let second = crate::materialize_token_2022_state_snapshot("account111", 7, &state);
assert_eq!(first, second);
assert!(crate::materialize_token_2022_state_snapshot("", 7, &state).is_err());
}
}