838 lines
36 KiB
Rust
838 lines
36 KiB
Rust
// file: kb-lib/src/materializer/token/accounts.rs
|
|
// version: 3
|
|
|
|
//! Stable committed SPL Token mutation and ATA lifecycle projections.
|
|
|
|
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[
|
|
crate::MdEventFamily::TokenAccount,
|
|
crate::MdEventFamily::TokenMint,
|
|
crate::MdEventFamily::TokenBurn,
|
|
crate::MdEventFamily::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 MtTokenAccountsMaterializer;
|
|
|
|
impl crate::MtMaterializer for crate::MtTokenAccountsMaterializer {
|
|
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: &crate::MdDecodedProtocolEvent) -> bool {
|
|
return accepts_event(event);
|
|
}
|
|
|
|
fn materialize_event(
|
|
&self,
|
|
_event: &crate::MdDecodedProtocolEvent,
|
|
) -> kb_core::Result<std::vec::Vec<crate::MdMaterializedEvent>> {
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
}
|
|
}
|
|
|
|
impl crate::MtApiEventMaterializer for crate::MtTokenAccountsMaterializer {
|
|
fn identity(&self) -> crate::MtApiMaterializerIdentity {
|
|
return crate::MtApiMaterializerIdentity {
|
|
name: crate::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME.to_string(),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
};
|
|
}
|
|
|
|
fn accepted_families(&self) -> &'static [crate::MdEventFamily] {
|
|
return ACCEPTED_FAMILIES;
|
|
}
|
|
|
|
fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> bool {
|
|
return accepts_event(&observation.event);
|
|
}
|
|
|
|
fn transaction_policy(
|
|
&self,
|
|
_family: crate::MdEventFamily,
|
|
) -> crate::MtApiMaterializationTransactionPolicy {
|
|
return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
}
|
|
|
|
fn materialize(
|
|
&self,
|
|
observation: &crate::DcApiDecodedObservation,
|
|
) -> crate::MtApiMaterializerExecutionResult {
|
|
if !accepts_event(&observation.event) {
|
|
return crate::MtApiMaterializerExecutionResult::ignored();
|
|
}
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
return crate::MtApiMaterializerExecutionResult::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 = crate::MtApiMaterializedOutput {
|
|
output_key: format!("token_account_mutation:{operation}:0"),
|
|
family: crate::MdMaterializedEventFamily::TokenAccount,
|
|
payload_json: serde_json::json!({
|
|
"projectionVersion": crate::MT_TOKEN_ACCOUNTS_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::MT_TOKEN_ACCOUNTS_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::MT_TOKEN_ACCOUNTS_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 crate::MtApiMaterializerExecutionResult {
|
|
status: crate::MtApiMaterializerOutcomeStatus::Inserted,
|
|
outputs: std::vec![output],
|
|
diagnostics: std::vec::Vec::new(),
|
|
};
|
|
}
|
|
}
|
|
|
|
fn accepts_event(event: &crate::MdDecodedProtocolEvent) -> 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_TOKEN2022_PROGRAM_ID
|
|
&& event.surface_code.0 == "spl_token2022");
|
|
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: &crate::MdDecodedProtocolEvent) -> bool {
|
|
return event.program_id.0 == kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
|
|
&& event.surface_code.0 == "spl_associated_token_account"
|
|
&& event.event_family == crate::MdEventFamily::Lifecycle
|
|
&& ATA_LIFECYCLE_ENTRIES
|
|
.iter()
|
|
.any(|(entry, _)| return *entry == event.event_name.0);
|
|
}
|
|
|
|
fn materialize_ata(
|
|
observation: &crate::DcApiDecodedObservation,
|
|
) -> crate::MtApiMaterializerExecutionResult {
|
|
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 crate::MtApiMaterializerExecutionResult::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 crate::MtApiMaterializerExecutionResult::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 crate::MtApiMaterializerExecutionResult::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 = crate::MtApiMaterializedOutput {
|
|
output_key: format!("ata_lifecycle:{lifecycle_kind}:0"),
|
|
family: crate::MdMaterializedEventFamily::TokenAccount,
|
|
payload_json: serde_json::json!({
|
|
"projectionVersion": crate::MT_TOKEN_ACCOUNTS_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::MT_TOKEN_ACCOUNTS_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::MT_TOKEN_ACCOUNTS_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 crate::MtApiMaterializerExecutionResult {
|
|
status: crate::MtApiMaterializerOutcomeStatus::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_TOKEN2022_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 materializer_token_materialize_token2022_state_snapshot(
|
|
account_key: &str,
|
|
slot: u64,
|
|
state: &crate::DcToken2022State,
|
|
) -> std::result::Result<crate::MtApiMaterializedOutput, 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 {
|
|
crate::DcToken2022StateKind::Mint => "mint",
|
|
crate::DcToken2022StateKind::Account => "account",
|
|
crate::DcToken2022StateKind::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(crate::MtApiMaterializedOutput {
|
|
output_key: format!("token2022_state_snapshot:{account_key}:{slot}"),
|
|
family: crate::MdMaterializedEventFamily::TokenAccount,
|
|
payload_json: serde_json::json!({
|
|
"projectionVersion": crate::MT_TOKEN_ACCOUNTS_PROJECTION_VERSION,
|
|
"domain": "spl_token2022_account_state",
|
|
"projectionSemantics": "authoritative_bounded_account_snapshot",
|
|
"idempotenceKey": format!("token2022_state:{account_key}:{slot}"),
|
|
"accountKey": account_key,
|
|
"slot": slot,
|
|
"programId": kb_program_ids::SPL_TOKEN2022_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::MT_TOKEN_ACCOUNTS_PROCESSOR_NAME,
|
|
"processorVersion": env!("CARGO_PKG_VERSION"),
|
|
"source": "bounded_token2022_state_parser"
|
|
}
|
|
}),
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn observation(
|
|
entry: &str,
|
|
family: crate::MdEventFamily,
|
|
failed: bool,
|
|
) -> crate::DcApiDecodedObservation {
|
|
return crate::DcApiDecodedObservation {
|
|
event_key: format!("token:{entry}:0"),
|
|
event: crate::MdDecodedProtocolEvent {
|
|
signature: crate::MdSignature("signature".to_string()),
|
|
slot: crate::MdSlot(42),
|
|
instruction_path: crate::MdInstructionPath("1/0".to_string()),
|
|
program_id: crate::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
|
protocol_code: crate::MdProtocolCode("spl_token".to_string()),
|
|
surface_code: crate::MdSurfaceCode("spl_token".to_string()),
|
|
event_code: crate::MdEventCode(format!("spl_token.{entry}")),
|
|
event_name: crate::MdEventName(entry.to_string()),
|
|
event_family: family,
|
|
source_kind: crate::MdEventSourceKind::InnerInstruction,
|
|
confidence: crate::MdDecoderConfidence::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: crate::DcApiDecoderProof {
|
|
kind: crate::DcApiDecoderProofKind::Manual,
|
|
confidence: crate::MdDecoderConfidence::ManualExact,
|
|
evidence: std::vec!["fixture".to_string()],
|
|
},
|
|
};
|
|
}
|
|
|
|
fn token2022_observation(
|
|
entry: &str,
|
|
family: crate::MdEventFamily,
|
|
failed: bool,
|
|
) -> crate::DcApiDecodedObservation {
|
|
let mut observation = observation(entry, family, failed);
|
|
observation.event.program_id =
|
|
crate::MdProgramId(kb_program_ids::SPL_TOKEN2022_PROGRAM_ID.to_string());
|
|
observation.event.protocol_code = crate::MdProtocolCode("spl_token2022".to_string());
|
|
observation.event.surface_code = crate::MdSurfaceCode("spl_token2022".to_string());
|
|
observation.event.event_code = crate::MdEventCode(format!("spl_token2022.{entry}"));
|
|
observation.event_key = format!("token2022:{entry}:0");
|
|
return observation;
|
|
}
|
|
|
|
fn ata_observation(entry: &str, failed: bool, valid: bool) -> crate::DcApiDecodedObservation {
|
|
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_TOKEN2022_PROGRAM_ID,
|
|
"kind": "token2022",
|
|
"supported": true
|
|
},
|
|
"associatedTokenAccount": {"expected":"ata111","observed":"ata111","valid":valid},
|
|
"fundingAccount": "payer111",
|
|
"walletOwner": "wallet111",
|
|
"mint": "mint111"
|
|
})
|
|
};
|
|
return crate::DcApiDecodedObservation {
|
|
event_key: format!("ata:{entry}:0"),
|
|
event: crate::MdDecodedProtocolEvent {
|
|
signature: crate::MdSignature("signature".to_string()),
|
|
slot: crate::MdSlot(42),
|
|
instruction_path: crate::MdInstructionPath("0".to_string()),
|
|
program_id: crate::MdProgramId(
|
|
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
|
|
),
|
|
protocol_code: crate::MdProtocolCode("spl_associated_token_account".to_string()),
|
|
surface_code: crate::MdSurfaceCode("spl_associated_token_account".to_string()),
|
|
event_code: crate::MdEventCode(format!("spl_associated_token_account.{entry}")),
|
|
event_name: crate::MdEventName(entry.to_string()),
|
|
event_family: crate::MdEventFamily::Lifecycle,
|
|
source_kind: crate::MdEventSourceKind::Instruction,
|
|
confidence: crate::MdDecoderConfidence::ManualExact,
|
|
},
|
|
payload_json: serde_json::json!({"addresses":addresses}),
|
|
transaction_failed: failed,
|
|
transaction_error: std::option::Option::None,
|
|
observation_committed: !failed,
|
|
proof: crate::DcApiDecoderProof {
|
|
kind: crate::DcApiDecoderProofKind::Manual,
|
|
confidence: crate::MdDecoderConfidence::ManualExact,
|
|
evidence: std::vec!["fixture".to_string()],
|
|
},
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn committed_checked_transfer_preserves_exact_amount_mint_and_path() {
|
|
let observation =
|
|
observation("transfer_checked", crate::MdEventFamily::TokenAccount, false);
|
|
let result = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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_token2022_transfer_has_the_same_single_projection_owner() {
|
|
let observation =
|
|
token2022_observation("transfer_checked", crate::MdEventFamily::TokenAccount, false);
|
|
let result = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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_TOKEN2022_PROGRAM_ID
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn failed_mutation_is_refused() {
|
|
let observation = observation("burn", crate::MdEventFamily::TokenBurn, true);
|
|
let result = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
|
assert!(result.outputs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn batch_parent_admin_and_conversion_events_are_not_duplicated() {
|
|
for (entry, family) in [
|
|
("batch", crate::MdEventFamily::Audit),
|
|
("set_authority", crate::MdEventFamily::Admin),
|
|
("amount_to_ui_amount", crate::MdEventFamily::Audit),
|
|
] {
|
|
let observation = observation(entry, family, false);
|
|
let result = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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_TOKEN2022_PROGRAM_ID),
|
|
(
|
|
"create_idempotent",
|
|
"ata_created_or_reused_idempotently",
|
|
"ata111",
|
|
kb_program_ids::SPL_TOKEN2022_PROGRAM_ID,
|
|
),
|
|
(
|
|
"recover_nested",
|
|
"nested_ata_recovered",
|
|
"nestedAta",
|
|
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
|
),
|
|
] {
|
|
let result = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&ata_observation(entry, false, true),
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::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 = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&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 = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation,
|
|
);
|
|
assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused);
|
|
assert!(result.outputs.is_empty());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn ata_parent_and_classic_token_cpi_children_own_distinct_facts() {
|
|
let parent = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&ata_observation("recover_nested", false, true),
|
|
);
|
|
let transfer = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation("transfer", crate::MdEventFamily::TokenAccount, false),
|
|
);
|
|
let close = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation("close_account", crate::MdEventFamily::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 = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&observation,
|
|
);
|
|
let second = crate::MtApiEventMaterializer::materialize(
|
|
&crate::MtTokenAccountsMaterializer,
|
|
&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 token2022_state_snapshot_preserves_base_and_ordered_extensions() {
|
|
let state = crate::DcToken2022State {
|
|
kind: crate::DcToken2022StateKind::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![
|
|
crate::DcToken2022TlvEntry {
|
|
extension_type: 1,
|
|
extension_name: "transfer_fee_config",
|
|
value_hex: "11".repeat(108),
|
|
value_fields: serde_json::json!({"withheldAmount":"7"}),
|
|
},
|
|
crate::DcToken2022TlvEntry {
|
|
extension_type: 4,
|
|
extension_name: "confidential_transfer_mint",
|
|
value_hex: "22".repeat(65),
|
|
value_fields: serde_json::json!({"autoApproveNewAccounts":true}),
|
|
},
|
|
],
|
|
};
|
|
let output = match crate::materializer_token_materialize_token2022_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 token2022_state_snapshot_is_idempotent_and_rejects_missing_identity() {
|
|
let state = crate::DcToken2022State {
|
|
kind: crate::DcToken2022StateKind::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::materializer_token_materialize_token2022_state_snapshot("account111", 7, &state);
|
|
let second =
|
|
crate::materializer_token_materialize_token2022_state_snapshot("account111", 7, &state);
|
|
assert_eq!(first, second);
|
|
assert!(
|
|
crate::materializer_token_materialize_token2022_state_snapshot("", 7, &state).is_err()
|
|
);
|
|
}
|
|
}
|