// file: ks-pipeline/src/spl_token_stateful.rs // version: 4 //! Stateful Localnet and Devnet readiness checks for classic SPL Token operations. const MINT_LEN: usize = 82; const TOKEN_ACCOUNT_LEN: usize = 165; const MULTISIG_LEN: usize = 355; const MAX_STATE_ACCOUNTS: usize = 128; /// Stateful readiness outcome for one classic SPL Token operation. #[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum SplTokenStatefulReadinessStatus { /// Every stateful preflight check passed. Ready, /// At least one stateful preflight check failed. Blocked, } /// One machine-readable classic SPL Token stateful check. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct SplTokenStatefulCheck { /// 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 fact measured during classic SPL Token state inspection. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct SplTokenStatefulFact { /// Stable fact key. pub key: std::string::String, /// Exact string representation of the measured value. pub value: std::string::String, } /// Complete request for one classic SPL Token stateful readiness inspection. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct SplTokenStatefulReadinessRequest { /// HTTP endpoint role used for all state reads. pub query_role: std::string::String, /// Expected Localnet or Devnet cluster. pub cluster: ks_lib::ExApiExecutionCluster, /// Typed operation whose external state must be inspected. pub operation: ks_lib::ExSplClassicTokenOperation, } /// Complete classic SPL Token stateful readiness report. #[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] pub struct SplTokenStatefulReadinessReport { /// Expected cluster. pub cluster: ks_lib::ExApiExecutionCluster, /// Stable top-level operation code. pub operation_code: std::string::String, /// Aggregate readiness status. pub status: crate::SplTokenStatefulReadinessStatus, /// Highest contextual slot observed while reading accounts. pub context_slot: std::option::Option, /// Ordered stateful checks. pub checks: std::vec::Vec, /// Ordered measured facts. pub facts: std::vec::Vec, } #[derive(Clone, Debug, Eq, PartialEq)] struct MintState { mint_authority: std::option::Option, supply: u64, decimals: u8, initialized: bool, freeze_authority: std::option::Option, } #[derive(Clone, Debug, Eq, PartialEq)] struct TokenAccountState { mint: std::string::String, owner: std::string::String, amount: u64, delegate: std::option::Option, state: u8, native_reserve: std::option::Option, delegated_amount: u64, close_authority: std::option::Option, } #[derive(Clone, Debug, Eq, PartialEq)] struct MultisigState { threshold: u8, member_count: u8, initialized: bool, members: std::vec::Vec, } type AccountCache = std::collections::BTreeMap< std::string::String, std::option::Option, >; /// Inspects Localnet or Devnet Token state required before simulation. pub async fn inspect_spl_token_stateful_readiness( pool: &ks_onchain_transport::HttpEndpointPool, request: &crate::SplTokenStatefulReadinessRequest, ) -> ks_core::Result { if request.query_role.trim().is_empty() { return std::result::Result::Err(ks_core::Error::config( "SPL Token stateful readiness query_role must not be empty", )); } match request.cluster { ks_lib::ExApiExecutionCluster::Localnet | ks_lib::ExApiExecutionCluster::Devnet => {}, ks_lib::ExApiExecutionCluster::Testnet | ks_lib::ExApiExecutionCluster::Mainnet => { return std::result::Result::Err(ks_core::Error::new( "spl_token_stateful_cluster_unsupported", "SPL Token stateful readiness is restricted to Localnet and Devnet", )); }, } 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_cluster(request.cluster, &genesis) { std::result::Result::Ok(()) => {}, std::result::Result::Err(error) => return std::result::Result::Err(error), } let addresses = match operation_addresses(&request.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(MULTISIG_LEN) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut cache = AccountCache::new(); let mut highest_slot: std::option::Option = std::option::Option::None; for address in addresses { let result = pool .get_account_info_for_role(request.query_role.as_str(), &address, &config) .await; let result = match result { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; highest_slot = std::option::Option::Some(match highest_slot { std::option::Option::Some(slot) => slot.max(result.context.slot), std::option::Option::None => result.context.slot, }); cache.insert(address.0, result.account); } let mut report = crate::SplTokenStatefulReadinessReport { cluster: request.cluster, operation_code: request.operation.operation_code().to_string(), status: crate::SplTokenStatefulReadinessStatus::Ready, context_slot: highest_slot, checks: std::vec::Vec::new(), facts: std::vec![crate::SplTokenStatefulFact { key: "genesis_hash".to_string(), value: genesis.genesis_hash, }], }; let initialization_accounts = initialization_accounts(&request.operation); for (address, data_length) in initialization_accounts { let rent = pool .get_minimum_balance_for_rent_exemption_for_role( request.query_role.as_str(), data_length as u64, &ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(), ) .await; let rent = match rent { std::result::Result::Ok(value) => value.minimum_balance_lamports, std::result::Result::Err(error) => return std::result::Result::Err(error), }; push_fact(&mut report, "rent_exempt_minimum_lamports", format!("{}:{rent}", address.0)); let lamports = cache .get(address.0.as_str()) .and_then(|value| return value.as_ref()) .map(|value| return value.lamports); push_check( &mut report, "rent_exempt", lamports.is_some_and(|value| return value >= rent), format!("initialization account {} must be rent exempt", address.0), ); } let withdraw_accounts = withdraw_excess_accounts(&request.operation); for address in withdraw_accounts { let account = cache.get(address.0.as_str()).and_then(|value| return value.as_ref()).cloned(); if let std::option::Option::Some(account) = account { let rent = pool .get_minimum_balance_for_rent_exemption_for_role( request.query_role.as_str(), account.space, &ks_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(), ) .await; let rent = match rent { std::result::Result::Ok(value) => value.minimum_balance_lamports, std::result::Result::Err(error) => return std::result::Result::Err(error), }; push_fact( &mut report, "withdraw_rent_exempt_minimum_lamports", format!("{}:{rent}", address.0), ); push_check( &mut report, "excess_lamports_present", account.lamports > rent, format!("source account {} must contain lamports above rent exemption", address.0), ); } } match &request.operation { ks_lib::ExSplClassicTokenOperation::Instruction { value } => { inspect_single(&mut report, &cache, value); }, ks_lib::ExSplClassicTokenOperation::Batch { instructions } => { for (index, value) in instructions.iter().enumerate() { push_fact( &mut report, "batch_child", format!("{index}:{}", value.operation_code()), ); inspect_single(&mut report, &cache, value); } }, } report.status = if report.checks.iter().all(|check| return check.passed) { crate::SplTokenStatefulReadinessStatus::Ready } else { crate::SplTokenStatefulReadinessStatus::Blocked }; return std::result::Result::Ok(report); } fn validate_cluster( 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(()) } else { std::result::Result::Err(ks_core::Error::new( "spl_token_stateful_devnet_genesis_mismatch", "SPL Token Devnet readiness requires the official Devnet genesis hash", )) } }, ks_lib::ExApiExecutionCluster::Localnet => { if genesis.classified_cluster.is_none() { std::result::Result::Ok(()) } else { std::result::Result::Err(ks_core::Error::new( "spl_token_stateful_localnet_public_cluster", "Localnet readiness refuses endpoints classified as a public cluster", )) } }, ks_lib::ExApiExecutionCluster::Testnet | ks_lib::ExApiExecutionCluster::Mainnet => { std::result::Result::Err(ks_core::Error::new( "spl_token_stateful_cluster_unsupported", "SPL Token stateful readiness is restricted to Localnet and Devnet", )) }, }; } fn operation_addresses( operation: &ks_lib::ExSplClassicTokenOperation, ) -> ks_core::Result> { let mut addresses = std::vec::Vec::new(); match operation { ks_lib::ExSplClassicTokenOperation::Instruction { value } => { collect_single_addresses(&mut addresses, value); }, ks_lib::ExSplClassicTokenOperation::Batch { instructions } => { for value in instructions { collect_single_addresses(&mut addresses, value); } }, } if addresses.len() > MAX_STATE_ACCOUNTS { return std::result::Result::Err(ks_core::Error::new( "spl_token_stateful_account_limit", format!("SPL Token stateful readiness exceeds {MAX_STATE_ACCOUNTS} unique accounts"), )); } return std::result::Result::Ok(addresses); } fn initialization_accounts( operation: &ks_lib::ExSplClassicTokenOperation, ) -> std::vec::Vec<(ks_lib::MdPubkey, usize)> { let mut values = std::vec::Vec::new(); let instructions: std::vec::Vec<&ks_lib::ExSplClassicTokenSingleOperation> = match operation { ks_lib::ExSplClassicTokenOperation::Instruction { value } => { std::vec![value] }, ks_lib::ExSplClassicTokenOperation::Batch { instructions } => instructions.iter().collect(), }; for operation in instructions { let candidate = match operation { ks_lib::ExSplClassicTokenSingleOperation::InitializeMint { mint, .. } => { std::option::Option::Some((mint.clone(), MINT_LEN)) }, ks_lib::ExSplClassicTokenSingleOperation::InitializeAccount { account, .. } => { std::option::Option::Some((account.clone(), TOKEN_ACCOUNT_LEN)) }, ks_lib::ExSplClassicTokenSingleOperation::InitializeMultisig { multisig, .. } => { std::option::Option::Some((multisig.clone(), MULTISIG_LEN)) }, _ => std::option::Option::None, }; if let std::option::Option::Some(candidate) = candidate { if !values .iter() .any(|(address, length)| return address == &candidate.0 && *length == candidate.1) { values.push(candidate); } } } return values; } fn withdraw_excess_accounts( operation: &ks_lib::ExSplClassicTokenOperation, ) -> std::vec::Vec { let instructions: std::vec::Vec<&ks_lib::ExSplClassicTokenSingleOperation> = match operation { ks_lib::ExSplClassicTokenOperation::Instruction { value } => { std::vec![value] }, ks_lib::ExSplClassicTokenOperation::Batch { instructions } => instructions.iter().collect(), }; let mut values = std::vec::Vec::new(); for operation in instructions { if let ks_lib::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { account, .. } = operation { if !values.iter().any(|value| return value == account) { values.push(account.clone()); } } } return values; } fn add_address(addresses: &mut std::vec::Vec, value: &ks_lib::MdPubkey) { if !addresses.iter().any(|existing| return existing == value) { addresses.push(value.clone()); } } fn add_authority( addresses: &mut std::vec::Vec, authority: &ks_lib::ExSplClassicTokenAuthority, ) { if !authority.multisig_signers.is_empty() { add_address(addresses, &authority.authority); } } fn collect_single_addresses( addresses: &mut std::vec::Vec, operation: &ks_lib::ExSplClassicTokenSingleOperation, ) { match operation { ks_lib::ExSplClassicTokenSingleOperation::InitializeMint { mint, .. } | ks_lib::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint } | ks_lib::ExSplClassicTokenSingleOperation::AmountToUiAmount { mint, .. } | ks_lib::ExSplClassicTokenSingleOperation::UiAmountToAmount { mint, .. } => { add_address(addresses, mint); }, ks_lib::ExSplClassicTokenSingleOperation::InitializeAccount { account, mint, .. } => { add_address(addresses, account); add_address(addresses, mint); }, ks_lib::ExSplClassicTokenSingleOperation::InitializeMultisig { multisig, .. } => { add_address(addresses, multisig); }, ks_lib::ExSplClassicTokenSingleOperation::Transfer { source, destination, authority, .. } => { add_address(addresses, source); add_address(addresses, destination); add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::Approve { source, authority, .. } | ks_lib::ExSplClassicTokenSingleOperation::Revoke { source, authority } => { add_address(addresses, source); add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::SetAuthority { owned, current_authority, .. } => { add_address(addresses, owned); add_authority(addresses, current_authority); }, ks_lib::ExSplClassicTokenSingleOperation::MintTo { mint, destination, authority, .. } | ks_lib::ExSplClassicTokenSingleOperation::MintToChecked { mint, destination, authority, .. } => { add_address(addresses, mint); add_address(addresses, destination); add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::Burn { source, mint, authority, .. } | ks_lib::ExSplClassicTokenSingleOperation::BurnChecked { source, mint, authority, .. } => { add_address(addresses, source); add_address(addresses, mint); add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::CloseAccount { account, authority, .. } | ks_lib::ExSplClassicTokenSingleOperation::UnwrapLamports { account, authority, .. } | ks_lib::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { account, authority, .. } => { add_address(addresses, account); add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::FreezeAccount { account, mint, authority } | ks_lib::ExSplClassicTokenSingleOperation::ThawAccount { account, mint, authority } => { add_address(addresses, account); add_address(addresses, mint); add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { source, mint, destination, authority, .. } | ks_lib::ExSplClassicTokenSingleOperation::ApproveChecked { source, mint, delegate: destination, authority, .. } => { add_address(addresses, source); add_address(addresses, mint); if matches!(operation, ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { .. }) { add_address(addresses, destination); } add_authority(addresses, authority); }, ks_lib::ExSplClassicTokenSingleOperation::SyncNative { account, .. } | ks_lib::ExSplClassicTokenSingleOperation::InitializeImmutableOwner { account } => { add_address(addresses, account); }, } } fn inspect_single( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, operation: &ks_lib::ExSplClassicTokenSingleOperation, ) { match operation { ks_lib::ExSplClassicTokenSingleOperation::InitializeMint { mint, .. } => { let state = mint_state(report, cache, mint); if let std::option::Option::Some(state) = state { push_check( report, "mint_uninitialized", !state.initialized, "mint must be uninitialized before InitializeMint2", ); } }, ks_lib::ExSplClassicTokenSingleOperation::InitializeAccount { account, mint, .. } => { let state = token_state(report, cache, account); if let std::option::Option::Some(state) = state { push_check( report, "account_uninitialized", state.state == 0, "token account must be uninitialized before InitializeAccount3", ); } let mint_state = mint_state(report, cache, mint); check_mint_initialized(report, mint_state.as_ref()); }, ks_lib::ExSplClassicTokenSingleOperation::InitializeMultisig { multisig, .. } => { let state = multisig_state(report, cache, multisig); if let std::option::Option::Some(state) = state { push_check( report, "multisig_uninitialized", !state.initialized, "multisig must be uninitialized before InitializeMultisig2", ); } }, ks_lib::ExSplClassicTokenSingleOperation::Transfer { source, destination, authority, amount, } => { inspect_transfer( report, cache, source, destination, std::option::Option::None, authority, amount, std::option::Option::None, ); }, ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { source, mint, destination, authority, amount, decimals, } => { inspect_transfer( report, cache, source, destination, std::option::Option::Some(mint), authority, amount, std::option::Option::Some(*decimals), ); }, ks_lib::ExSplClassicTokenSingleOperation::Approve { source, authority, amount, .. } => { inspect_owner_amount(report, cache, source, authority, amount, false); }, ks_lib::ExSplClassicTokenSingleOperation::ApproveChecked { source, mint, authority, amount, decimals, .. } => { inspect_owner_amount(report, cache, source, authority, amount, false); inspect_checked_mint(report, cache, source, mint, *decimals); }, ks_lib::ExSplClassicTokenSingleOperation::Revoke { source, authority } => { let state = token_state(report, cache, source); if let std::option::Option::Some(state) = state { check_authority( report, cache, authority, std::option::Option::Some(state.owner.as_str()), ); push_check( report, "delegate_present", state.delegate.is_some(), "revoke requires an existing delegate", ); } }, ks_lib::ExSplClassicTokenSingleOperation::MintTo { mint, destination, authority, amount, } | ks_lib::ExSplClassicTokenSingleOperation::MintToChecked { mint, destination, authority, amount, .. } => { let mint_value = mint_state(report, cache, mint); let destination_value = token_state(report, cache, destination); check_mint_initialized(report, mint_value.as_ref()); if let ( std::option::Option::Some(mint_value), std::option::Option::Some(destination_value), ) = (mint_value, destination_value) { push_check( report, "destination_mint_matches", destination_value.mint == mint.0, "destination token account must use the mint", ); check_authority(report, cache, authority, mint_value.mint_authority.as_deref()); let parsed = amount_value(report, amount); if let std::option::Option::Some(parsed) = parsed { push_check( report, "mint_supply_no_overflow", mint_value.supply.checked_add(parsed).is_some(), "mint supply must not overflow u64", ); } } if let ks_lib::ExSplClassicTokenSingleOperation::MintToChecked { decimals, .. } = operation { if let std::option::Option::Some(mint_value) = mint_state_quiet(cache, mint) { push_check( report, "mint_decimals_match", mint_value.decimals == *decimals, "checked decimals must match mint state", ); } } }, ks_lib::ExSplClassicTokenSingleOperation::Burn { source, mint, authority, amount } | ks_lib::ExSplClassicTokenSingleOperation::BurnChecked { source, mint, authority, amount, .. } => { inspect_owner_amount(report, cache, source, authority, amount, true); inspect_checked_relation(report, cache, source, mint); if let (std::option::Option::Some(mint_state), std::option::Option::Some(parsed)) = (mint_state_quiet(cache, mint), amount.0.parse::().ok()) { push_check( report, "mint_supply_sufficient", mint_state.supply >= parsed, "mint supply must cover burned amount", ); } if let ks_lib::ExSplClassicTokenSingleOperation::BurnChecked { decimals, .. } = operation { inspect_checked_mint(report, cache, source, mint, *decimals); } }, ks_lib::ExSplClassicTokenSingleOperation::CloseAccount { account, authority, .. } => { let state = token_state(report, cache, account); if let std::option::Option::Some(state) = state { let expected = match state.close_authority.as_deref() { std::option::Option::Some(value) => value, std::option::Option::None => state.owner.as_str(), }; check_authority(report, cache, authority, std::option::Option::Some(expected)); push_check( report, "close_balance_allowed", state.native_reserve.is_some() || state.amount == 0, "non-native token account must have zero token balance before close", ); } }, ks_lib::ExSplClassicTokenSingleOperation::FreezeAccount { account, mint, authority } => { inspect_freeze_thaw(report, cache, account, mint, authority, 1); }, ks_lib::ExSplClassicTokenSingleOperation::ThawAccount { account, mint, authority } => { inspect_freeze_thaw(report, cache, account, mint, authority, 2); }, ks_lib::ExSplClassicTokenSingleOperation::SyncNative { account, .. } => { let state = token_state(report, cache, account); if let std::option::Option::Some(state) = state { push_check( report, "native_account", state.native_reserve.is_some(), "SyncNative requires a wrapped-SOL token account", ); } }, ks_lib::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint } | ks_lib::ExSplClassicTokenSingleOperation::AmountToUiAmount { mint, .. } | ks_lib::ExSplClassicTokenSingleOperation::UiAmountToAmount { mint, .. } => { let state = mint_state(report, cache, mint); check_mint_initialized(report, state.as_ref()); }, ks_lib::ExSplClassicTokenSingleOperation::InitializeImmutableOwner { account } => { let state = token_state(report, cache, account); if let std::option::Option::Some(state) = state { push_check( report, "account_uninitialized", state.state == 0, "classic immutable-owner compatibility no-op precedes account initialization", ); } }, ks_lib::ExSplClassicTokenSingleOperation::UnwrapLamports { account, authority, amount_lamports, .. } => { let state = token_state(report, cache, account); if let std::option::Option::Some(state) = state { push_check( report, "native_account", state.native_reserve.is_some(), "UnwrapLamports requires a wrapped-SOL token account", ); let expected = match state.close_authority.as_deref() { std::option::Option::Some(value) => value, std::option::Option::None => state.owner.as_str(), }; check_authority(report, cache, authority, std::option::Option::Some(expected)); if let std::option::Option::Some(amount) = amount_lamports { if let std::option::Option::Some(parsed) = amount_value(report, amount) { push_check( report, "unwrap_balance_sufficient", state.amount >= parsed, "wrapped-SOL balance must cover requested unwrap amount", ); } } } }, ks_lib::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { account, authority, .. } => { let value = account_value(report, cache, account, std::option::Option::None); if let std::option::Option::Some(value) = value { match value.data.len() { MINT_LEN => { if let std::result::Result::Ok(state) = parse_mint(value.data.as_slice()) { check_authority( report, cache, authority, state.mint_authority.as_deref(), ); } }, TOKEN_ACCOUNT_LEN => { if let std::result::Result::Ok(state) = parse_token_account(value.data.as_slice()) { check_authority( report, cache, authority, std::option::Option::Some(state.owner.as_str()), ); } }, MULTISIG_LEN => { check_authority( report, cache, authority, std::option::Option::Some(account.0.as_str()), ); }, _ => {}, } } }, ks_lib::ExSplClassicTokenSingleOperation::SetAuthority { owned, authority_type, new_authority, current_authority, } => { inspect_set_authority( report, cache, owned, *authority_type, new_authority, current_authority, ); }, } } fn inspect_transfer( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, source: &ks_lib::MdPubkey, destination: &ks_lib::MdPubkey, explicit_mint: std::option::Option<&ks_lib::MdPubkey>, authority: &ks_lib::ExSplClassicTokenAuthority, amount: &ks_lib::ExSplClassicTokenAmount, decimals: std::option::Option, ) { let source_state = token_state(report, cache, source); let destination_state = token_state(report, cache, destination); if let (std::option::Option::Some(source_state), std::option::Option::Some(destination_state)) = (source_state, destination_state) { push_check( report, "transfer_mint_matches", source_state.mint == destination_state.mint, "source and destination token accounts must share a mint", ); push_check( report, "source_not_frozen", source_state.state == 1, "source token account must be initialized and unfrozen", ); push_check( report, "destination_not_frozen", destination_state.state == 1, "destination token account must be initialized and unfrozen", ); if let std::option::Option::Some(parsed) = amount_value(report, amount) { push_check( report, "source_balance_sufficient", source_state.amount >= parsed, "source token balance must cover transfer amount", ); if source_state.delegate.as_deref() == std::option::Option::Some(authority.authority.0.as_str()) { push_check( report, "delegate_allowance_sufficient", source_state.delegated_amount >= parsed, "delegate allowance must cover transfer amount", ); } } check_token_authority(report, cache, authority, &source_state); if let std::option::Option::Some(mint) = explicit_mint { push_check( report, "explicit_mint_matches", source_state.mint == mint.0, "checked mint must match source account state", ); if let std::option::Option::Some(decimals) = decimals { if let std::option::Option::Some(mint_state) = mint_state(report, cache, mint) { push_check( report, "mint_decimals_match", mint_state.decimals == decimals, "checked decimals must match mint state", ); } } } } } fn inspect_owner_amount( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, source: &ks_lib::MdPubkey, authority: &ks_lib::ExSplClassicTokenAuthority, amount: &ks_lib::ExSplClassicTokenAmount, require_balance: bool, ) { let state = token_state(report, cache, source); if let std::option::Option::Some(state) = state { push_check( report, "source_not_frozen", state.state == 1, "source token account must be initialized and unfrozen", ); check_token_authority(report, cache, authority, &state); if let std::option::Option::Some(parsed) = amount_value(report, amount) { if require_balance { push_check( report, "source_balance_sufficient", state.amount >= parsed, "source token balance must cover requested amount", ); } if state.delegate.as_deref() == std::option::Option::Some(authority.authority.0.as_str()) { push_check( report, "delegate_allowance_sufficient", state.delegated_amount >= parsed, "delegate allowance must cover requested amount", ); } } } } fn inspect_checked_relation( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, source: &ks_lib::MdPubkey, mint: &ks_lib::MdPubkey, ) { if let std::option::Option::Some(state) = token_state_quiet(cache, source) { push_check( report, "source_mint_matches", state.mint == mint.0, "source token account must use the explicit mint", ); } let mint_state = mint_state(report, cache, mint); check_mint_initialized(report, mint_state.as_ref()); } fn inspect_checked_mint( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, source: &ks_lib::MdPubkey, mint: &ks_lib::MdPubkey, decimals: u8, ) { inspect_checked_relation(report, cache, source, mint); if let std::option::Option::Some(state) = mint_state_quiet(cache, mint) { push_check( report, "mint_decimals_match", state.decimals == decimals, "checked decimals must match mint state", ); } } fn inspect_freeze_thaw( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, account: &ks_lib::MdPubkey, mint: &ks_lib::MdPubkey, authority: &ks_lib::ExSplClassicTokenAuthority, required_state: u8, ) { let account_state = token_state(report, cache, account); let mint_state = mint_state(report, cache, mint); if let std::option::Option::Some(account_state) = account_state { push_check( report, "account_mint_matches", account_state.mint == mint.0, "token account must use the supplied mint", ); push_check( report, "account_state_matches", account_state.state == required_state, "token account state must match freeze or thaw precondition", ); } if let std::option::Option::Some(mint_state) = mint_state { check_authority(report, cache, authority, mint_state.freeze_authority.as_deref()); } } fn inspect_set_authority( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, owned: &ks_lib::MdPubkey, authority_type: ks_lib::ExSplClassicTokenAuthorityType, new_authority: &std::option::Option, current: &ks_lib::ExSplClassicTokenAuthority, ) { let value = account_value(report, cache, owned, std::option::Option::None); if let std::option::Option::Some(value) = value { match authority_type { ks_lib::ExSplClassicTokenAuthorityType::MintTokens | ks_lib::ExSplClassicTokenAuthorityType::FreezeAccount => { if let std::result::Result::Ok(state) = parse_mint(value.data.as_slice()) { let expected = if authority_type == ks_lib::ExSplClassicTokenAuthorityType::MintTokens { state.mint_authority.as_deref() } else { state.freeze_authority.as_deref() }; check_authority(report, cache, current, expected); } else { push_check( report, "authority_domain_layout", false, "mint authority domains require a Mint account", ); } }, ks_lib::ExSplClassicTokenAuthorityType::AccountOwner | ks_lib::ExSplClassicTokenAuthorityType::CloseAccount => { if let std::result::Result::Ok(state) = parse_token_account(value.data.as_slice()) { let close_authority = match state.close_authority.as_deref() { std::option::Option::Some(value) => value, std::option::Option::None => state.owner.as_str(), }; let expected = if authority_type == ks_lib::ExSplClassicTokenAuthorityType::AccountOwner { std::option::Option::Some(state.owner.as_str()) } else { std::option::Option::Some(close_authority) }; check_authority(report, cache, current, expected); if authority_type == ks_lib::ExSplClassicTokenAuthorityType::AccountOwner { push_check( report, "account_owner_not_revoked", new_authority.is_some(), "token-account owner cannot be revoked", ); } } else { push_check( report, "authority_domain_layout", false, "account authority domains require a token Account", ); } }, } } } fn check_token_authority( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, authority: &ks_lib::ExSplClassicTokenAuthority, account: &TokenAccountState, ) { let supplied = authority.authority.0.as_str(); let expected = if account.delegate.as_deref() == std::option::Option::Some(supplied) { account.delegate.as_deref() } else { std::option::Option::Some(account.owner.as_str()) }; check_authority(report, cache, authority, expected); } fn check_authority( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, authority: &ks_lib::ExSplClassicTokenAuthority, expected: std::option::Option<&str>, ) { push_check( report, "authority_present", expected.is_some(), "required authority must not be revoked", ); if let std::option::Option::Some(expected) = expected { push_check( report, "authority_matches", authority.authority.0 == expected, "declared authority must match on-chain state", ); } if authority.multisig_signers.is_empty() { return; } let state = multisig_state(report, cache, &authority.authority); if let std::option::Option::Some(state) = state { push_check( report, "multisig_initialized", state.initialized, "multisig authority must be initialized", ); push_check( report, "multisig_shape", state.threshold >= 1 && state.threshold <= state.member_count && state.member_count <= 11, "multisig M/N must satisfy 1 <= M <= N <= 11", ); let mut unique = std::collections::BTreeSet::new(); for signer in &authority.multisig_signers { if state.members.iter().any(|member| return member == &signer.0) { unique.insert(signer.0.clone()); } } push_check( report, "multisig_threshold", unique.len() >= usize::from(state.threshold), "distinct supplied member signers must meet the multisig threshold", ); } } fn check_mint_initialized( report: &mut crate::SplTokenStatefulReadinessReport, state: std::option::Option<&MintState>, ) { if let std::option::Option::Some(state) = state { push_check(report, "mint_initialized", state.initialized, "mint must be initialized"); } } fn account_value<'a>( report: &mut crate::SplTokenStatefulReadinessReport, cache: &'a AccountCache, address: &ks_lib::MdPubkey, expected_len: std::option::Option, ) -> std::option::Option<&'a ks_onchain_transport::AccountInfoValue> { let value = cache.get(address.0.as_str()).and_then(|value| return value.as_ref()); push_check( report, "account_exists", value.is_some(), format!("state account {} must exist", address.0), ); if let std::option::Option::Some(value) = value { push_check( report, "classic_token_owner", value.owner.0 == ks_program_ids::SPL_TOKEN_PROGRAM_ID, format!("state account {} must be owned by classic SPL Token", address.0), ); if let std::option::Option::Some(expected_len) = expected_len { push_check( report, "account_layout", value.data.len() == expected_len && value.space == expected_len as u64, format!( "state account {} must use the exact {expected_len}-byte layout", address.0 ), ); } } return value; } fn mint_state( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, address: &ks_lib::MdPubkey, ) -> std::option::Option { let value = account_value(report, cache, address, std::option::Option::Some(MINT_LEN)); return match value { std::option::Option::Some(value) => match parse_mint(value.data.as_slice()) { std::result::Result::Ok(state) => { push_fact(report, "mint_decimals", state.decimals); push_fact(report, "mint_supply", state.supply); std::option::Option::Some(state) }, std::result::Result::Err(message) => { push_check(report, "mint_layout_valid", false, message); std::option::Option::None }, }, std::option::Option::None => std::option::Option::None, }; } fn mint_state_quiet( cache: &AccountCache, address: &ks_lib::MdPubkey, ) -> std::option::Option { return cache .get(address.0.as_str()) .and_then(|value| return value.as_ref()) .and_then(|value| return parse_mint(value.data.as_slice()).ok()); } fn token_state( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, address: &ks_lib::MdPubkey, ) -> std::option::Option { let value = account_value(report, cache, address, std::option::Option::Some(TOKEN_ACCOUNT_LEN)); return match value { std::option::Option::Some(value) => match parse_token_account(value.data.as_slice()) { std::result::Result::Ok(state) => { push_fact(report, "token_amount", state.amount); push_fact(report, "token_mint", state.mint.as_str()); std::option::Option::Some(state) }, std::result::Result::Err(message) => { push_check(report, "token_account_layout_valid", false, message); std::option::Option::None }, }, std::option::Option::None => std::option::Option::None, }; } fn token_state_quiet( cache: &AccountCache, address: &ks_lib::MdPubkey, ) -> std::option::Option { return cache.get(address.0.as_str()).and_then(|value| return value.as_ref()).and_then( |value| { return parse_token_account(value.data.as_slice()).ok(); }, ); } fn multisig_state( report: &mut crate::SplTokenStatefulReadinessReport, cache: &AccountCache, address: &ks_lib::MdPubkey, ) -> std::option::Option { let value = account_value(report, cache, address, std::option::Option::Some(MULTISIG_LEN)); return match value { std::option::Option::Some(value) => match parse_multisig(value.data.as_slice()) { std::result::Result::Ok(state) => std::option::Option::Some(state), std::result::Result::Err(message) => { push_check(report, "multisig_layout_valid", false, message); std::option::Option::None }, }, std::option::Option::None => std::option::Option::None, }; } fn amount_value( report: &mut crate::SplTokenStatefulReadinessReport, amount: &ks_lib::ExSplClassicTokenAmount, ) -> std::option::Option { return match amount.0.parse::() { std::result::Result::Ok(value) => std::option::Option::Some(value), std::result::Result::Err(_) => { push_check(report, "amount_valid", false, "raw token amount must be a canonical u64"); std::option::Option::None }, }; } fn push_check( report: &mut crate::SplTokenStatefulReadinessReport, code: &str, passed: bool, message: impl std::convert::Into, ) { report.checks.push(crate::SplTokenStatefulCheck { code: code.to_string(), passed, message: message.into(), }); } fn push_fact( report: &mut crate::SplTokenStatefulReadinessReport, key: &str, value: impl std::fmt::Display, ) { report.facts.push(crate::SplTokenStatefulFact { key: key.to_string(), value: value.to_string(), }); } fn parse_mint(bytes: &[u8]) -> std::result::Result { if bytes.len() != MINT_LEN { return std::result::Result::Err("Mint data must contain exactly 82 bytes"); } let mint_authority = match parse_coption_pubkey(&bytes[0..36]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let supply = match read_u64(&bytes[36..44]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let initialized = match bytes[45] { 0 => false, 1 => true, _ => return std::result::Result::Err("Mint initialized flag must be 0 or 1"), }; let freeze_authority = match parse_coption_pubkey(&bytes[46..82]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; return std::result::Result::Ok(MintState { mint_authority, supply, decimals: bytes[44], initialized, freeze_authority, }); } fn parse_token_account(bytes: &[u8]) -> std::result::Result { if bytes.len() != TOKEN_ACCOUNT_LEN { return std::result::Result::Err("Token Account data must contain exactly 165 bytes"); } if bytes[108] > 2 { return std::result::Result::Err( "Token Account state must be Uninitialized, Initialized or Frozen", ); } let amount = match read_u64(&bytes[64..72]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let delegate = match parse_coption_pubkey(&bytes[72..108]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let native_reserve = match parse_coption_u64(&bytes[109..121]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let delegated_amount = match read_u64(&bytes[121..129]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; let close_authority = match parse_coption_pubkey(&bytes[129..165]) { std::result::Result::Ok(value) => value, std::result::Result::Err(message) => return std::result::Result::Err(message), }; return std::result::Result::Ok(TokenAccountState { mint: bs58::encode(&bytes[0..32]).into_string(), owner: bs58::encode(&bytes[32..64]).into_string(), amount, delegate, state: bytes[108], native_reserve, delegated_amount, close_authority, }); } fn parse_multisig(bytes: &[u8]) -> std::result::Result { if bytes.len() != MULTISIG_LEN { return std::result::Result::Err("Multisig data must contain exactly 355 bytes"); } let initialized = match bytes[2] { 0 => false, 1 => true, _ => return std::result::Result::Err("Multisig initialized flag must be 0 or 1"), }; let count = usize::from(bytes[1].min(11)); let mut members = std::vec::Vec::new(); for index in 0..count { let start = 3 + index * 32; members.push(bs58::encode(&bytes[start..start + 32]).into_string()); } return std::result::Result::Ok(MultisigState { threshold: bytes[0], member_count: bytes[1], initialized, members, }); } fn parse_coption_pubkey( bytes: &[u8], ) -> std::result::Result, &'static str> { if bytes.len() != 36 { return std::result::Result::Err("COption must contain exactly 36 bytes"); } return match &bytes[0..4] { [0, 0, 0, 0] => std::result::Result::Ok(std::option::Option::None), [1, 0, 0, 0] => std::result::Result::Ok(std::option::Option::Some( bs58::encode(&bytes[4..36]).into_string(), )), _ => std::result::Result::Err("COption tag must be 0 or 1"), }; } fn parse_coption_u64(bytes: &[u8]) -> std::result::Result, &'static str> { if bytes.len() != 12 { return std::result::Result::Err("COption must contain exactly 12 bytes"); } return match &bytes[0..4] { [0, 0, 0, 0] => std::result::Result::Ok(std::option::Option::None), [1, 0, 0, 0] => match read_u64(&bytes[4..12]) { std::result::Result::Ok(value) => { std::result::Result::Ok(std::option::Option::Some(value)) }, std::result::Result::Err(message) => std::result::Result::Err(message), }, _ => std::result::Result::Err("COption tag must be 0 or 1"), }; } fn read_u64(bytes: &[u8]) -> std::result::Result { let array = match <[u8; 8]>::try_from(bytes) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err("u64 field must contain exactly 8 bytes"); }, }; return std::result::Result::Ok(u64::from_le_bytes(array)); } #[cfg(test)] mod tests { fn empty_report() -> crate::SplTokenStatefulReadinessReport { return crate::SplTokenStatefulReadinessReport { cluster: ks_lib::ExApiExecutionCluster::Devnet, operation_code: ks_lib::EX_SPL_TOKEN_TRANSFER_CHECKED_OPERATION.to_string(), status: crate::SplTokenStatefulReadinessStatus::Ready, context_slot: std::option::Option::Some(1), checks: std::vec::Vec::new(), facts: std::vec::Vec::new(), }; } fn account_info(data: std::vec::Vec) -> ks_onchain_transport::AccountInfoValue { return ks_onchain_transport::AccountInfoValue { lamports: 10_000_000, owner: ks_lib::MdProgramId(ks_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), executable: false, rent_epoch: 0, space: data.len() as u64, data, }; } fn local_devnet_profile() -> ks_config::ProfileConfig { let config = match ks_config::parse_config_json(include_str!("../../config/example.config.json")) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("example config parse failed: {error}"), }; for profile in config.profiles { if profile.name == "local_devnet" { return profile; } } panic!("local_devnet profile missing"); } #[test] fn exact_mint_account_and_multisig_layouts_are_parsed() { let mut mint = [0_u8; super::MINT_LEN]; mint[0..4].copy_from_slice(&1_u32.to_le_bytes()); mint[4..36].copy_from_slice(&[1_u8; 32]); mint[36..44].copy_from_slice(&u64::MAX.to_le_bytes()); mint[44] = 9; mint[45] = 1; let parsed_mint = match super::parse_mint(&mint) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Mint parse failed: {error}"), }; assert_eq!(parsed_mint.supply, u64::MAX); assert_eq!(parsed_mint.decimals, 9); assert!(parsed_mint.initialized); let mut account = [0_u8; super::TOKEN_ACCOUNT_LEN]; account[0..32].copy_from_slice(&[2_u8; 32]); account[32..64].copy_from_slice(&[3_u8; 32]); account[64..72].copy_from_slice(&42_u64.to_le_bytes()); account[108] = 1; account[109..113].copy_from_slice(&1_u32.to_le_bytes()); account[113..121].copy_from_slice(&2_039_280_u64.to_le_bytes()); let parsed_account = match super::parse_token_account(&account) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Account parse failed: {error}"), }; assert_eq!(parsed_account.amount, 42); assert_eq!(parsed_account.native_reserve, std::option::Option::Some(2_039_280)); let mut multisig = [0_u8; super::MULTISIG_LEN]; multisig[0] = 2; multisig[1] = 3; multisig[2] = 1; multisig[3..35].copy_from_slice(&[4_u8; 32]); multisig[35..67].copy_from_slice(&[5_u8; 32]); multisig[67..99].copy_from_slice(&[6_u8; 32]); let parsed_multisig = match super::parse_multisig(&multisig) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Multisig parse failed: {error}"), }; assert_eq!(parsed_multisig.threshold, 2); assert_eq!(parsed_multisig.members.len(), 3); } #[test] fn malformed_layouts_and_option_tags_fail_closed() { assert!(super::parse_mint(&[0_u8; 81]).is_err()); let mut account = [0_u8; super::TOKEN_ACCOUNT_LEN]; account[108] = 3; assert!(super::parse_token_account(&account).is_err()); assert!(super::parse_coption_pubkey(&[2_u8; 36]).is_err()); assert!(super::parse_coption_u64(&[2_u8; 12]).is_err()); } #[test] fn checked_transfer_state_checks_exact_mint_decimals_balance_and_owner() { let mint_key = ks_lib::MdPubkey(bs58::encode([2_u8; 32]).into_string()); let owner_key = ks_lib::MdPubkey(bs58::encode([3_u8; 32]).into_string()); let source_key = ks_lib::MdPubkey(bs58::encode([7_u8; 32]).into_string()); let destination_key = ks_lib::MdPubkey(bs58::encode([8_u8; 32]).into_string()); let mut mint = [0_u8; super::MINT_LEN]; mint[44] = 9; mint[45] = 1; let mut source = [0_u8; super::TOKEN_ACCOUNT_LEN]; source[0..32].copy_from_slice(&[2_u8; 32]); source[32..64].copy_from_slice(&[3_u8; 32]); source[64..72].copy_from_slice(&42_u64.to_le_bytes()); source[108] = 1; let mut destination = [0_u8; super::TOKEN_ACCOUNT_LEN]; destination[0..32].copy_from_slice(&[2_u8; 32]); destination[32..64].copy_from_slice(&[9_u8; 32]); destination[108] = 1; let mut cache = super::AccountCache::new(); cache.insert(mint_key.0.clone(), std::option::Option::Some(account_info(mint.to_vec()))); cache .insert(source_key.0.clone(), std::option::Option::Some(account_info(source.to_vec()))); cache.insert( destination_key.0.clone(), std::option::Option::Some(account_info(destination.to_vec())), ); let mut report = empty_report(); super::inspect_single( &mut report, &cache, &ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { source: source_key, mint: mint_key, destination: destination_key, authority: ks_lib::ExSplClassicTokenAuthority { authority: owner_key, multisig_signers: std::vec::Vec::new(), }, amount: ks_lib::ExSplClassicTokenAmount("42".to_string()), decimals: 9, }, ); assert!(report.checks.iter().all(|check| return check.passed)); assert!(report.checks.iter().any(|check| return check.code == "mint_decimals_match")); assert!( report .checks .iter() .any(|check| return check.code == "source_balance_sufficient") ); } #[test] fn multisig_members_are_deduplicated_before_threshold_validation() { let multisig_key = ks_lib::MdPubkey(bs58::encode([10_u8; 32]).into_string()); let member_one = ks_lib::MdPubkey(bs58::encode([11_u8; 32]).into_string()); let member_two = ks_lib::MdPubkey(bs58::encode([12_u8; 32]).into_string()); let mut multisig = [0_u8; super::MULTISIG_LEN]; multisig[0] = 2; multisig[1] = 2; multisig[2] = 1; multisig[3..35].copy_from_slice(&[11_u8; 32]); multisig[35..67].copy_from_slice(&[12_u8; 32]); let mut cache = super::AccountCache::new(); cache.insert( multisig_key.0.clone(), std::option::Option::Some(account_info(multisig.to_vec())), ); let mut report = empty_report(); super::check_authority( &mut report, &cache, &ks_lib::ExSplClassicTokenAuthority { authority: multisig_key.clone(), multisig_signers: std::vec![member_one.clone(), member_one, member_two,], }, std::option::Option::Some(multisig_key.0.as_str()), ); assert!(report.checks.iter().all(|check| return check.passed)); assert!(report.checks.iter().any(|check| return check.code == "multisig_threshold")); } #[test] fn operation_account_collection_is_bounded_and_deduplicated() { let key = ks_lib::MdPubkey(ks_program_ids::SYSTEM_PROGRAM_ID.to_string()); let operation = ks_lib::ExSplClassicTokenOperation::Instruction { value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { source: key.clone(), mint: key.clone(), destination: key.clone(), authority: ks_lib::ExSplClassicTokenAuthority { authority: key.clone(), multisig_signers: std::vec::Vec::new(), }, amount: ks_lib::ExSplClassicTokenAmount("1".to_string()), decimals: 9, }, }; let addresses = match super::operation_addresses(&operation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("address collection failed: {error}"), }; assert_eq!(addresses, std::vec![key]); } #[tokio::test] async fn optional_devnet_checked_transfer_readiness_from_env() { if std::env::var("KS_DEVNET_SPL_TOKEN_PREFLIGHT_TEST").ok().as_deref() != std::option::Option::Some("1") { return; } let source = match std::env::var("KS_DEVNET_SPL_TOKEN_SOURCE") { std::result::Result::Ok(value) => ks_lib::MdPubkey(value), std::result::Result::Err(error) => { panic!("KS_DEVNET_SPL_TOKEN_SOURCE is required: {error}"); }, }; let mint = match std::env::var("KS_DEVNET_SPL_TOKEN_MINT") { std::result::Result::Ok(value) => ks_lib::MdPubkey(value), std::result::Result::Err(error) => { panic!("KS_DEVNET_SPL_TOKEN_MINT is required: {error}"); }, }; let destination = match std::env::var("KS_DEVNET_SPL_TOKEN_DESTINATION") { std::result::Result::Ok(value) => ks_lib::MdPubkey(value), std::result::Result::Err(error) => { panic!("KS_DEVNET_SPL_TOKEN_DESTINATION is required: {error}"); }, }; let authority = match std::env::var("KS_DEVNET_SPL_TOKEN_AUTHORITY") { std::result::Result::Ok(value) => ks_lib::MdPubkey(value), std::result::Result::Err(error) => { panic!("KS_DEVNET_SPL_TOKEN_AUTHORITY is required: {error}"); }, }; let decimals = match std::env::var("KS_DEVNET_SPL_TOKEN_DECIMALS") { std::result::Result::Ok(value) => match value.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("KS_DEVNET_SPL_TOKEN_DECIMALS must be a u8: {error}"); }, }, std::result::Result::Err(_) => 9, }; let amount = match std::env::var("KS_DEVNET_SPL_TOKEN_AMOUNT") { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => "1".to_string(), }; let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(&local_devnet_profile()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"), }; let report = match crate::inspect_spl_token_stateful_readiness( &pool, &crate::SplTokenStatefulReadinessRequest { query_role: "http_queries".to_string(), cluster: ks_lib::ExApiExecutionCluster::Devnet, operation: ks_lib::ExSplClassicTokenOperation::Instruction { value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked { source, mint, destination, authority: ks_lib::ExSplClassicTokenAuthority { authority, multisig_signers: std::vec::Vec::new(), }, amount: ks_lib::ExSplClassicTokenAmount(amount), decimals, }, }, }, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Devnet Token preflight failed: {error}"), }; assert_eq!( report.status, crate::SplTokenStatefulReadinessStatus::Ready, "{:#?}", report.checks ); } }