1178 lines
46 KiB
Rust
1178 lines
46 KiB
Rust
// file: ks-pipeline/src/spl_ata_stateful.rs
|
|
// version: 5
|
|
|
|
//! Stateful Localnet and Devnet readiness checks for ATA operations.
|
|
|
|
const BASE_MINT_LEN: usize = 82;
|
|
const BASE_TOKEN_ACCOUNT_LEN: usize = 165;
|
|
const MAX_TOKEN_2022_ACCOUNT_BYTES: usize = 16_384;
|
|
|
|
macro_rules! result_value {
|
|
($expression:expr) => {
|
|
match $expression {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
};
|
|
}
|
|
|
|
/// ATA stateful readiness outcome.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum SplAssociatedTokenAccountStatefulReadinessStatus {
|
|
/// Every stateful check passed.
|
|
Ready,
|
|
/// At least one stateful check failed.
|
|
Blocked,
|
|
}
|
|
|
|
/// One machine-readable ATA stateful check.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SplAssociatedTokenAccountStatefulCheck {
|
|
/// Stable diagnostic code.
|
|
pub code: std::string::String,
|
|
/// Whether the check passed.
|
|
pub passed: bool,
|
|
/// Operator-readable explanation.
|
|
pub message: std::string::String,
|
|
}
|
|
|
|
/// One contextual ATA stateful fact.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SplAssociatedTokenAccountStatefulFact {
|
|
/// Stable fact key.
|
|
pub key: std::string::String,
|
|
/// Exact string representation.
|
|
pub value: std::string::String,
|
|
}
|
|
|
|
/// Complete request for one ATA stateful readiness inspection.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SplAssociatedTokenAccountStatefulReadinessRequest {
|
|
/// HTTP endpoint role used for every state read.
|
|
pub query_role: std::string::String,
|
|
/// Expected Localnet or Devnet cluster.
|
|
pub cluster: ks_lib::ExApiExecutionCluster,
|
|
/// Typed simulation-first ATA intent.
|
|
pub intent: ks_lib::ExSplAssociatedTokenAccountExecutionIntent,
|
|
/// Signer public keys currently available to the caller.
|
|
pub available_signers: std::vec::Vec<ks_lib::MdPubkey>,
|
|
}
|
|
|
|
/// Complete ATA stateful readiness report.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SplAssociatedTokenAccountStatefulReadinessReport {
|
|
/// Expected cluster.
|
|
pub cluster: ks_lib::ExApiExecutionCluster,
|
|
/// Stable operation code.
|
|
pub operation_code: std::string::String,
|
|
/// Aggregate readiness status.
|
|
pub status: SplAssociatedTokenAccountStatefulReadinessStatus,
|
|
/// Highest contextual slot observed.
|
|
pub context_slot: std::option::Option<u64>,
|
|
/// Ordered checks.
|
|
pub checks: std::vec::Vec<SplAssociatedTokenAccountStatefulCheck>,
|
|
/// Ordered facts.
|
|
pub facts: std::vec::Vec<SplAssociatedTokenAccountStatefulFact>,
|
|
}
|
|
|
|
/// Stateful ATA invariants observed after a confirmed execution.
|
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
|
pub struct SplAssociatedTokenAccountPostExecutionReport {
|
|
/// Expected Localnet or Devnet cluster.
|
|
pub cluster: ks_lib::ExApiExecutionCluster,
|
|
/// Stable operation code.
|
|
pub operation_code: std::string::String,
|
|
/// Aggregate postcondition status.
|
|
pub status: SplAssociatedTokenAccountStatefulReadinessStatus,
|
|
/// Highest contextual slot observed.
|
|
pub context_slot: std::option::Option<u64>,
|
|
/// Ordered state checks.
|
|
pub checks: std::vec::Vec<SplAssociatedTokenAccountStatefulCheck>,
|
|
/// Ordered derived-address facts.
|
|
pub facts: std::vec::Vec<SplAssociatedTokenAccountStatefulFact>,
|
|
}
|
|
|
|
type AccountCache = std::collections::BTreeMap<
|
|
std::string::String,
|
|
std::option::Option<ks_onchain_transport::AccountInfoValue>,
|
|
>;
|
|
|
|
/// Inspects Localnet or Devnet ATA state required before simulation.
|
|
pub async fn inspect_spl_associated_token_account_stateful_readiness(
|
|
pool: &ks_onchain_transport::HttpEndpointPool,
|
|
request: &SplAssociatedTokenAccountStatefulReadinessRequest,
|
|
) -> ks_core::Result<SplAssociatedTokenAccountStatefulReadinessReport> {
|
|
if request.query_role.trim().is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::config(
|
|
"ATA stateful readiness query_role must not be empty",
|
|
));
|
|
}
|
|
match validate_cluster_kind(request.cluster) {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let genesis = match pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match validate_genesis(request.cluster, &genesis) {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let plan = match ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&ks_lib::ExSplAssociatedTokenAccountExecutor,
|
|
&request.intent,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let addresses = match operation_addresses(&request.intent.operation) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let config = match ks_onchain_transport::GetAccountInfoConfig::confirmed_with_data(
|
|
MAX_TOKEN_2022_ACCOUNT_BYTES,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mut cache = AccountCache::new();
|
|
let mut context_slot: std::option::Option<u64> = std::option::Option::None;
|
|
for address in &addresses {
|
|
let result = match pool
|
|
.get_account_info_for_role(request.query_role.as_str(), address, &config)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
context_slot = std::option::Option::Some(match context_slot {
|
|
std::option::Option::Some(slot) => slot.max(result.context.slot),
|
|
std::option::Option::None => result.context.slot,
|
|
});
|
|
cache.insert(address.0.clone(), result.account);
|
|
}
|
|
let rent = match pool
|
|
.get_minimum_balance_for_rent_exemption_for_role(
|
|
request.query_role.as_str(),
|
|
BASE_TOKEN_ACCOUNT_LEN as u64,
|
|
&ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value.minimum_balance_lamports,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let balance_config = ks_onchain_transport::GetBalanceConfig::confirmed();
|
|
let payer_balance = match pool
|
|
.get_balance_for_role(
|
|
request.query_role.as_str(),
|
|
&request.intent.fee_payer,
|
|
&balance_config,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value.lamports,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return evaluate(
|
|
request,
|
|
&plan,
|
|
&genesis.genesis_hash,
|
|
context_slot,
|
|
&cache,
|
|
rent,
|
|
payer_balance,
|
|
);
|
|
}
|
|
|
|
/// Verifies final ATA account relationships after a confirmed execution.
|
|
pub async fn inspect_spl_associated_token_account_post_execution(
|
|
pool: &ks_onchain_transport::HttpEndpointPool,
|
|
query_role: &str,
|
|
cluster: ks_lib::ExApiExecutionCluster,
|
|
operation: &ks_lib::ExSplAssociatedTokenAccountOperation,
|
|
) -> ks_core::Result<SplAssociatedTokenAccountPostExecutionReport> {
|
|
if query_role.trim().is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::config(
|
|
"ATA post-execution query role must not be empty",
|
|
));
|
|
}
|
|
if let std::result::Result::Err(error) = validate_cluster_kind(cluster) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let genesis = match pool.get_genesis_hash_for_role(query_role).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = validate_genesis(cluster, &genesis) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let addresses = match operation_addresses(operation) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let config = match ks_onchain_transport::GetAccountInfoConfig::confirmed_with_data(
|
|
MAX_TOKEN_2022_ACCOUNT_BYTES,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mut cache = AccountCache::new();
|
|
let mut context_slot: std::option::Option<u64> = std::option::Option::None;
|
|
for address in &addresses {
|
|
let result = match pool.get_account_info_for_role(query_role, address, &config).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
context_slot = std::option::Option::Some(match context_slot {
|
|
std::option::Option::Some(slot) => slot.max(result.context.slot),
|
|
std::option::Option::None => result.context.slot,
|
|
});
|
|
cache.insert(address.0.clone(), result.account);
|
|
}
|
|
return evaluate_post_execution(cluster, operation, context_slot, &cache);
|
|
}
|
|
|
|
fn validate_cluster_kind(cluster: ks_lib::ExApiExecutionCluster) -> ks_core::Result<()> {
|
|
return match cluster {
|
|
ks_lib::ExApiExecutionCluster::Localnet | ks_lib::ExApiExecutionCluster::Devnet => {
|
|
std::result::Result::Ok(())
|
|
},
|
|
ks_lib::ExApiExecutionCluster::Testnet | ks_lib::ExApiExecutionCluster::Mainnet => {
|
|
std::result::Result::Err(ks_core::Error::new(
|
|
"spl_ata_stateful_cluster_unsupported",
|
|
"ATA stateful readiness is restricted to Localnet and Devnet",
|
|
))
|
|
},
|
|
};
|
|
}
|
|
|
|
fn validate_genesis(
|
|
expected: ks_lib::ExApiExecutionCluster,
|
|
genesis: &ks_onchain_transport::GenesisHashResult,
|
|
) -> ks_core::Result<()> {
|
|
return match expected {
|
|
ks_lib::ExApiExecutionCluster::Devnet
|
|
if genesis.classified_cluster
|
|
== std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet) =>
|
|
{
|
|
std::result::Result::Ok(())
|
|
},
|
|
ks_lib::ExApiExecutionCluster::Localnet if genesis.classified_cluster.is_none() => {
|
|
std::result::Result::Ok(())
|
|
},
|
|
ks_lib::ExApiExecutionCluster::Devnet => std::result::Result::Err(ks_core::Error::new(
|
|
"spl_ata_stateful_devnet_genesis_mismatch",
|
|
"ATA Devnet readiness requires the official Devnet genesis hash",
|
|
)),
|
|
ks_lib::ExApiExecutionCluster::Localnet => std::result::Result::Err(ks_core::Error::new(
|
|
"spl_ata_stateful_localnet_public_cluster",
|
|
"ATA Localnet readiness refuses endpoints classified as a public cluster",
|
|
)),
|
|
ks_lib::ExApiExecutionCluster::Testnet | ks_lib::ExApiExecutionCluster::Mainnet => {
|
|
validate_cluster_kind(expected)
|
|
},
|
|
};
|
|
}
|
|
|
|
fn operation_addresses(
|
|
operation: &ks_lib::ExSplAssociatedTokenAccountOperation,
|
|
) -> ks_core::Result<std::vec::Vec<ks_lib::MdPubkey>> {
|
|
let mut values = std::vec::Vec::new();
|
|
match operation {
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::Create {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
}
|
|
| ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
} => {
|
|
values.push(mint.clone());
|
|
values.push(result_value!(derive_ata(wallet_owner, mint, token_program.program_id(),)));
|
|
},
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::RecoverNested {
|
|
wallet_owner,
|
|
owner_mint,
|
|
nested_mint,
|
|
token_program,
|
|
} => {
|
|
let owner_ata =
|
|
result_value!(derive_ata(wallet_owner, owner_mint, token_program.program_id(),));
|
|
let nested_ata =
|
|
result_value!(derive_ata(&owner_ata, nested_mint, token_program.program_id(),));
|
|
let destination_ata =
|
|
result_value!(derive_ata(wallet_owner, nested_mint, token_program.program_id(),));
|
|
values.extend([
|
|
owner_mint.clone(),
|
|
nested_mint.clone(),
|
|
owner_ata,
|
|
nested_ata,
|
|
destination_ata,
|
|
]);
|
|
},
|
|
}
|
|
values.sort_by(|left, right| return left.0.cmp(&right.0));
|
|
values.dedup();
|
|
return std::result::Result::Ok(values);
|
|
}
|
|
|
|
fn derive_ata(
|
|
wallet: &ks_lib::MdPubkey,
|
|
mint: &ks_lib::MdPubkey,
|
|
token_program: &str,
|
|
) -> ks_core::Result<ks_lib::MdPubkey> {
|
|
let wallet: solana_pubkey::Pubkey = match wallet.0.parse() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"spl_ata_stateful_wallet_invalid",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let mint: solana_pubkey::Pubkey = match mint.0.parse() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"spl_ata_stateful_mint_invalid",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let token_program: solana_pubkey::Pubkey = match token_program.parse() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"spl_ata_stateful_token_program_invalid",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let derived = spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
|
|
&wallet,
|
|
&mint,
|
|
&token_program,
|
|
);
|
|
return std::result::Result::Ok(ks_lib::MdPubkey(derived.to_string()));
|
|
}
|
|
|
|
fn evaluate(
|
|
request: &SplAssociatedTokenAccountStatefulReadinessRequest,
|
|
plan: &ks_lib::ExApiPreparedExecutionPlan,
|
|
genesis_hash: &str,
|
|
context_slot: std::option::Option<u64>,
|
|
cache: &AccountCache,
|
|
base_rent_lamports: u64,
|
|
payer_balance_lamports: u64,
|
|
) -> ks_core::Result<SplAssociatedTokenAccountStatefulReadinessReport> {
|
|
let mut report = SplAssociatedTokenAccountStatefulReadinessReport {
|
|
cluster: request.cluster,
|
|
operation_code: request.intent.operation.operation_code().to_string(),
|
|
status: SplAssociatedTokenAccountStatefulReadinessStatus::Ready,
|
|
context_slot,
|
|
checks: std::vec::Vec::new(),
|
|
facts: std::vec![
|
|
SplAssociatedTokenAccountStatefulFact {
|
|
key: "genesis_hash".to_string(),
|
|
value: genesis_hash.to_string(),
|
|
},
|
|
SplAssociatedTokenAccountStatefulFact {
|
|
key: "base_token_account_rent_lamports".to_string(),
|
|
value: base_rent_lamports.to_string(),
|
|
},
|
|
],
|
|
};
|
|
push_check(
|
|
&mut report,
|
|
"ata_program_id",
|
|
plan.instructions.len() == 1
|
|
&& plan.instructions[0].program_id.0 == ks_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
"plan must contain exactly one instruction for the canonical ATA Program",
|
|
);
|
|
push_check(
|
|
&mut report,
|
|
"simulation_required",
|
|
plan.policy.simulation == ks_lib::ExApiExecutionSimulationPolicy::Required,
|
|
"ATA plan must require simulation",
|
|
);
|
|
for signer in &plan.required_signers {
|
|
push_check(
|
|
&mut report,
|
|
"required_signer_available",
|
|
request.available_signers.contains(&signer.pubkey),
|
|
format!("required {} signer {} must be available", signer.role, signer.pubkey.0),
|
|
);
|
|
}
|
|
let max_fee = match plan.policy.cost_limit.max_fee_lamports {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => 0,
|
|
};
|
|
let total_ceiling = match plan.requested_spend_lamports.checked_add(max_fee) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => u64::MAX,
|
|
};
|
|
push_check(
|
|
&mut report,
|
|
"payer_balance_covers_ceiling",
|
|
payer_balance_lamports >= total_ceiling,
|
|
format!("payer balance must cover rent-plus-fee ceiling {total_ceiling}"),
|
|
);
|
|
let operation = &request.intent.operation;
|
|
match operation {
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::Create {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
} => {
|
|
check_mint(&mut report, cache, mint, token_program.program_id());
|
|
let ata = result_value!(derive_ata(wallet_owner, mint, token_program.program_id(),));
|
|
push_fact(&mut report, "derived_ata", ata.0.clone());
|
|
check_creation_plan_accounts(&mut report, plan, &ata, token_program.program_id());
|
|
push_check(
|
|
&mut report,
|
|
"create_ata_absent",
|
|
cache.get(ata.0.as_str()).is_some_and(|value| return value.is_none()),
|
|
"strict Create requires the canonical ATA to be absent",
|
|
);
|
|
push_check(
|
|
&mut report,
|
|
"rent_ceiling_covers_base_account",
|
|
plan.requested_spend_lamports >= base_rent_lamports,
|
|
"creation rent ceiling must cover at least the base Token account rent",
|
|
);
|
|
},
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
} => {
|
|
check_mint(&mut report, cache, mint, token_program.program_id());
|
|
let ata = result_value!(derive_ata(wallet_owner, mint, token_program.program_id(),));
|
|
push_fact(&mut report, "derived_ata", ata.0.clone());
|
|
check_creation_plan_accounts(&mut report, plan, &ata, token_program.program_id());
|
|
if let std::option::Option::Some(std::option::Option::Some(account)) =
|
|
cache.get(ata.0.as_str())
|
|
{
|
|
check_token_account(
|
|
&mut report,
|
|
account,
|
|
token_program.program_id(),
|
|
mint,
|
|
wallet_owner,
|
|
"idempotent_existing_ata",
|
|
);
|
|
} else {
|
|
push_check(
|
|
&mut report,
|
|
"idempotent_absent_or_compatible",
|
|
cache.contains_key(ata.0.as_str()),
|
|
"idempotent ATA must be confirmed absent or validated as compatible",
|
|
);
|
|
push_check(
|
|
&mut report,
|
|
"rent_ceiling_covers_base_account",
|
|
plan.requested_spend_lamports >= base_rent_lamports,
|
|
"absent idempotent ATA requires a ceiling covering base account rent",
|
|
);
|
|
}
|
|
},
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::RecoverNested {
|
|
wallet_owner,
|
|
owner_mint,
|
|
nested_mint,
|
|
token_program,
|
|
} => {
|
|
check_mint(&mut report, cache, owner_mint, token_program.program_id());
|
|
check_mint(&mut report, cache, nested_mint, token_program.program_id());
|
|
let owner_ata =
|
|
result_value!(derive_ata(wallet_owner, owner_mint, token_program.program_id(),));
|
|
let nested_ata =
|
|
result_value!(derive_ata(&owner_ata, nested_mint, token_program.program_id(),));
|
|
let destination_ata =
|
|
result_value!(derive_ata(wallet_owner, nested_mint, token_program.program_id(),));
|
|
for (key, value) in [
|
|
("derived_owner_ata", &owner_ata),
|
|
("derived_nested_ata", &nested_ata),
|
|
("derived_destination_ata", &destination_ata),
|
|
] {
|
|
push_fact(&mut report, key, value.0.clone());
|
|
}
|
|
let accounts_exact = match plan.instructions.first() {
|
|
std::option::Option::Some(instruction) => {
|
|
instruction.accounts.len() == 7
|
|
&& instruction.accounts[0].pubkey == nested_ata
|
|
&& instruction.accounts[2].pubkey == destination_ata
|
|
&& instruction.accounts[3].pubkey == owner_ata
|
|
&& instruction.accounts[6].pubkey.0 == token_program.program_id()
|
|
},
|
|
std::option::Option::None => false,
|
|
};
|
|
push_check(
|
|
&mut report,
|
|
"recover_plan_accounts_exact",
|
|
accounts_exact,
|
|
"RecoverNested plan must retain all three derived ATA addresses and selected Token Program in official positions",
|
|
);
|
|
check_cached_token_account(
|
|
&mut report,
|
|
cache,
|
|
&owner_ata,
|
|
token_program.program_id(),
|
|
owner_mint,
|
|
wallet_owner,
|
|
"owner_ata",
|
|
);
|
|
check_cached_token_account(
|
|
&mut report,
|
|
cache,
|
|
&nested_ata,
|
|
token_program.program_id(),
|
|
nested_mint,
|
|
&owner_ata,
|
|
"nested_ata",
|
|
);
|
|
check_cached_token_account(
|
|
&mut report,
|
|
cache,
|
|
&destination_ata,
|
|
token_program.program_id(),
|
|
nested_mint,
|
|
wallet_owner,
|
|
"destination_ata",
|
|
);
|
|
push_check(
|
|
&mut report,
|
|
"recovery_zero_rent_spend",
|
|
plan.requested_spend_lamports == 0,
|
|
"RecoverNested must not reserve rent spending",
|
|
);
|
|
},
|
|
}
|
|
report.status = if report.checks.iter().all(|check| return check.passed) {
|
|
SplAssociatedTokenAccountStatefulReadinessStatus::Ready
|
|
} else {
|
|
SplAssociatedTokenAccountStatefulReadinessStatus::Blocked
|
|
};
|
|
return std::result::Result::Ok(report);
|
|
}
|
|
|
|
fn evaluate_post_execution(
|
|
cluster: ks_lib::ExApiExecutionCluster,
|
|
operation: &ks_lib::ExSplAssociatedTokenAccountOperation,
|
|
context_slot: std::option::Option<u64>,
|
|
cache: &AccountCache,
|
|
) -> ks_core::Result<SplAssociatedTokenAccountPostExecutionReport> {
|
|
let mut report = SplAssociatedTokenAccountPostExecutionReport {
|
|
cluster,
|
|
operation_code: operation.operation_code().to_string(),
|
|
status: SplAssociatedTokenAccountStatefulReadinessStatus::Ready,
|
|
context_slot,
|
|
checks: std::vec::Vec::new(),
|
|
facts: std::vec::Vec::new(),
|
|
};
|
|
match operation {
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::Create {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
}
|
|
| ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
} => {
|
|
let ata = match derive_ata(wallet_owner, mint, token_program.program_id()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
push_post_fact(&mut report, "derived_ata", ata.0.clone());
|
|
check_post_cached_token_account(
|
|
&mut report,
|
|
cache,
|
|
&ata,
|
|
token_program.program_id(),
|
|
mint,
|
|
wallet_owner,
|
|
"post_execution_ata",
|
|
);
|
|
},
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::RecoverNested {
|
|
wallet_owner,
|
|
owner_mint,
|
|
nested_mint,
|
|
token_program,
|
|
} => {
|
|
let owner_ata =
|
|
result_value!(derive_ata(wallet_owner, owner_mint, token_program.program_id(),));
|
|
let nested_ata =
|
|
result_value!(derive_ata(&owner_ata, nested_mint, token_program.program_id(),));
|
|
let destination_ata =
|
|
result_value!(derive_ata(wallet_owner, nested_mint, token_program.program_id(),));
|
|
for (key, value) in [
|
|
("derived_owner_ata", &owner_ata),
|
|
("derived_nested_ata", &nested_ata),
|
|
("derived_destination_ata", &destination_ata),
|
|
] {
|
|
push_post_fact(&mut report, key, value.0.clone());
|
|
}
|
|
check_post_cached_token_account(
|
|
&mut report,
|
|
cache,
|
|
&owner_ata,
|
|
token_program.program_id(),
|
|
owner_mint,
|
|
wallet_owner,
|
|
"post_execution_owner_ata",
|
|
);
|
|
push_post_check(
|
|
&mut report,
|
|
"post_execution_nested_ata_closed",
|
|
cache.get(nested_ata.0.as_str()).is_some_and(|account| return account.is_none()),
|
|
"confirmed RecoverNested must leave the nested ATA closed",
|
|
);
|
|
check_post_cached_token_account(
|
|
&mut report,
|
|
cache,
|
|
&destination_ata,
|
|
token_program.program_id(),
|
|
nested_mint,
|
|
wallet_owner,
|
|
"post_execution_destination_ata",
|
|
);
|
|
},
|
|
}
|
|
report.status = if report.checks.iter().all(|check| return check.passed) {
|
|
SplAssociatedTokenAccountStatefulReadinessStatus::Ready
|
|
} else {
|
|
SplAssociatedTokenAccountStatefulReadinessStatus::Blocked
|
|
};
|
|
return std::result::Result::Ok(report);
|
|
}
|
|
|
|
fn check_post_cached_token_account(
|
|
report: &mut SplAssociatedTokenAccountPostExecutionReport,
|
|
cache: &AccountCache,
|
|
address: &ks_lib::MdPubkey,
|
|
token_program: &str,
|
|
expected_mint: &ks_lib::MdPubkey,
|
|
expected_owner: &ks_lib::MdPubkey,
|
|
role: &str,
|
|
) {
|
|
let account = cache.get(address.0.as_str()).and_then(|value| return value.as_ref());
|
|
push_post_check(
|
|
report,
|
|
format!("{role}_exists"),
|
|
account.is_some(),
|
|
format!("{role} {} must exist after confirmed execution", address.0),
|
|
);
|
|
if let std::option::Option::Some(account) = account {
|
|
push_post_check(
|
|
report,
|
|
format!("{role}_token_program_owner"),
|
|
account.owner.0 == token_program,
|
|
format!("{role} must be owned by the selected Token Program"),
|
|
);
|
|
let layout_valid = account.data.len() >= BASE_TOKEN_ACCOUNT_LEN;
|
|
push_post_check(
|
|
report,
|
|
format!("{role}_base_layout"),
|
|
layout_valid,
|
|
format!("{role} must expose the complete base Token Account layout"),
|
|
);
|
|
if layout_valid {
|
|
let mint = bs58::encode(&account.data[0..32]).into_string();
|
|
let owner = bs58::encode(&account.data[32..64]).into_string();
|
|
push_post_check(
|
|
report,
|
|
format!("{role}_mint"),
|
|
mint == expected_mint.0,
|
|
format!("{role} mint must match {}", expected_mint.0),
|
|
);
|
|
push_post_check(
|
|
report,
|
|
format!("{role}_wallet_owner"),
|
|
owner == expected_owner.0,
|
|
format!("{role} owner must match {}", expected_owner.0),
|
|
);
|
|
push_post_check(
|
|
report,
|
|
format!("{role}_initialized"),
|
|
matches!(account.data[108], 1 | 2),
|
|
format!("{role} must be initialized or frozen"),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn push_post_check(
|
|
report: &mut SplAssociatedTokenAccountPostExecutionReport,
|
|
code: impl std::convert::Into<std::string::String>,
|
|
passed: bool,
|
|
message: impl std::convert::Into<std::string::String>,
|
|
) {
|
|
report.checks.push(SplAssociatedTokenAccountStatefulCheck {
|
|
code: code.into(),
|
|
passed,
|
|
message: message.into(),
|
|
});
|
|
}
|
|
|
|
fn push_post_fact(
|
|
report: &mut SplAssociatedTokenAccountPostExecutionReport,
|
|
key: impl std::convert::Into<std::string::String>,
|
|
value: impl std::convert::Into<std::string::String>,
|
|
) {
|
|
report
|
|
.facts
|
|
.push(SplAssociatedTokenAccountStatefulFact { key: key.into(), value: value.into() });
|
|
}
|
|
|
|
fn check_creation_plan_accounts(
|
|
report: &mut SplAssociatedTokenAccountStatefulReadinessReport,
|
|
plan: &ks_lib::ExApiPreparedExecutionPlan,
|
|
derived_ata: &ks_lib::MdPubkey,
|
|
token_program: &str,
|
|
) {
|
|
let accounts_exact = match plan.instructions.first() {
|
|
std::option::Option::Some(instruction) => {
|
|
instruction.accounts.len() == 6
|
|
&& instruction.accounts[1].pubkey == derived_ata.clone()
|
|
&& instruction.accounts[4].pubkey.0 == ks_program_ids::SYSTEM_PROGRAM_ID
|
|
&& instruction.accounts[5].pubkey.0 == token_program
|
|
},
|
|
std::option::Option::None => false,
|
|
};
|
|
push_check(
|
|
report,
|
|
"creation_plan_accounts_exact",
|
|
accounts_exact,
|
|
"creation plan must retain derived ATA, System Program and selected Token Program in official positions",
|
|
);
|
|
}
|
|
|
|
fn check_mint(
|
|
report: &mut SplAssociatedTokenAccountStatefulReadinessReport,
|
|
cache: &AccountCache,
|
|
mint: &ks_lib::MdPubkey,
|
|
token_program: &str,
|
|
) {
|
|
let account = cache.get(mint.0.as_str()).and_then(|value| return value.as_ref());
|
|
push_check(report, "mint_exists", account.is_some(), format!("mint {} must exist", mint.0));
|
|
if let std::option::Option::Some(account) = account {
|
|
push_check(
|
|
report,
|
|
"mint_token_program_owner",
|
|
account.owner.0 == token_program,
|
|
format!("mint {} must be owned by selected Token Program", mint.0),
|
|
);
|
|
push_check(
|
|
report,
|
|
"mint_base_layout",
|
|
account.data.len() >= BASE_MINT_LEN,
|
|
format!("mint {} must expose the complete base Mint layout", mint.0),
|
|
);
|
|
push_check(
|
|
report,
|
|
"mint_initialized",
|
|
account.data.get(45) == std::option::Option::Some(&1),
|
|
format!("mint {} must be initialized", mint.0),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn check_cached_token_account(
|
|
report: &mut SplAssociatedTokenAccountStatefulReadinessReport,
|
|
cache: &AccountCache,
|
|
address: &ks_lib::MdPubkey,
|
|
token_program: &str,
|
|
expected_mint: &ks_lib::MdPubkey,
|
|
expected_owner: &ks_lib::MdPubkey,
|
|
role: &str,
|
|
) {
|
|
let account = cache.get(address.0.as_str()).and_then(|value| return value.as_ref());
|
|
push_check(
|
|
report,
|
|
format!("{role}_exists"),
|
|
account.is_some(),
|
|
format!("{role} {} must exist", address.0),
|
|
);
|
|
if let std::option::Option::Some(account) = account {
|
|
check_token_account(report, account, token_program, expected_mint, expected_owner, role);
|
|
}
|
|
}
|
|
|
|
fn check_token_account(
|
|
report: &mut SplAssociatedTokenAccountStatefulReadinessReport,
|
|
account: &ks_onchain_transport::AccountInfoValue,
|
|
token_program: &str,
|
|
expected_mint: &ks_lib::MdPubkey,
|
|
expected_owner: &ks_lib::MdPubkey,
|
|
role: &str,
|
|
) {
|
|
push_check(
|
|
report,
|
|
format!("{role}_token_program_owner"),
|
|
account.owner.0 == token_program,
|
|
format!("{role} must be owned by selected Token Program"),
|
|
);
|
|
let layout_valid = account.data.len() >= BASE_TOKEN_ACCOUNT_LEN;
|
|
push_check(
|
|
report,
|
|
format!("{role}_base_layout"),
|
|
layout_valid,
|
|
format!("{role} must expose the complete base Token Account layout"),
|
|
);
|
|
if layout_valid {
|
|
let mint = bs58::encode(&account.data[0..32]).into_string();
|
|
let owner = bs58::encode(&account.data[32..64]).into_string();
|
|
push_check(
|
|
report,
|
|
format!("{role}_mint"),
|
|
mint == expected_mint.0,
|
|
format!("{role} mint must match {}", expected_mint.0),
|
|
);
|
|
push_check(
|
|
report,
|
|
format!("{role}_wallet_owner"),
|
|
owner == expected_owner.0,
|
|
format!("{role} owner must match {}", expected_owner.0),
|
|
);
|
|
push_check(
|
|
report,
|
|
format!("{role}_initialized"),
|
|
matches!(account.data[108], 1 | 2),
|
|
format!("{role} must be initialized or frozen"),
|
|
);
|
|
}
|
|
}
|
|
|
|
fn push_check(
|
|
report: &mut SplAssociatedTokenAccountStatefulReadinessReport,
|
|
code: impl std::convert::Into<std::string::String>,
|
|
passed: bool,
|
|
message: impl std::convert::Into<std::string::String>,
|
|
) {
|
|
report.checks.push(SplAssociatedTokenAccountStatefulCheck {
|
|
code: code.into(),
|
|
passed,
|
|
message: message.into(),
|
|
});
|
|
}
|
|
|
|
fn push_fact(
|
|
report: &mut SplAssociatedTokenAccountStatefulReadinessReport,
|
|
key: impl std::convert::Into<std::string::String>,
|
|
value: impl std::convert::Into<std::string::String>,
|
|
) {
|
|
report
|
|
.facts
|
|
.push(SplAssociatedTokenAccountStatefulFact { key: key.into(), value: value.into() });
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn pubkey(value: &str) -> ks_lib::MdPubkey {
|
|
return ks_lib::MdPubkey(value.to_string());
|
|
}
|
|
|
|
fn policy(rent: u64, signers: std::vec::Vec<ks_lib::MdPubkey>) -> ks_lib::ExApiExecutionPolicy {
|
|
return ks_lib::ExApiExecutionPolicy {
|
|
cost_limit: ks_lib::ExApiExecutionCostLimit {
|
|
max_spend_lamports: std::option::Option::Some(rent),
|
|
max_fee_lamports: std::option::Option::Some(10_000),
|
|
max_compute_unit_price_micro_lamports: std::option::Option::None,
|
|
},
|
|
authorized_signers: signers,
|
|
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
|
|
canonical_insert_required: true,
|
|
core_extraction_required: true,
|
|
decode_replay_required: true,
|
|
materialization_required: true,
|
|
},
|
|
..ks_lib::ExApiExecutionPolicy::default()
|
|
};
|
|
}
|
|
|
|
fn request(
|
|
operation: ks_lib::ExSplAssociatedTokenAccountOperation,
|
|
rent: u64,
|
|
) -> crate::SplAssociatedTokenAccountStatefulReadinessRequest {
|
|
let fee_payer = pubkey(ks_program_ids::SYSTEM_PROGRAM_ID);
|
|
let wallet = match &operation {
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::Create { wallet_owner, .. }
|
|
| ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner, ..
|
|
}
|
|
| ks_lib::ExSplAssociatedTokenAccountOperation::RecoverNested {
|
|
wallet_owner, ..
|
|
} => wallet_owner.clone(),
|
|
};
|
|
let mut signers = std::vec![fee_payer.clone()];
|
|
if !signers.contains(&wallet) {
|
|
signers.push(wallet);
|
|
}
|
|
return crate::SplAssociatedTokenAccountStatefulReadinessRequest {
|
|
query_role: "query".to_string(),
|
|
cluster: ks_lib::ExApiExecutionCluster::Devnet,
|
|
intent: ks_lib::ExSplAssociatedTokenAccountExecutionIntent {
|
|
intent_id: "ata-stateful-1".to_string(),
|
|
fee_payer,
|
|
max_rent_lamports: rent,
|
|
policy: policy(rent, signers.clone()),
|
|
operation,
|
|
},
|
|
available_signers: signers,
|
|
};
|
|
}
|
|
|
|
fn mint_account(token_program: &str) -> ks_onchain_transport::AccountInfoValue {
|
|
let mut data = std::vec![0; super::BASE_MINT_LEN];
|
|
data[45] = 1;
|
|
return ks_onchain_transport::AccountInfoValue {
|
|
lamports: 1,
|
|
owner: ks_lib::MdProgramId(token_program.to_string()),
|
|
executable: false,
|
|
rent_epoch: 0,
|
|
space: data.len() as u64,
|
|
data,
|
|
};
|
|
}
|
|
|
|
fn token_account(
|
|
token_program: &str,
|
|
mint: &ks_lib::MdPubkey,
|
|
owner: &ks_lib::MdPubkey,
|
|
) -> ks_onchain_transport::AccountInfoValue {
|
|
let mut data = std::vec![0; super::BASE_TOKEN_ACCOUNT_LEN];
|
|
let mint_bytes = bs58::decode(&mint.0)
|
|
.into_vec()
|
|
.unwrap_or_else(|error| panic!("mint fixture decode failed: {error}"));
|
|
let owner_bytes = bs58::decode(&owner.0)
|
|
.into_vec()
|
|
.unwrap_or_else(|error| panic!("owner fixture decode failed: {error}"));
|
|
data[0..32].copy_from_slice(&mint_bytes);
|
|
data[32..64].copy_from_slice(&owner_bytes);
|
|
data[108] = 1;
|
|
return ks_onchain_transport::AccountInfoValue {
|
|
lamports: 1,
|
|
owner: ks_lib::MdProgramId(token_program.to_string()),
|
|
executable: false,
|
|
rent_epoch: 0,
|
|
space: data.len() as u64,
|
|
data,
|
|
};
|
|
}
|
|
|
|
fn plan(
|
|
request: &crate::SplAssociatedTokenAccountStatefulReadinessRequest,
|
|
) -> ks_lib::ExApiPreparedExecutionPlan {
|
|
return ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&ks_lib::ExSplAssociatedTokenAccountExecutor,
|
|
&request.intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("ATA test plan failed: {error}"));
|
|
}
|
|
|
|
#[test]
|
|
fn absent_classic_create_is_ready_with_exact_derived_address_and_rent_ceiling() {
|
|
let wallet = pubkey(ks_program_ids::STAKE_PROGRAM_ID);
|
|
let mint = pubkey(ks_program_ids::VOTE_PROGRAM_ID);
|
|
let request = request(
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::Create {
|
|
wallet_owner: wallet.clone(),
|
|
mint: mint.clone(),
|
|
token_program: ks_lib::ExSplAssociatedTokenProgram::Classic,
|
|
},
|
|
2_100_000,
|
|
);
|
|
let ata = super::derive_ata(&wallet, &mint, ks_program_ids::SPL_TOKEN_PROGRAM_ID)
|
|
.unwrap_or_else(|error| panic!("ATA derivation failed: {error}"));
|
|
let mut cache = super::AccountCache::new();
|
|
cache.insert(
|
|
mint.0.clone(),
|
|
std::option::Option::Some(mint_account(ks_program_ids::SPL_TOKEN_PROGRAM_ID)),
|
|
);
|
|
cache.insert(ata.0, std::option::Option::None);
|
|
let report = super::evaluate(
|
|
&request,
|
|
&plan(&request),
|
|
"devnet",
|
|
std::option::Option::Some(1),
|
|
&cache,
|
|
2_000_000,
|
|
3_000_000,
|
|
)
|
|
.unwrap_or_else(|error| panic!("ATA readiness failed: {error}"));
|
|
assert_eq!(report.status, crate::SplAssociatedTokenAccountStatefulReadinessStatus::Ready);
|
|
}
|
|
|
|
#[test]
|
|
fn idempotent_existing_account_conflict_is_blocked() {
|
|
let wallet = pubkey(ks_program_ids::STAKE_PROGRAM_ID);
|
|
let mint = pubkey(ks_program_ids::VOTE_PROGRAM_ID);
|
|
let request = request(
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner: wallet.clone(),
|
|
mint: mint.clone(),
|
|
token_program: ks_lib::ExSplAssociatedTokenProgram::Token2022,
|
|
},
|
|
2_100_000,
|
|
);
|
|
let ata = super::derive_ata(&wallet, &mint, ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID)
|
|
.unwrap_or_else(|error| panic!("ATA derivation failed: {error}"));
|
|
let mut cache = super::AccountCache::new();
|
|
cache.insert(
|
|
mint.0.clone(),
|
|
std::option::Option::Some(mint_account(ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID)),
|
|
);
|
|
cache.insert(
|
|
ata.0,
|
|
std::option::Option::Some(token_account(
|
|
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
&mint,
|
|
&pubkey(ks_program_ids::CONFIG_PROGRAM_ID),
|
|
)),
|
|
);
|
|
let report = super::evaluate(
|
|
&request,
|
|
&plan(&request),
|
|
"devnet",
|
|
std::option::Option::Some(1),
|
|
&cache,
|
|
2_000_000,
|
|
3_000_000,
|
|
)
|
|
.unwrap_or_else(|error| panic!("ATA readiness failed: {error}"));
|
|
assert_eq!(report.status, crate::SplAssociatedTokenAccountStatefulReadinessStatus::Blocked);
|
|
assert!(
|
|
report
|
|
.checks
|
|
.iter()
|
|
.any(|check| return check.code == "idempotent_existing_ata_wallet_owner"
|
|
&& !check.passed)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn recover_nested_validates_all_three_canonical_accounts_and_signers() {
|
|
let wallet = pubkey(ks_program_ids::STAKE_PROGRAM_ID);
|
|
let owner_mint = pubkey(ks_program_ids::VOTE_PROGRAM_ID);
|
|
let nested_mint = pubkey(ks_program_ids::CONFIG_PROGRAM_ID);
|
|
let token_program = ks_program_ids::SPL_TOKEN_PROGRAM_ID;
|
|
let request = request(
|
|
ks_lib::ExSplAssociatedTokenAccountOperation::RecoverNested {
|
|
wallet_owner: wallet.clone(),
|
|
owner_mint: owner_mint.clone(),
|
|
nested_mint: nested_mint.clone(),
|
|
token_program: ks_lib::ExSplAssociatedTokenProgram::Classic,
|
|
},
|
|
0,
|
|
);
|
|
let owner_ata = super::derive_ata(&wallet, &owner_mint, token_program)
|
|
.unwrap_or_else(|error| panic!("owner ATA failed: {error}"));
|
|
let nested_ata = super::derive_ata(&owner_ata, &nested_mint, token_program)
|
|
.unwrap_or_else(|error| panic!("nested ATA failed: {error}"));
|
|
let destination = super::derive_ata(&wallet, &nested_mint, token_program)
|
|
.unwrap_or_else(|error| panic!("destination ATA failed: {error}"));
|
|
let mut cache = super::AccountCache::new();
|
|
cache.insert(owner_mint.0.clone(), std::option::Option::Some(mint_account(token_program)));
|
|
cache.insert(nested_mint.0.clone(), std::option::Option::Some(mint_account(token_program)));
|
|
cache.insert(
|
|
owner_ata.0.clone(),
|
|
std::option::Option::Some(token_account(token_program, &owner_mint, &wallet)),
|
|
);
|
|
cache.insert(
|
|
nested_ata.0,
|
|
std::option::Option::Some(token_account(token_program, &nested_mint, &owner_ata)),
|
|
);
|
|
cache.insert(
|
|
destination.0,
|
|
std::option::Option::Some(token_account(token_program, &nested_mint, &wallet)),
|
|
);
|
|
let report = super::evaluate(
|
|
&request,
|
|
&plan(&request),
|
|
"devnet",
|
|
std::option::Option::Some(1),
|
|
&cache,
|
|
2_000_000,
|
|
20_000,
|
|
)
|
|
.unwrap_or_else(|error| panic!("ATA readiness failed: {error}"));
|
|
assert_eq!(report.status, crate::SplAssociatedTokenAccountStatefulReadinessStatus::Ready);
|
|
}
|
|
|
|
#[test]
|
|
fn creation_postcondition_requires_one_compatible_existing_ata() {
|
|
let wallet = pubkey(ks_program_ids::STAKE_PROGRAM_ID);
|
|
let mint = pubkey(ks_program_ids::VOTE_PROGRAM_ID);
|
|
let operation = ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner: wallet.clone(),
|
|
mint: mint.clone(),
|
|
token_program: ks_lib::ExSplAssociatedTokenProgram::Token2022,
|
|
};
|
|
let ata = super::derive_ata(&wallet, &mint, ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID)
|
|
.unwrap_or_else(|error| panic!("ATA derivation failed: {error}"));
|
|
let mut cache = super::AccountCache::new();
|
|
cache.insert(
|
|
ata.0,
|
|
std::option::Option::Some(token_account(
|
|
ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
|
&mint,
|
|
&wallet,
|
|
)),
|
|
);
|
|
let report = super::evaluate_post_execution(
|
|
ks_lib::ExApiExecutionCluster::Devnet,
|
|
&operation,
|
|
std::option::Option::Some(12),
|
|
&cache,
|
|
)
|
|
.unwrap_or_else(|error| panic!("ATA postcondition failed: {error}"));
|
|
assert_eq!(report.status, crate::SplAssociatedTokenAccountStatefulReadinessStatus::Ready);
|
|
}
|
|
|
|
#[test]
|
|
fn recover_postcondition_requires_nested_closed_and_destination_compatible() {
|
|
let wallet = pubkey(ks_program_ids::STAKE_PROGRAM_ID);
|
|
let owner_mint = pubkey(ks_program_ids::VOTE_PROGRAM_ID);
|
|
let nested_mint = pubkey(ks_program_ids::CONFIG_PROGRAM_ID);
|
|
let token_program = ks_program_ids::SPL_TOKEN_PROGRAM_ID;
|
|
let operation = ks_lib::ExSplAssociatedTokenAccountOperation::RecoverNested {
|
|
wallet_owner: wallet.clone(),
|
|
owner_mint: owner_mint.clone(),
|
|
nested_mint: nested_mint.clone(),
|
|
token_program: ks_lib::ExSplAssociatedTokenProgram::Classic,
|
|
};
|
|
let owner_ata = super::derive_ata(&wallet, &owner_mint, token_program)
|
|
.unwrap_or_else(|error| panic!("owner ATA failed: {error}"));
|
|
let nested_ata = super::derive_ata(&owner_ata, &nested_mint, token_program)
|
|
.unwrap_or_else(|error| panic!("nested ATA failed: {error}"));
|
|
let destination = super::derive_ata(&wallet, &nested_mint, token_program)
|
|
.unwrap_or_else(|error| panic!("destination ATA failed: {error}"));
|
|
let mut cache = super::AccountCache::new();
|
|
cache.insert(
|
|
owner_ata.0,
|
|
std::option::Option::Some(token_account(token_program, &owner_mint, &wallet)),
|
|
);
|
|
cache.insert(nested_ata.0, std::option::Option::None);
|
|
cache.insert(
|
|
destination.0,
|
|
std::option::Option::Some(token_account(token_program, &nested_mint, &wallet)),
|
|
);
|
|
let report = super::evaluate_post_execution(
|
|
ks_lib::ExApiExecutionCluster::Devnet,
|
|
&operation,
|
|
std::option::Option::Some(13),
|
|
&cache,
|
|
)
|
|
.unwrap_or_else(|error| panic!("RecoverNested postcondition failed: {error}"));
|
|
assert_eq!(report.status, crate::SplAssociatedTokenAccountStatefulReadinessStatus::Ready);
|
|
}
|
|
}
|