v0.1.0-pre.011
This commit is contained in:
@@ -1,10 +1,831 @@
|
||||
// file: kb-lib/src/materializer/token/accounts.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_materializer_token_accounts`.
|
||||
//! Stable committed SPL Token mutation and ATA lifecycle projections.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_materializer_token_accounts";
|
||||
const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[
|
||||
crate::MdEventFamily::TokenAccount,
|
||||
crate::MdEventFamily::TokenMint,
|
||||
crate::MdEventFamily::TokenBurn,
|
||||
crate::MdEventFamily::Lifecycle,
|
||||
];
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
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 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::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::materialize_token2022_state_snapshot("account111", 7, &state);
|
||||
let second = crate::materialize_token2022_state_snapshot("account111", 7, &state);
|
||||
assert_eq!(first, second);
|
||||
assert!(crate::materialize_token2022_state_snapshot("", 7, &state).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user