pub mod accounts;
pub mod error;
pub use self::error::TokenError;
mod approve;
mod approve_checked;
mod burn;
mod burn_checked;
mod close_account;
mod create_account;
mod create_ata;
mod create_ata_idempotent;
mod create_mint;
mod create_multisig;
#[cfg(not(feature = "token-2022"))]
mod create_native_mint;
#[cfg(feature = "token-2022")]
mod create_native_mint_2022;
mod freeze_account;
mod mint_to;
mod mint_to_checked;
mod revoke;
mod set_authority;
mod sync_native;
mod thaw_account;
mod transfer;
mod transfer_checked;
use solana_address::Address;
use solana_keypair::Keypair;
use solana_program_pack::{IsInitialized, Pack};
use solana_signer::Signer;
use solana_transaction::Transaction;
#[cfg(feature = "token-2022")]
pub use spl_token_2022_interface as spl_token;
#[cfg(not(feature = "token-2022"))]
pub use spl_token_interface as spl_token;
#[cfg(feature = "token-2022")]
use self::create_native_mint_2022 as create_native_mint;
pub use self::{
approve::*, approve_checked::*, burn::*, burn_checked::*, close_account::*, create_account::*,
create_ata::*, create_ata_idempotent::*, create_mint::*, create_multisig::*,
create_native_mint::*, freeze_account::*, mint_to::*, mint_to_checked::*, revoke::*,
set_authority::*, sync_native::*, thaw_account::*, transfer::*, transfer_checked::*,
};
#[cfg(test)]
use crate::Hpsvm;
use crate::{Svm, SvmTypesHost, types::FailedTransactionMetadata};
pub const TOKEN_ID: Address = spl_token::ID;
pub fn get_spl_account<A: Pack + IsInitialized, T: SvmTypesHost>(
svm: &Svm<'_, T>,
account: &Address,
) -> Result<A, TokenError> {
let account = svm.get_account(account).ok_or(TokenError::AccountNotFound(*account))?;
let data = account
.data
.get(..A::LEN)
.ok_or(TokenError::AccountDataTooSmall { expected: A::LEN, actual: account.data.len() })?;
let account = A::unpack(data)?;
Ok(account)
}
fn get_multisig_signers<'a>(
authority: &Address,
signing_pubkeys: &'a [Address],
) -> Vec<&'a Address> {
if signing_pubkeys == [*authority] {
vec![]
} else {
signing_pubkeys.iter().collect::<Vec<_>>()
}
}
pub(crate) fn sign_and_send<T: SvmTypesHost>(
svm: &mut Svm<'_, T>,
payer: &Keypair,
signers: &[&Keypair],
ix: solana_instruction::Instruction,
) -> Result<(), FailedTransactionMetadata> {
let payer_pk = payer.pubkey();
let block_hash = svm.latest_blockhash();
let mut tx = Transaction::new_with_payer(&[ix], Some(&payer_pk));
tx.partial_sign(&[payer], block_hash);
tx.partial_sign(signers, block_hash);
svm.send_transaction(tx)?;
Ok(())
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use solana_program_option::COption;
use solana_program_pack::IsInitialized;
use super::*;
use crate::token::accounts::{keyed_mint_account, keyed_system_account, system_account};
fn sample_mint() -> spl_token::state::Mint {
spl_token::state::Mint {
mint_authority: COption::Some(Address::new_unique()),
supply: 1_000,
decimals: 6,
is_initialized: true,
..Default::default()
}
}
#[test]
fn multisig_signers_are_empty_for_a_single_owner() {
let authority = Address::new_unique();
assert!(get_multisig_signers(&authority, &[authority]).is_empty());
}
#[test]
fn multisig_signers_are_forwarded_for_multi_owner_authorities() {
let authority = Address::new_unique();
let signers = [Address::new_unique(), authority, Address::new_unique()];
assert_eq!(
get_multisig_signers(&authority, &signers),
vec![&signers[0], &signers[1], &signers[2]]
);
}
#[test]
fn multisig_signers_of_an_empty_list_are_empty() {
let authority = Address::new_unique();
assert!(get_multisig_signers(&authority, &[]).is_empty());
}
#[test]
fn two_distinct_signers_are_not_collapsed() {
let authority = Address::new_unique();
let other = Address::new_unique();
assert_eq!(get_multisig_signers(&authority, &[authority, other]).len(), 2);
}
#[test]
fn get_spl_account_reports_a_missing_account() {
let svm = Hpsvm::new();
let address = Address::new_unique();
match get_spl_account::<spl_token::state::Mint, _>(&svm, &address) {
Err(TokenError::AccountNotFound(missing)) => assert_eq!(missing, address),
other => panic!("expected TokenError::AccountNotFound, got {other:?}"),
}
}
#[test]
fn get_spl_account_reports_short_account_data() {
let mut svm = Hpsvm::new();
let address = Address::new_unique();
svm.set_account(address, system_account(1_000)).unwrap();
match get_spl_account::<spl_token::state::Mint, _>(&svm, &address) {
Err(TokenError::AccountDataTooSmall { expected, actual }) => {
assert_eq!(expected, spl_token::state::Mint::LEN);
assert_eq!(actual, 0);
}
other => panic!("expected TokenError::AccountDataTooSmall, got {other:?}"),
}
}
#[test]
fn get_spl_account_ignores_trailing_padding() {
let mut svm = Hpsvm::new();
let address = Address::new_unique();
let mint_state = sample_mint();
let mut data = vec![0u8; spl_token::state::Mint::LEN + 32];
let (head, _padding) = data.split_at_mut(spl_token::state::Mint::LEN);
spl_token::state::Mint::pack(mint_state, head).unwrap();
let account = solana_account::Account {
lamports: 1,
owner: TOKEN_ID,
data,
executable: false,
rent_epoch: 0,
};
svm.set_account(address, account).unwrap();
assert_eq!(
get_spl_account::<spl_token::state::Mint, _>(&svm, &address).unwrap(),
mint_state
);
}
#[test]
fn get_spl_account_surfaces_unpack_failures() {
let mut svm = Hpsvm::new();
let address = Address::new_unique();
let account = solana_account::Account {
lamports: 1,
owner: TOKEN_ID,
data: vec![0u8; spl_token::state::Mint::LEN],
executable: false,
rent_epoch: 0,
};
svm.set_account(address, account).unwrap();
match get_spl_account::<spl_token::state::Mint, _>(&svm, &address) {
Err(TokenError::UnpackError(_)) => {}
other => panic!("expected TokenError::UnpackError, got {other:?}"),
}
}
#[test]
fn get_spl_account_round_trips_a_mint_installed_by_the_factory() {
let mut svm = Hpsvm::new();
let address = Address::new_unique();
let mint_state = sample_mint();
set_keyed(&mut svm, keyed_mint_account(address, mint_state));
let unpacked = get_spl_account::<spl_token::state::Mint, _>(&svm, &address).unwrap();
assert_eq!(unpacked, mint_state);
assert!(unpacked.is_initialized());
}
#[test]
fn get_spl_account_round_trips_a_token_account_installed_by_the_factory() {
use crate::token::accounts::keyed_token_account;
let mut svm = Hpsvm::new();
let address = Address::new_unique();
let mint = Address::new_unique();
let token = spl_token::state::Account {
mint,
owner: Address::new_unique(),
amount: 77,
state: spl_token::state::AccountState::Initialized,
..Default::default()
};
set_keyed(&mut svm, keyed_token_account(address, token));
assert_eq!(get_spl_account::<spl_token::state::Account, _>(&svm, &address).unwrap(), token);
}
#[test]
fn get_spl_account_prefers_not_found_over_size_checks() {
let svm = Hpsvm::new();
let address = Address::new_unique();
assert!(matches!(
get_spl_account::<spl_token::state::Account, _>(&svm, &address),
Err(TokenError::AccountNotFound(_))
));
}
#[test]
fn get_spl_account_does_not_check_account_ownership() {
let mut svm = Hpsvm::new();
let address = Address::new_unique();
let mint_state = sample_mint();
let (key, account) = keyed_mint_account(address, mint_state);
svm.set_account(key, solana_account::Account { owner: Address::new_unique(), ..account })
.unwrap();
assert_eq!(
get_spl_account::<spl_token::state::Mint, _>(&svm, &address).unwrap(),
mint_state
);
}
#[test]
fn get_spl_account_reports_a_missing_account_even_when_other_accounts_exist() {
let mut svm = Hpsvm::new();
let present = Address::new_unique();
let absent = Address::new_unique();
svm.set_account(
present,
solana_account::Account {
lamports: 5,
owner: Address::new_unique(),
..Default::default()
},
)
.unwrap();
assert!(matches!(
get_spl_account::<spl_token::state::Mint, _>(&svm, &absent),
Err(TokenError::AccountNotFound(missing)) if missing == absent
));
}
fn set_keyed(svm: &mut Hpsvm, keyed: (Address, solana_account::Account)) {
let (address, account) = keyed;
svm.set_account(address, account).unwrap();
}
proptest! {
#[test]
fn multisig_signer_collapse_is_exactly_the_single_authority_case(
extra in prop::collection::vec(any::<[u8; 32]>(), 0..4),
) {
let authority = Address::new_unique();
let mut signing: Vec<Address> =
extra.iter().map(|bytes| Address::new_from_array(*bytes)).collect();
signing.push(authority);
let collapses = signing.len() == 1;
let resolved = get_multisig_signers(&authority, &signing);
if collapses {
prop_assert!(resolved.is_empty());
} else {
prop_assert_eq!(resolved.len(), signing.len());
}
}
}
proptest! {
#[test]
fn multisig_signers_preserve_input_order(
keys in prop::collection::vec(any::<[u8; 32]>(), 0..8),
) {
let authority = Address::new_unique();
let signing: Vec<Address> =
keys.iter().map(|bytes| Address::new_from_array(*bytes)).collect();
let resolved = get_multisig_signers(&authority, &signing);
prop_assert!(resolved.len() <= signing.len());
for signer in resolved {
prop_assert!(signing.contains(signer));
}
}
}
proptest! {
#[test]
fn get_spl_account_always_reports_short_data_for_system_accounts(
lamports in 0u64..1_000_000_000,
) {
let mut svm = Hpsvm::new();
let address = Address::new_unique();
set_keyed(&mut svm, keyed_system_account(address, lamports));
match get_spl_account::<spl_token::state::Mint, _>(&svm, &address) {
Err(TokenError::AccountDataTooSmall { expected, actual }) => {
prop_assert_eq!(expected, spl_token::state::Mint::LEN);
prop_assert_eq!(actual, 0);
}
other => prop_assert!(false, "unexpected result {other:?}"),
}
}
}
}