Files
khadhroony-bot3/kb-pipeline/src/solana_token_stateful.rs
2026-07-25 15:13:46 +02:00

1697 lines
66 KiB
Rust

// file: kb-pipeline/src/solana_token_stateful.rs
// version: 1
//! 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: kb_lib::ExApiExecutionCluster,
/// Typed operation whose external state must be inspected.
pub operation: kb_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: kb_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<u64>,
/// Ordered stateful checks.
pub checks: std::vec::Vec<crate::SplTokenStatefulCheck>,
/// Ordered measured facts.
pub facts: std::vec::Vec<crate::SplTokenStatefulFact>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MintState {
mint_authority: std::option::Option<std::string::String>,
supply: u64,
decimals: u8,
initialized: bool,
freeze_authority: std::option::Option<std::string::String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct TokenAccountState {
mint: std::string::String,
owner: std::string::String,
amount: u64,
delegate: std::option::Option<std::string::String>,
state: u8,
native_reserve: std::option::Option<u64>,
delegated_amount: u64,
close_authority: std::option::Option<std::string::String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
struct MultisigState {
threshold: u8,
member_count: u8,
initialized: bool,
members: std::vec::Vec<std::string::String>,
}
type AccountCache = std::collections::BTreeMap<
std::string::String,
std::option::Option<kb_onchain_transport::AccountInfoValue>,
>;
/// Inspects Localnet or Devnet Token state required before simulation.
pub async fn inspect_spl_token_stateful_readiness(
pool: &kb_onchain_transport::HttpEndpointPool,
request: &crate::SplTokenStatefulReadinessRequest,
) -> kb_core::Result<crate::SplTokenStatefulReadinessReport> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"SPL Token stateful readiness query_role must not be empty",
));
}
match request.cluster {
kb_lib::ExApiExecutionCluster::Localnet | kb_lib::ExApiExecutionCluster::Devnet => {},
kb_lib::ExApiExecutionCluster::Testnet | kb_lib::ExApiExecutionCluster::Mainnet => {
return std::result::Result::Err(kb_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 kb_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<u64> = 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,
&kb_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,
&kb_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 {
kb_lib::ExSplClassicTokenOperation::Instruction { value } => {
inspect_single(&mut report, &cache, value);
},
kb_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: kb_lib::ExApiExecutionCluster,
genesis: &kb_onchain_transport::GenesisHashResult,
) -> kb_core::Result<()> {
return match expected {
kb_lib::ExApiExecutionCluster::Devnet => {
if genesis.classified_cluster
== std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet)
{
std::result::Result::Ok(())
} else {
std::result::Result::Err(kb_core::Error::new(
"spl_token_stateful_devnet_genesis_mismatch",
"SPL Token Devnet readiness requires the official Devnet genesis hash",
))
}
},
kb_lib::ExApiExecutionCluster::Localnet => {
if genesis.classified_cluster.is_none() {
std::result::Result::Ok(())
} else {
std::result::Result::Err(kb_core::Error::new(
"spl_token_stateful_localnet_public_cluster",
"Localnet readiness refuses endpoints classified as a public cluster",
))
}
},
kb_lib::ExApiExecutionCluster::Testnet | kb_lib::ExApiExecutionCluster::Mainnet => {
std::result::Result::Err(kb_core::Error::new(
"spl_token_stateful_cluster_unsupported",
"SPL Token stateful readiness is restricted to Localnet and Devnet",
))
},
};
}
fn operation_addresses(
operation: &kb_lib::ExSplClassicTokenOperation,
) -> kb_core::Result<std::vec::Vec<kb_lib::MdPubkey>> {
let mut addresses = std::vec::Vec::new();
match operation {
kb_lib::ExSplClassicTokenOperation::Instruction { value } => {
collect_single_addresses(&mut addresses, value);
},
kb_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(kb_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: &kb_lib::ExSplClassicTokenOperation,
) -> std::vec::Vec<(kb_lib::MdPubkey, usize)> {
let mut values = std::vec::Vec::new();
let instructions: std::vec::Vec<&kb_lib::ExSplClassicTokenSingleOperation> = match operation {
kb_lib::ExSplClassicTokenOperation::Instruction { value } => {
std::vec![value]
},
kb_lib::ExSplClassicTokenOperation::Batch { instructions } => instructions.iter().collect(),
};
for operation in instructions {
let candidate = match operation {
kb_lib::ExSplClassicTokenSingleOperation::InitializeMint { mint, .. } => {
std::option::Option::Some((mint.clone(), MINT_LEN))
},
kb_lib::ExSplClassicTokenSingleOperation::InitializeAccount { account, .. } => {
std::option::Option::Some((account.clone(), TOKEN_ACCOUNT_LEN))
},
kb_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: &kb_lib::ExSplClassicTokenOperation,
) -> std::vec::Vec<kb_lib::MdPubkey> {
let instructions: std::vec::Vec<&kb_lib::ExSplClassicTokenSingleOperation> = match operation {
kb_lib::ExSplClassicTokenOperation::Instruction { value } => {
std::vec![value]
},
kb_lib::ExSplClassicTokenOperation::Batch { instructions } => instructions.iter().collect(),
};
let mut values = std::vec::Vec::new();
for operation in instructions {
if let kb_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<kb_lib::MdPubkey>, value: &kb_lib::MdPubkey) {
if !addresses.iter().any(|existing| return existing == value) {
addresses.push(value.clone());
}
}
fn add_authority(
addresses: &mut std::vec::Vec<kb_lib::MdPubkey>,
authority: &kb_lib::ExSplClassicTokenAuthority,
) {
if !authority.multisig_signers.is_empty() {
add_address(addresses, &authority.authority);
}
}
fn collect_single_addresses(
addresses: &mut std::vec::Vec<kb_lib::MdPubkey>,
operation: &kb_lib::ExSplClassicTokenSingleOperation,
) {
match operation {
kb_lib::ExSplClassicTokenSingleOperation::InitializeMint { mint, .. }
| kb_lib::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint }
| kb_lib::ExSplClassicTokenSingleOperation::AmountToUiAmount { mint, .. }
| kb_lib::ExSplClassicTokenSingleOperation::UiAmountToAmount { mint, .. } => {
add_address(addresses, mint);
},
kb_lib::ExSplClassicTokenSingleOperation::InitializeAccount { account, mint, .. } => {
add_address(addresses, account);
add_address(addresses, mint);
},
kb_lib::ExSplClassicTokenSingleOperation::InitializeMultisig { multisig, .. } => {
add_address(addresses, multisig);
},
kb_lib::ExSplClassicTokenSingleOperation::Transfer {
source,
destination,
authority,
..
} => {
add_address(addresses, source);
add_address(addresses, destination);
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::Approve { source, authority, .. }
| kb_lib::ExSplClassicTokenSingleOperation::Revoke { source, authority } => {
add_address(addresses, source);
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::SetAuthority {
owned, current_authority, ..
} => {
add_address(addresses, owned);
add_authority(addresses, current_authority);
},
kb_lib::ExSplClassicTokenSingleOperation::MintTo {
mint, destination, authority, ..
}
| kb_lib::ExSplClassicTokenSingleOperation::MintToChecked {
mint,
destination,
authority,
..
} => {
add_address(addresses, mint);
add_address(addresses, destination);
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::Burn { source, mint, authority, .. }
| kb_lib::ExSplClassicTokenSingleOperation::BurnChecked {
source, mint, authority, ..
} => {
add_address(addresses, source);
add_address(addresses, mint);
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::CloseAccount { account, authority, .. }
| kb_lib::ExSplClassicTokenSingleOperation::UnwrapLamports { account, authority, .. }
| kb_lib::ExSplClassicTokenSingleOperation::WithdrawExcessLamports {
account,
authority,
..
} => {
add_address(addresses, account);
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::FreezeAccount { account, mint, authority }
| kb_lib::ExSplClassicTokenSingleOperation::ThawAccount { account, mint, authority } => {
add_address(addresses, account);
add_address(addresses, mint);
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::TransferChecked {
source,
mint,
destination,
authority,
..
}
| kb_lib::ExSplClassicTokenSingleOperation::ApproveChecked {
source,
mint,
delegate: destination,
authority,
..
} => {
add_address(addresses, source);
add_address(addresses, mint);
if matches!(operation, kb_lib::ExSplClassicTokenSingleOperation::TransferChecked { .. })
{
add_address(addresses, destination);
}
add_authority(addresses, authority);
},
kb_lib::ExSplClassicTokenSingleOperation::SyncNative { account, .. }
| kb_lib::ExSplClassicTokenSingleOperation::InitializeImmutableOwner { account } => {
add_address(addresses, account);
},
}
}
fn inspect_single(
report: &mut crate::SplTokenStatefulReadinessReport,
cache: &AccountCache,
operation: &kb_lib::ExSplClassicTokenSingleOperation,
) {
match operation {
kb_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",
);
}
},
kb_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());
},
kb_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",
);
}
},
kb_lib::ExSplClassicTokenSingleOperation::Transfer {
source,
destination,
authority,
amount,
} => {
inspect_transfer(
report,
cache,
source,
destination,
std::option::Option::None,
authority,
amount,
std::option::Option::None,
);
},
kb_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),
);
},
kb_lib::ExSplClassicTokenSingleOperation::Approve { source, authority, amount, .. } => {
inspect_owner_amount(report, cache, source, authority, amount, false);
},
kb_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);
},
kb_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",
);
}
},
kb_lib::ExSplClassicTokenSingleOperation::MintTo {
mint,
destination,
authority,
amount,
}
| kb_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 kb_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",
);
}
}
},
kb_lib::ExSplClassicTokenSingleOperation::Burn { source, mint, authority, amount }
| kb_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::<u64>().ok())
{
push_check(
report,
"mint_supply_sufficient",
mint_state.supply >= parsed,
"mint supply must cover burned amount",
);
}
if let kb_lib::ExSplClassicTokenSingleOperation::BurnChecked { decimals, .. } =
operation
{
inspect_checked_mint(report, cache, source, mint, *decimals);
}
},
kb_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",
);
}
},
kb_lib::ExSplClassicTokenSingleOperation::FreezeAccount { account, mint, authority } => {
inspect_freeze_thaw(report, cache, account, mint, authority, 1);
},
kb_lib::ExSplClassicTokenSingleOperation::ThawAccount { account, mint, authority } => {
inspect_freeze_thaw(report, cache, account, mint, authority, 2);
},
kb_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",
);
}
},
kb_lib::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint }
| kb_lib::ExSplClassicTokenSingleOperation::AmountToUiAmount { mint, .. }
| kb_lib::ExSplClassicTokenSingleOperation::UiAmountToAmount { mint, .. } => {
let state = mint_state(report, cache, mint);
check_mint_initialized(report, state.as_ref());
},
kb_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",
);
}
},
kb_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",
);
}
}
}
},
kb_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()),
);
},
_ => {},
}
}
},
kb_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: &kb_lib::MdPubkey,
destination: &kb_lib::MdPubkey,
explicit_mint: std::option::Option<&kb_lib::MdPubkey>,
authority: &kb_lib::ExSplClassicTokenAuthority,
amount: &kb_lib::ExSplClassicTokenAmount,
decimals: std::option::Option<u8>,
) {
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: &kb_lib::MdPubkey,
authority: &kb_lib::ExSplClassicTokenAuthority,
amount: &kb_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: &kb_lib::MdPubkey,
mint: &kb_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: &kb_lib::MdPubkey,
mint: &kb_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: &kb_lib::MdPubkey,
mint: &kb_lib::MdPubkey,
authority: &kb_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: &kb_lib::MdPubkey,
authority_type: kb_lib::ExSplClassicTokenAuthorityType,
new_authority: &std::option::Option<kb_lib::MdPubkey>,
current: &kb_lib::ExSplClassicTokenAuthority,
) {
let value = account_value(report, cache, owned, std::option::Option::None);
if let std::option::Option::Some(value) = value {
match authority_type {
kb_lib::ExSplClassicTokenAuthorityType::MintTokens
| kb_lib::ExSplClassicTokenAuthorityType::FreezeAccount => {
if let std::result::Result::Ok(state) = parse_mint(value.data.as_slice()) {
let expected =
if authority_type == kb_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",
);
}
},
kb_lib::ExSplClassicTokenAuthorityType::AccountOwner
| kb_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 == kb_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 == kb_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: &kb_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: &kb_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: &kb_lib::MdPubkey,
expected_len: std::option::Option<usize>,
) -> std::option::Option<&'a kb_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 == kb_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: &kb_lib::MdPubkey,
) -> std::option::Option<MintState> {
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: &kb_lib::MdPubkey,
) -> std::option::Option<MintState> {
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: &kb_lib::MdPubkey,
) -> std::option::Option<TokenAccountState> {
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: &kb_lib::MdPubkey,
) -> std::option::Option<TokenAccountState> {
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: &kb_lib::MdPubkey,
) -> std::option::Option<MultisigState> {
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: &kb_lib::ExSplClassicTokenAmount,
) -> std::option::Option<u64> {
return match amount.0.parse::<u64>() {
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<std::string::String>,
) {
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<MintState, &'static str> {
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<TokenAccountState, &'static str> {
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<MultisigState, &'static str> {
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<std::option::Option<std::string::String>, &'static str> {
if bytes.len() != 36 {
return std::result::Result::Err("COption<Pubkey> 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<Pubkey> tag must be 0 or 1"),
};
}
fn parse_coption_u64(bytes: &[u8]) -> std::result::Result<std::option::Option<u64>, &'static str> {
if bytes.len() != 12 {
return std::result::Result::Err("COption<u64> 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<u64> tag must be 0 or 1"),
};
}
fn read_u64(bytes: &[u8]) -> std::result::Result<u64, &'static str> {
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: kb_lib::ExApiExecutionCluster::Devnet,
operation_code: kb_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<u8>) -> kb_onchain_transport::AccountInfoValue {
return kb_onchain_transport::AccountInfoValue {
lamports: 10_000_000,
owner: kb_lib::MdProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
executable: false,
rent_epoch: 0,
space: data.len() as u64,
data,
};
}
fn local_devnet_profile() -> kb_config::ProfileConfig {
let config =
match kb_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 = kb_lib::MdPubkey(bs58::encode([2_u8; 32]).into_string());
let owner_key = kb_lib::MdPubkey(bs58::encode([3_u8; 32]).into_string());
let source_key = kb_lib::MdPubkey(bs58::encode([7_u8; 32]).into_string());
let destination_key = kb_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,
&kb_lib::ExSplClassicTokenSingleOperation::TransferChecked {
source: source_key,
mint: mint_key,
destination: destination_key,
authority: kb_lib::ExSplClassicTokenAuthority {
authority: owner_key,
multisig_signers: std::vec::Vec::new(),
},
amount: kb_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 = kb_lib::MdPubkey(bs58::encode([10_u8; 32]).into_string());
let member_one = kb_lib::MdPubkey(bs58::encode([11_u8; 32]).into_string());
let member_two = kb_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,
&kb_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 = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
let operation = kb_lib::ExSplClassicTokenOperation::Instruction {
value: kb_lib::ExSplClassicTokenSingleOperation::TransferChecked {
source: key.clone(),
mint: key.clone(),
destination: key.clone(),
authority: kb_lib::ExSplClassicTokenAuthority {
authority: key.clone(),
multisig_signers: std::vec::Vec::new(),
},
amount: kb_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("KB_DEVNET_SPL_TOKEN_PREFLIGHT_TEST").ok().as_deref()
!= std::option::Option::Some("1")
{
return;
}
let source = match std::env::var("KB_DEVNET_SPL_TOKEN_SOURCE") {
std::result::Result::Ok(value) => kb_lib::MdPubkey(value),
std::result::Result::Err(error) => {
panic!("KB_DEVNET_SPL_TOKEN_SOURCE is required: {error}");
},
};
let mint = match std::env::var("KB_DEVNET_SPL_TOKEN_MINT") {
std::result::Result::Ok(value) => kb_lib::MdPubkey(value),
std::result::Result::Err(error) => {
panic!("KB_DEVNET_SPL_TOKEN_MINT is required: {error}");
},
};
let destination = match std::env::var("KB_DEVNET_SPL_TOKEN_DESTINATION") {
std::result::Result::Ok(value) => kb_lib::MdPubkey(value),
std::result::Result::Err(error) => {
panic!("KB_DEVNET_SPL_TOKEN_DESTINATION is required: {error}");
},
};
let authority = match std::env::var("KB_DEVNET_SPL_TOKEN_AUTHORITY") {
std::result::Result::Ok(value) => kb_lib::MdPubkey(value),
std::result::Result::Err(error) => {
panic!("KB_DEVNET_SPL_TOKEN_AUTHORITY is required: {error}");
},
};
let decimals = match std::env::var("KB_DEVNET_SPL_TOKEN_DECIMALS") {
std::result::Result::Ok(value) => match value.parse::<u8>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
panic!("KB_DEVNET_SPL_TOKEN_DECIMALS must be a u8: {error}");
},
},
std::result::Result::Err(_) => 9,
};
let amount = match std::env::var("KB_DEVNET_SPL_TOKEN_AMOUNT") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => "1".to_string(),
};
let pool =
match kb_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: kb_lib::ExApiExecutionCluster::Devnet,
operation: kb_lib::ExSplClassicTokenOperation::Instruction {
value: kb_lib::ExSplClassicTokenSingleOperation::TransferChecked {
source,
mint,
destination,
authority: kb_lib::ExSplClassicTokenAuthority {
authority,
multisig_signers: std::vec::Vec::new(),
},
amount: kb_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
);
}
}