1215 lines
49 KiB
Rust
1215 lines
49 KiB
Rust
// file: ks-lib/src/executor/spl/token/builder.rs
|
|
// version: 11
|
|
|
|
//! Official classic SPL Token builders and conservative plan validation.
|
|
|
|
use std::str::FromStr; // rust-rules: trait-import
|
|
|
|
struct ParsedAuthority {
|
|
authority: solana_pubkey::Pubkey,
|
|
signers: std::vec::Vec<solana_pubkey::Pubkey>,
|
|
}
|
|
|
|
pub(crate) fn executor_spl_token_build_prepared_plan(
|
|
intent: &crate::ExSplClassicTokenExecutionIntent,
|
|
) -> ks_core::Result<crate::ExApiPreparedExecutionPlan> {
|
|
if intent.intent_id.trim().is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_intent_id_empty",
|
|
"SPL Token execution intent id must not be empty",
|
|
));
|
|
}
|
|
let fee_payer = match parse_pubkey(&intent.fee_payer, "fee_payer") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
match validate_policy(intent) {
|
|
std::result::Result::Ok(()) => {},
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
let program_id = match solana_pubkey::Pubkey::from_str(ks_program_ids::SPL_TOKEN_PROGRAM_ID) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_program_id_invalid",
|
|
error.to_string(),
|
|
));
|
|
},
|
|
};
|
|
let instruction = match &intent.operation {
|
|
crate::ExSplClassicTokenOperation::Instruction { value } => {
|
|
match build_single(&program_id, value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
},
|
|
crate::ExSplClassicTokenOperation::Batch { instructions } => {
|
|
match build_batch(&program_id, instructions.as_slice()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
},
|
|
};
|
|
let requested_spend_lamports = match declared_spend_lamports(&intent.operation) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let planned_instruction = planned_instruction(intent.operation.operation_code(), &instruction);
|
|
let required_signers = required_signers(&intent.fee_payer, &instruction.accounts);
|
|
tracing::debug!(
|
|
target: crate::TRACING_TARGET_EXECUTOR_SPL_TOKEN,
|
|
action = "executor_spl_token_build_prepared_plan",
|
|
intent_id = %intent.intent_id,
|
|
operation_code = intent.operation.operation_code(),
|
|
program_id = ks_program_ids::SPL_TOKEN_PROGRAM_ID,
|
|
account_occurrence_count = instruction.accounts.len(),
|
|
required_signer_count = required_signers.len(),
|
|
requested_spend_lamports,
|
|
dry_run = intent.policy.dry_run,
|
|
"built classic SPL Token execution plan"
|
|
);
|
|
return std::result::Result::Ok(crate::ExApiPreparedExecutionPlan {
|
|
executor_name: std::string::String::from("ks-lib-executor.spl.token"),
|
|
executor_version: std::string::String::from(env!("CARGO_PKG_VERSION")),
|
|
intent_id: intent.intent_id.clone(),
|
|
operation_code: std::string::String::from(intent.operation.operation_code()),
|
|
fee_payer: crate::MdPubkey(fee_payer.to_string()),
|
|
instructions: vec![planned_instruction],
|
|
required_signers,
|
|
policy: intent.policy.clone(),
|
|
requested_spend_lamports,
|
|
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
|
});
|
|
}
|
|
|
|
fn validate_policy(intent: &crate::ExSplClassicTokenExecutionIntent) -> ks_core::Result<()> {
|
|
if intent.policy.simulation != crate::ExApiExecutionSimulationPolicy::Required {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_simulation_required",
|
|
"SPL Token execution requires simulation before signing or sending",
|
|
));
|
|
}
|
|
match intent.policy.cost_limit.max_fee_lamports {
|
|
std::option::Option::Some(limit) if limit > 0 => {},
|
|
std::option::Option::Some(_) | std::option::Option::None => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_fee_limit_missing",
|
|
"SPL Token execution requires a positive transaction fee ceiling",
|
|
));
|
|
},
|
|
}
|
|
let validation = &intent.policy.post_execution_validation;
|
|
if !validation.canonical_insert_required
|
|
|| !validation.core_extraction_required
|
|
|| !validation.decode_replay_required
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_post_validation_required",
|
|
"SPL Token execution requires canonical insertion, core extraction and decode replay validation",
|
|
));
|
|
}
|
|
if intent.operation.requires_materialization() && !validation.materialization_required {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_materialization_validation_required",
|
|
"Mutating SPL Token execution requires materialization validation",
|
|
));
|
|
}
|
|
if has_unbounded_lamport_movement(&intent.operation) && !intent.policy.dry_run {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_stateful_lamport_preflight_required",
|
|
"WithdrawExcessLamports and full-balance UnwrapLamports remain simulation-only until a stateful preflight bounds the movable lamports",
|
|
));
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
macro_rules! parse_key {
|
|
($value:expr, $field:expr) => {
|
|
match parse_pubkey($value, $field) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! parse_raw_amount {
|
|
($value:expr, $field:expr) => {
|
|
match parse_amount($value, $field) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! parse_token_authority {
|
|
($value:expr) => {
|
|
match parse_authority($value) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
}
|
|
};
|
|
}
|
|
|
|
fn build_single(
|
|
program_id: &solana_pubkey::Pubkey,
|
|
operation: &crate::ExSplClassicTokenSingleOperation,
|
|
) -> ks_core::Result<solana_instruction::Instruction> {
|
|
return match operation {
|
|
crate::ExSplClassicTokenSingleOperation::InitializeMint {
|
|
mint,
|
|
mint_authority,
|
|
freeze_authority,
|
|
decimals,
|
|
} => {
|
|
let mint = parse_key!(mint, "mint");
|
|
let mint_authority = parse_key!(mint_authority, "mint_authority");
|
|
let freeze_authority = match freeze_authority {
|
|
std::option::Option::Some(value) => {
|
|
std::option::Option::Some(parse_key!(value, "freeze_authority"))
|
|
},
|
|
std::option::Option::None => std::option::Option::None,
|
|
};
|
|
official_instruction(spl_token_interface::instruction::initialize_mint2(
|
|
program_id,
|
|
&mint,
|
|
&mint_authority,
|
|
freeze_authority.as_ref(),
|
|
*decimals,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::InitializeAccount { account, mint, owner } => {
|
|
let account = parse_key!(account, "account");
|
|
let mint = parse_key!(mint, "mint");
|
|
let owner = parse_key!(owner, "owner");
|
|
official_instruction(spl_token_interface::instruction::initialize_account3(
|
|
program_id, &account, &mint, &owner,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::InitializeMultisig {
|
|
multisig,
|
|
members,
|
|
threshold,
|
|
} => {
|
|
if members.is_empty()
|
|
|| members.len() > crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS
|
|
|| *threshold == 0
|
|
|| usize::from(*threshold) > members.len()
|
|
{
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_multisig_threshold_invalid",
|
|
format!(
|
|
"SPL Token multisig requires 1..={} members and threshold 1..=N; received M={} N={}",
|
|
crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS,
|
|
threshold,
|
|
members.len()
|
|
),
|
|
));
|
|
}
|
|
let multisig = parse_key!(multisig, "multisig");
|
|
let mut parsed_members = std::vec::Vec::with_capacity(members.len());
|
|
for member in members {
|
|
parsed_members.push(parse_key!(member, "multisig_member"));
|
|
}
|
|
let member_refs = parsed_members.iter().collect::<std::vec::Vec<_>>();
|
|
official_instruction(spl_token_interface::instruction::initialize_multisig2(
|
|
program_id,
|
|
&multisig,
|
|
member_refs.as_slice(),
|
|
*threshold,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::Transfer {
|
|
source,
|
|
destination,
|
|
authority,
|
|
amount,
|
|
} => {
|
|
let source = parse_key!(source, "source");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::transfer(
|
|
program_id,
|
|
&source,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::Approve {
|
|
source,
|
|
delegate,
|
|
authority,
|
|
amount,
|
|
} => {
|
|
let source = parse_key!(source, "source");
|
|
let delegate = parse_key!(delegate, "delegate");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::approve(
|
|
program_id,
|
|
&source,
|
|
&delegate,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::Revoke { source, authority } => {
|
|
let source = parse_key!(source, "source");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
official_instruction(spl_token_interface::instruction::revoke(
|
|
program_id,
|
|
&source,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::SetAuthority {
|
|
owned,
|
|
authority_type,
|
|
new_authority,
|
|
current_authority,
|
|
} => {
|
|
let owned = parse_key!(owned, "owned");
|
|
let new_authority = match new_authority {
|
|
std::option::Option::Some(value) => {
|
|
std::option::Option::Some(parse_key!(value, "new_authority"))
|
|
},
|
|
std::option::Option::None => std::option::Option::None,
|
|
};
|
|
let current_authority = parse_token_authority!(current_authority);
|
|
let signer_refs = current_authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let authority_type = match authority_type {
|
|
crate::ExSplClassicTokenAuthorityType::MintTokens => {
|
|
spl_token_interface::instruction::AuthorityType::MintTokens
|
|
},
|
|
crate::ExSplClassicTokenAuthorityType::FreezeAccount => {
|
|
spl_token_interface::instruction::AuthorityType::FreezeAccount
|
|
},
|
|
crate::ExSplClassicTokenAuthorityType::AccountOwner => {
|
|
spl_token_interface::instruction::AuthorityType::AccountOwner
|
|
},
|
|
crate::ExSplClassicTokenAuthorityType::CloseAccount => {
|
|
spl_token_interface::instruction::AuthorityType::CloseAccount
|
|
},
|
|
};
|
|
official_instruction(spl_token_interface::instruction::set_authority(
|
|
program_id,
|
|
&owned,
|
|
new_authority.as_ref(),
|
|
authority_type,
|
|
¤t_authority.authority,
|
|
signer_refs.as_slice(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::MintTo {
|
|
mint,
|
|
destination,
|
|
authority,
|
|
amount,
|
|
} => {
|
|
let mint = parse_key!(mint, "mint");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::mint_to(
|
|
program_id,
|
|
&mint,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::Burn { source, mint, authority, amount } => {
|
|
let source = parse_key!(source, "source");
|
|
let mint = parse_key!(mint, "mint");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::burn(
|
|
program_id,
|
|
&source,
|
|
&mint,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::CloseAccount {
|
|
account,
|
|
destination,
|
|
authority,
|
|
} => {
|
|
let account = parse_key!(account, "account");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
official_instruction(spl_token_interface::instruction::close_account(
|
|
program_id,
|
|
&account,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::FreezeAccount { account, mint, authority } => {
|
|
let account = parse_key!(account, "account");
|
|
let mint = parse_key!(mint, "mint");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
official_instruction(spl_token_interface::instruction::freeze_account(
|
|
program_id,
|
|
&account,
|
|
&mint,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::ThawAccount { account, mint, authority } => {
|
|
let account = parse_key!(account, "account");
|
|
let mint = parse_key!(mint, "mint");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
official_instruction(spl_token_interface::instruction::thaw_account(
|
|
program_id,
|
|
&account,
|
|
&mint,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::TransferChecked {
|
|
source,
|
|
mint,
|
|
destination,
|
|
authority,
|
|
amount,
|
|
decimals,
|
|
} => {
|
|
let source = parse_key!(source, "source");
|
|
let mint = parse_key!(mint, "mint");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::transfer_checked(
|
|
program_id,
|
|
&source,
|
|
&mint,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
*decimals,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::ApproveChecked {
|
|
source,
|
|
mint,
|
|
delegate,
|
|
authority,
|
|
amount,
|
|
decimals,
|
|
} => {
|
|
let source = parse_key!(source, "source");
|
|
let mint = parse_key!(mint, "mint");
|
|
let delegate = parse_key!(delegate, "delegate");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::approve_checked(
|
|
program_id,
|
|
&source,
|
|
&mint,
|
|
&delegate,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
*decimals,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::MintToChecked {
|
|
mint,
|
|
destination,
|
|
authority,
|
|
amount,
|
|
decimals,
|
|
} => {
|
|
let mint = parse_key!(mint, "mint");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::mint_to_checked(
|
|
program_id,
|
|
&mint,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
*decimals,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::BurnChecked {
|
|
source,
|
|
mint,
|
|
authority,
|
|
amount,
|
|
decimals,
|
|
} => {
|
|
let source = parse_key!(source, "source");
|
|
let mint = parse_key!(mint, "mint");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::burn_checked(
|
|
program_id,
|
|
&source,
|
|
&mint,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount,
|
|
*decimals,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::SyncNative { account, include_rent_sysvar } => {
|
|
let account = parse_key!(account, "account");
|
|
if *include_rent_sysvar {
|
|
official_instruction(
|
|
spl_token_interface::instruction::sync_native_with_rent_sysvar(
|
|
program_id, &account,
|
|
),
|
|
)
|
|
} else {
|
|
official_instruction(spl_token_interface::instruction::sync_native(
|
|
program_id, &account,
|
|
))
|
|
}
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint } => {
|
|
let mint = parse_key!(mint, "mint");
|
|
official_instruction(spl_token_interface::instruction::get_account_data_size(
|
|
program_id, &mint,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::InitializeImmutableOwner { account } => {
|
|
let account = parse_key!(account, "account");
|
|
official_instruction(spl_token_interface::instruction::initialize_immutable_owner(
|
|
program_id, &account,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::AmountToUiAmount { mint, amount } => {
|
|
let mint = parse_key!(mint, "mint");
|
|
let amount = parse_raw_amount!(amount, "amount");
|
|
official_instruction(spl_token_interface::instruction::amount_to_ui_amount(
|
|
program_id, &mint, amount,
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::UiAmountToAmount { mint, ui_amount } => {
|
|
if ui_amount.len() > crate::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_ui_amount_too_large",
|
|
format!(
|
|
"SPL Token UI amount length {} exceeds {} bytes",
|
|
ui_amount.len(),
|
|
crate::EX_SPL_TOKEN_MAX_UI_AMOUNT_BYTES
|
|
),
|
|
));
|
|
}
|
|
let mint = parse_key!(mint, "mint");
|
|
official_instruction(spl_token_interface::instruction::ui_amount_to_amount(
|
|
program_id,
|
|
&mint,
|
|
ui_amount.as_str(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::WithdrawExcessLamports {
|
|
account,
|
|
destination,
|
|
authority,
|
|
} => {
|
|
let account = parse_key!(account, "account");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
official_instruction(spl_token_interface::instruction::withdraw_excess_lamports(
|
|
program_id,
|
|
&account,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
))
|
|
},
|
|
crate::ExSplClassicTokenSingleOperation::UnwrapLamports {
|
|
account,
|
|
destination,
|
|
authority,
|
|
amount_lamports,
|
|
} => {
|
|
let account = parse_key!(account, "account");
|
|
let destination = parse_key!(destination, "destination");
|
|
let authority = parse_token_authority!(authority);
|
|
let signer_refs = authority.signers.iter().collect::<std::vec::Vec<_>>();
|
|
let amount_lamports = match amount_lamports {
|
|
std::option::Option::Some(value) => {
|
|
std::option::Option::Some(parse_raw_amount!(value, "amount_lamports"))
|
|
},
|
|
std::option::Option::None => std::option::Option::None,
|
|
};
|
|
official_instruction(spl_token_interface::instruction::unwrap_lamports(
|
|
program_id,
|
|
&account,
|
|
&destination,
|
|
&authority.authority,
|
|
signer_refs.as_slice(),
|
|
amount_lamports,
|
|
))
|
|
},
|
|
};
|
|
}
|
|
|
|
fn build_batch(
|
|
program_id: &solana_pubkey::Pubkey,
|
|
operations: &[crate::ExSplClassicTokenSingleOperation],
|
|
) -> ks_core::Result<solana_instruction::Instruction> {
|
|
if operations.len() > crate::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_batch_instruction_limit_exceeded",
|
|
format!(
|
|
"SPL Token Batch child count {} exceeds {}",
|
|
operations.len(),
|
|
crate::EX_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS
|
|
),
|
|
));
|
|
}
|
|
let mut instructions = std::vec::Vec::with_capacity(operations.len());
|
|
let mut account_count = 0_usize;
|
|
for operation in operations {
|
|
let instruction = match build_single(program_id, operation) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if instruction.data.len() > usize::from(u8::MAX) {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_batch_child_data_too_large",
|
|
format!(
|
|
"SPL Token Batch child {} has {} data bytes; maximum is {}",
|
|
instructions.len(),
|
|
instruction.data.len(),
|
|
u8::MAX
|
|
),
|
|
));
|
|
}
|
|
account_count = match account_count.checked_add(instruction.accounts.len()) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_batch_account_count_overflow",
|
|
"SPL Token Batch aggregate account count overflowed usize",
|
|
));
|
|
},
|
|
};
|
|
if account_count > crate::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_batch_account_limit_exceeded",
|
|
format!(
|
|
"SPL Token Batch account occurrence count {account_count} exceeds {}",
|
|
crate::EX_SPL_TOKEN_MAX_BATCH_ACCOUNTS
|
|
),
|
|
));
|
|
}
|
|
instructions.push(instruction);
|
|
}
|
|
return official_instruction(spl_token_interface::instruction::batch(
|
|
program_id,
|
|
instructions.as_slice(),
|
|
));
|
|
}
|
|
|
|
fn official_instruction<BuilderError>(
|
|
result: std::result::Result<solana_instruction::Instruction, BuilderError>,
|
|
) -> ks_core::Result<solana_instruction::Instruction>
|
|
where
|
|
BuilderError: std::fmt::Display,
|
|
{
|
|
return match result {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_official_builder_failed",
|
|
error.to_string(),
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn parse_pubkey(pubkey: &crate::MdPubkey, field: &str) -> ks_core::Result<solana_pubkey::Pubkey> {
|
|
if pubkey.0.trim().is_empty() {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_pubkey_empty",
|
|
format!("SPL Token {field} public key must not be empty"),
|
|
));
|
|
}
|
|
return match solana_pubkey::Pubkey::from_str(pubkey.0.as_str()) {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_pubkey_invalid",
|
|
format!("invalid SPL Token {field} public key: {error}"),
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn parse_amount(amount: &crate::ExSplClassicTokenAmount, field: &str) -> ks_core::Result<u64> {
|
|
if amount.0.is_empty() || !amount.0.bytes().all(|value| return value.is_ascii_digit()) {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_amount_invalid",
|
|
format!("SPL Token {field} must be an unsigned decimal u64 string"),
|
|
));
|
|
}
|
|
return match amount.0.parse::<u64>() {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_amount_invalid",
|
|
format!("invalid SPL Token {field}: {error}"),
|
|
)),
|
|
};
|
|
}
|
|
|
|
fn parse_authority(
|
|
authority: &crate::ExSplClassicTokenAuthority,
|
|
) -> ks_core::Result<ParsedAuthority> {
|
|
if authority.multisig_signers.len() > crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_multisig_signer_limit_exceeded",
|
|
format!(
|
|
"SPL Token multisig signer occurrence count {} exceeds {}",
|
|
authority.multisig_signers.len(),
|
|
crate::EX_SPL_TOKEN_MAX_MULTISIG_SIGNERS
|
|
),
|
|
));
|
|
}
|
|
let parsed_authority = match parse_pubkey(&authority.authority, "authority") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mut signers = std::vec::Vec::with_capacity(authority.multisig_signers.len());
|
|
for signer in &authority.multisig_signers {
|
|
let signer = match parse_pubkey(signer, "multisig_signer") {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
signers.push(signer);
|
|
}
|
|
return std::result::Result::Ok(ParsedAuthority { authority: parsed_authority, signers });
|
|
}
|
|
|
|
fn declared_spend_lamports(operation: &crate::ExSplClassicTokenOperation) -> ks_core::Result<u64> {
|
|
return match operation {
|
|
crate::ExSplClassicTokenOperation::Instruction { value } => single_spend_lamports(value),
|
|
crate::ExSplClassicTokenOperation::Batch { instructions } => {
|
|
let mut total = 0_u64;
|
|
for instruction in instructions {
|
|
let amount = match single_spend_lamports(instruction) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
total = match total.checked_add(amount) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
return std::result::Result::Err(ks_core::Error::new(
|
|
"execution_spl_token_lamport_spend_overflow",
|
|
"SPL Token Batch declared lamport spend exceeds u64",
|
|
));
|
|
},
|
|
};
|
|
}
|
|
std::result::Result::Ok(total)
|
|
},
|
|
};
|
|
}
|
|
|
|
fn single_spend_lamports(
|
|
operation: &crate::ExSplClassicTokenSingleOperation,
|
|
) -> ks_core::Result<u64> {
|
|
return match operation {
|
|
crate::ExSplClassicTokenSingleOperation::UnwrapLamports {
|
|
amount_lamports: std::option::Option::Some(value),
|
|
..
|
|
} => parse_amount(value, "amount_lamports"),
|
|
_ => std::result::Result::Ok(0),
|
|
};
|
|
}
|
|
|
|
fn has_unbounded_lamport_movement(operation: &crate::ExSplClassicTokenOperation) -> bool {
|
|
return match operation {
|
|
crate::ExSplClassicTokenOperation::Instruction { value } => {
|
|
single_has_unbounded_lamport_movement(value)
|
|
},
|
|
crate::ExSplClassicTokenOperation::Batch { instructions } => {
|
|
instructions.iter().any(single_has_unbounded_lamport_movement)
|
|
},
|
|
};
|
|
}
|
|
|
|
fn single_has_unbounded_lamport_movement(
|
|
operation: &crate::ExSplClassicTokenSingleOperation,
|
|
) -> bool {
|
|
return matches!(
|
|
operation,
|
|
crate::ExSplClassicTokenSingleOperation::WithdrawExcessLamports { .. }
|
|
| crate::ExSplClassicTokenSingleOperation::UnwrapLamports {
|
|
amount_lamports: std::option::Option::None,
|
|
..
|
|
}
|
|
);
|
|
}
|
|
|
|
fn planned_instruction(
|
|
operation_code: &str,
|
|
instruction: &solana_instruction::Instruction,
|
|
) -> crate::ExApiPlannedInstruction {
|
|
return crate::ExApiPlannedInstruction {
|
|
program_id: crate::MdProgramId(instruction.program_id.to_string()),
|
|
operation_code: std::string::String::from(operation_code),
|
|
accounts: instruction
|
|
.accounts
|
|
.iter()
|
|
.map(|account| {
|
|
return crate::ExApiPlannedAccount {
|
|
pubkey: crate::MdPubkey(account.pubkey.to_string()),
|
|
is_signer: account.is_signer,
|
|
is_writable: account.is_writable,
|
|
};
|
|
})
|
|
.collect(),
|
|
data: instruction.data.clone(),
|
|
};
|
|
}
|
|
|
|
fn required_signers(
|
|
fee_payer: &crate::MdPubkey,
|
|
accounts: &[solana_instruction::AccountMeta],
|
|
) -> std::vec::Vec<crate::ExApiRequiredSigner> {
|
|
let mut required = vec![crate::ExApiRequiredSigner {
|
|
pubkey: fee_payer.clone(),
|
|
role: std::string::String::from("fee_payer"),
|
|
}];
|
|
for account in accounts {
|
|
if !account.is_signer {
|
|
continue;
|
|
}
|
|
let pubkey = crate::MdPubkey(account.pubkey.to_string());
|
|
if required.iter().any(|value| return value.pubkey == pubkey) {
|
|
continue;
|
|
}
|
|
required.push(crate::ExApiRequiredSigner {
|
|
pubkey,
|
|
role: std::string::String::from("token_authority_or_multisig_signer"),
|
|
});
|
|
}
|
|
return required;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) mod tests {
|
|
fn pubkey(value: &str) -> crate::MdPubkey {
|
|
return crate::MdPubkey(std::string::String::from(value));
|
|
}
|
|
|
|
fn policy(signers: &[&str], materialization_required: bool) -> crate::ExApiExecutionPolicy {
|
|
return crate::ExApiExecutionPolicy {
|
|
cost_limit: crate::ExApiExecutionCostLimit {
|
|
max_spend_lamports: std::option::Option::Some(u64::MAX),
|
|
max_fee_lamports: std::option::Option::Some(10_000),
|
|
max_compute_unit_price_micro_lamports: std::option::Option::None,
|
|
},
|
|
authorized_signers: signers.iter().map(|value| return pubkey(value)).collect(),
|
|
post_execution_validation: crate::ExApiPostExecutionValidationPolicy {
|
|
canonical_insert_required: true,
|
|
core_extraction_required: true,
|
|
decode_replay_required: true,
|
|
materialization_required,
|
|
},
|
|
..crate::ExApiExecutionPolicy::default()
|
|
};
|
|
}
|
|
|
|
pub(crate) fn intent(
|
|
operation: crate::ExSplClassicTokenOperation,
|
|
) -> crate::ExSplClassicTokenExecutionIntent {
|
|
let fee_payer = ks_program_ids::SYSTEM_PROGRAM_ID;
|
|
return crate::ExSplClassicTokenExecutionIntent {
|
|
intent_id: std::string::String::from("token-intent-1"),
|
|
fee_payer: pubkey(fee_payer),
|
|
policy: policy(
|
|
&[fee_payer, ks_program_ids::VOTE_PROGRAM_ID, ks_program_ids::STAKE_PROGRAM_ID],
|
|
operation.requires_materialization(),
|
|
),
|
|
operation,
|
|
};
|
|
}
|
|
|
|
fn authority(signers: &[&str]) -> crate::ExSplClassicTokenAuthority {
|
|
return crate::ExSplClassicTokenAuthority {
|
|
authority: pubkey(ks_program_ids::CONFIG_PROGRAM_ID),
|
|
multisig_signers: signers.iter().map(|value| return pubkey(value)).collect(),
|
|
};
|
|
}
|
|
|
|
fn transfer_checked() -> crate::ExSplClassicTokenSingleOperation {
|
|
return crate::ExSplClassicTokenSingleOperation::TransferChecked {
|
|
source: pubkey(ks_program_ids::SYSTEM_PROGRAM_ID),
|
|
mint: pubkey(ks_program_ids::CONFIG_PROGRAM_ID),
|
|
destination: pubkey(ks_program_ids::STAKE_PROGRAM_ID),
|
|
authority: authority(&[ks_program_ids::VOTE_PROGRAM_ID]),
|
|
amount: crate::ExSplClassicTokenAmount(std::string::String::from(
|
|
"18446744073709551615",
|
|
)),
|
|
decimals: 9,
|
|
};
|
|
}
|
|
|
|
fn all_single_operations() -> std::vec::Vec<(u8, crate::ExSplClassicTokenSingleOperation)> {
|
|
let first = pubkey(ks_program_ids::SYSTEM_PROGRAM_ID);
|
|
let second = pubkey(ks_program_ids::CONFIG_PROGRAM_ID);
|
|
let third = pubkey(ks_program_ids::STAKE_PROGRAM_ID);
|
|
let authority = authority(&[]);
|
|
let amount = crate::ExSplClassicTokenAmount(std::string::String::from("42"));
|
|
return vec![
|
|
(
|
|
20,
|
|
crate::ExSplClassicTokenSingleOperation::InitializeMint {
|
|
mint: first.clone(),
|
|
mint_authority: second.clone(),
|
|
freeze_authority: std::option::Option::Some(third.clone()),
|
|
decimals: 9,
|
|
},
|
|
),
|
|
(
|
|
18,
|
|
crate::ExSplClassicTokenSingleOperation::InitializeAccount {
|
|
account: first.clone(),
|
|
mint: second.clone(),
|
|
owner: third.clone(),
|
|
},
|
|
),
|
|
(
|
|
19,
|
|
crate::ExSplClassicTokenSingleOperation::InitializeMultisig {
|
|
multisig: first.clone(),
|
|
members: vec![second.clone(), third.clone()],
|
|
threshold: 1,
|
|
},
|
|
),
|
|
(
|
|
3,
|
|
crate::ExSplClassicTokenSingleOperation::Transfer {
|
|
source: first.clone(),
|
|
destination: second.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
},
|
|
),
|
|
(
|
|
4,
|
|
crate::ExSplClassicTokenSingleOperation::Approve {
|
|
source: first.clone(),
|
|
delegate: second.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
},
|
|
),
|
|
(
|
|
5,
|
|
crate::ExSplClassicTokenSingleOperation::Revoke {
|
|
source: first.clone(),
|
|
authority: authority.clone(),
|
|
},
|
|
),
|
|
(
|
|
6,
|
|
crate::ExSplClassicTokenSingleOperation::SetAuthority {
|
|
owned: first.clone(),
|
|
authority_type: crate::ExSplClassicTokenAuthorityType::CloseAccount,
|
|
new_authority: std::option::Option::Some(second.clone()),
|
|
current_authority: authority.clone(),
|
|
},
|
|
),
|
|
(
|
|
7,
|
|
crate::ExSplClassicTokenSingleOperation::MintTo {
|
|
mint: first.clone(),
|
|
destination: second.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
},
|
|
),
|
|
(
|
|
8,
|
|
crate::ExSplClassicTokenSingleOperation::Burn {
|
|
source: first.clone(),
|
|
mint: second.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
},
|
|
),
|
|
(
|
|
9,
|
|
crate::ExSplClassicTokenSingleOperation::CloseAccount {
|
|
account: first.clone(),
|
|
destination: second.clone(),
|
|
authority: authority.clone(),
|
|
},
|
|
),
|
|
(
|
|
10,
|
|
crate::ExSplClassicTokenSingleOperation::FreezeAccount {
|
|
account: first.clone(),
|
|
mint: second.clone(),
|
|
authority: authority.clone(),
|
|
},
|
|
),
|
|
(
|
|
11,
|
|
crate::ExSplClassicTokenSingleOperation::ThawAccount {
|
|
account: first.clone(),
|
|
mint: second.clone(),
|
|
authority: authority.clone(),
|
|
},
|
|
),
|
|
(12, transfer_checked()),
|
|
(
|
|
13,
|
|
crate::ExSplClassicTokenSingleOperation::ApproveChecked {
|
|
source: first.clone(),
|
|
mint: second.clone(),
|
|
delegate: third.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
decimals: 9,
|
|
},
|
|
),
|
|
(
|
|
14,
|
|
crate::ExSplClassicTokenSingleOperation::MintToChecked {
|
|
mint: first.clone(),
|
|
destination: second.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
decimals: 9,
|
|
},
|
|
),
|
|
(
|
|
15,
|
|
crate::ExSplClassicTokenSingleOperation::BurnChecked {
|
|
source: first.clone(),
|
|
mint: second.clone(),
|
|
authority: authority.clone(),
|
|
amount: amount.clone(),
|
|
decimals: 9,
|
|
},
|
|
),
|
|
(
|
|
17,
|
|
crate::ExSplClassicTokenSingleOperation::SyncNative {
|
|
account: first.clone(),
|
|
include_rent_sysvar: true,
|
|
},
|
|
),
|
|
(
|
|
21,
|
|
crate::ExSplClassicTokenSingleOperation::GetAccountDataSize { mint: first.clone() },
|
|
),
|
|
(
|
|
22,
|
|
crate::ExSplClassicTokenSingleOperation::InitializeImmutableOwner {
|
|
account: first.clone(),
|
|
},
|
|
),
|
|
(
|
|
23,
|
|
crate::ExSplClassicTokenSingleOperation::AmountToUiAmount {
|
|
mint: first.clone(),
|
|
amount: amount.clone(),
|
|
},
|
|
),
|
|
(
|
|
24,
|
|
crate::ExSplClassicTokenSingleOperation::UiAmountToAmount {
|
|
mint: first.clone(),
|
|
ui_amount: std::string::String::from("0.000000042"),
|
|
},
|
|
),
|
|
(
|
|
38,
|
|
crate::ExSplClassicTokenSingleOperation::WithdrawExcessLamports {
|
|
account: first.clone(),
|
|
destination: second.clone(),
|
|
authority: authority.clone(),
|
|
},
|
|
),
|
|
(
|
|
45,
|
|
crate::ExSplClassicTokenSingleOperation::UnwrapLamports {
|
|
account: first,
|
|
destination: second,
|
|
authority,
|
|
amount_lamports: std::option::Option::Some(amount),
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
#[test]
|
|
fn every_current_or_recent_single_operation_uses_its_published_tag() {
|
|
let operations = all_single_operations();
|
|
assert_eq!(operations.len() + 1, crate::EX_SPL_TOKEN_SUPPORTED_OPERATION_CODES.len());
|
|
for (tag, operation) in operations {
|
|
let intent =
|
|
intent(crate::ExSplClassicTokenOperation::Instruction { value: operation });
|
|
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("tag {tag} plan failed: {error}"));
|
|
assert_eq!(plan.instructions[0].data[0], tag);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn checked_transfer_matches_official_builder_and_preserves_u64_max() {
|
|
let operation = transfer_checked();
|
|
let intent = intent(crate::ExSplClassicTokenOperation::Instruction { value: operation });
|
|
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("checked transfer plan failed: {error}"));
|
|
assert_eq!(plan.instructions.len(), 1);
|
|
assert_eq!(plan.instructions[0].data[0], 12);
|
|
assert_eq!(&plan.instructions[0].data[1..9], &u64::MAX.to_le_bytes());
|
|
assert_eq!(plan.instructions[0].data[9], 9);
|
|
assert_eq!(plan.required_signers.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn multisig_meta_order_is_preserved_while_transaction_signers_are_unique() {
|
|
let first = ks_program_ids::VOTE_PROGRAM_ID;
|
|
let second = ks_program_ids::STAKE_PROGRAM_ID;
|
|
let operation = crate::ExSplClassicTokenSingleOperation::Transfer {
|
|
source: pubkey(ks_program_ids::SYSTEM_PROGRAM_ID),
|
|
destination: pubkey(ks_program_ids::CONFIG_PROGRAM_ID),
|
|
authority: authority(&[first, second, first]),
|
|
amount: crate::ExSplClassicTokenAmount(std::string::String::from("1")),
|
|
};
|
|
let intent = intent(crate::ExSplClassicTokenOperation::Instruction { value: operation });
|
|
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("multisig transfer plan failed: {error}"));
|
|
let accounts = &plan.instructions[0].accounts;
|
|
assert_eq!(accounts[3].pubkey.0, first);
|
|
assert_eq!(accounts[4].pubkey.0, second);
|
|
assert_eq!(accounts[5].pubkey.0, first);
|
|
assert_eq!(plan.required_signers.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn recent_unwrap_and_batch_match_published_wire() {
|
|
let unwrap = crate::ExSplClassicTokenSingleOperation::UnwrapLamports {
|
|
account: pubkey(ks_program_ids::SYSTEM_PROGRAM_ID),
|
|
destination: pubkey(ks_program_ids::STAKE_PROGRAM_ID),
|
|
authority: authority(&[]),
|
|
amount_lamports: std::option::Option::Some(crate::ExSplClassicTokenAmount(
|
|
std::string::String::from("42"),
|
|
)),
|
|
};
|
|
let intent = intent(crate::ExSplClassicTokenOperation::Batch {
|
|
instructions: vec![unwrap, transfer_checked()],
|
|
});
|
|
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("recent Batch plan failed: {error}"));
|
|
assert_eq!(plan.instructions[0].data[0], 255);
|
|
assert_eq!(plan.instructions[0].data[2], 10);
|
|
assert_eq!(plan.instructions[0].data[3], 45);
|
|
assert_eq!(plan.requested_spend_lamports, 42);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_amount_multisig_and_optional_simulation_fail_closed() {
|
|
let mut operation = transfer_checked();
|
|
if let crate::ExSplClassicTokenSingleOperation::TransferChecked { amount, .. } =
|
|
&mut operation
|
|
{
|
|
amount.0 = std::string::String::from("-1");
|
|
}
|
|
let pintent = intent(crate::ExSplClassicTokenOperation::Instruction { value: operation });
|
|
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&pintent,
|
|
)
|
|
.expect_err("negative amount must fail");
|
|
assert_eq!(error.code(), "execution_spl_token_amount_invalid");
|
|
|
|
let mut intent =
|
|
intent(crate::ExSplClassicTokenOperation::Instruction { value: transfer_checked() });
|
|
intent.policy.simulation = crate::ExApiExecutionSimulationPolicy::Optional;
|
|
let error = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&intent,
|
|
)
|
|
.expect_err("optional simulation must fail");
|
|
assert_eq!(error.code(), "execution_spl_token_simulation_required");
|
|
}
|
|
|
|
#[test]
|
|
fn every_cluster_is_constructible_and_mainnet_remains_governed_by_common_safety() {
|
|
for cluster in [
|
|
crate::ExApiExecutionCluster::Localnet,
|
|
crate::ExApiExecutionCluster::Devnet,
|
|
crate::ExApiExecutionCluster::Testnet,
|
|
crate::ExApiExecutionCluster::Mainnet,
|
|
] {
|
|
let mut intent = intent(crate::ExSplClassicTokenOperation::Instruction {
|
|
value: transfer_checked(),
|
|
});
|
|
intent.policy.cluster.expected_cluster = cluster;
|
|
let plan = crate::ExApiTypedInstructionExecutor::build_prepared_plan(
|
|
&crate::ExSplTokenExecutor,
|
|
&intent,
|
|
)
|
|
.unwrap_or_else(|error| panic!("cluster plan failed: {error}"));
|
|
assert_eq!(plan.policy.cluster.expected_cluster, cluster);
|
|
let evaluation = crate::ExSafetyChecker
|
|
.evaluate_prepared_plan(&plan)
|
|
.unwrap_or_else(|error| panic!("safety evaluation failed: {error}"));
|
|
if cluster == crate::ExApiExecutionCluster::Mainnet {
|
|
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Deny);
|
|
assert!(evaluation.violations.iter().any(|violation| {
|
|
return violation.code == "execution_mainnet_disabled";
|
|
}));
|
|
let mut explicitly_enabled = plan.clone();
|
|
explicitly_enabled.policy.cluster.allow_mainnet = true;
|
|
explicitly_enabled.policy.cluster.mainnet_confirmation = true;
|
|
let enabled_evaluation = crate::ExSafetyChecker
|
|
.evaluate_prepared_plan(&explicitly_enabled)
|
|
.unwrap_or_else(|error| {
|
|
panic!("enabled Mainnet safety evaluation failed: {error}")
|
|
});
|
|
assert_eq!(enabled_evaluation.decision, crate::ExSafetyDecision::Allow);
|
|
} else {
|
|
assert_eq!(evaluation.decision, crate::ExSafetyDecision::Allow);
|
|
}
|
|
}
|
|
}
|
|
}
|