use super::*;
use solana_instruction::{AccountMeta, Instruction};
use spl_discriminator::{ArrayDiscriminator, SplDiscriminate};
use spl_tlv_account_resolution::account::ExtraAccountMeta;
use spl_tlv_account_resolution::pubkey_data::PubkeyData;
use spl_tlv_account_resolution::seeds::Seed;
use spl_tlv_account_resolution::state::ExtraAccountMetaList;
use spl_token_2022_interface::extension::ExtensionType;
use crate::constants::{
TOKEN_DECIMALS, TOKEN_PROGRAM_ID, USDC_MINT, USDC_MINT_INFO, WSOL_MINT,
WSOL_MINT_INFO,
};
use crate::transfer_fee::{FeeSchedule, TransferFee, TransferFeeConfig};
use crate::transfer_hook::{
EXECUTE_DISCRIMINATOR, Fetched, find_validation_address,
};
use crate::{dex, launchpad};
const BASE: Address = Address::new_from_array([9; 32]);
const QUOTE: Address = Address::new_from_array([21; 32]);
const PROGRAM: Address = Address::new_from_array([22; 32]);
const USER: Address = Address::new_from_array([5; 32]);
const PARTNER: Address = Address::new_from_array([3; 32]);
const PLATFORM: Address = Address::new_from_array([4; 32]);
const RENT: Address = Address::new_from_array([7; 32]);
const STATE: Address = Address::new_from_array([25; 32]);
const NAMED: Address = Address::new_from_array([26; 32]);
const TRADE_ACCOUNTS: usize = 17;
const MIGRATE_ACCOUNTS: usize = 23;
const MIGRATE_CURVE_INDEX: usize = 1;
const SPL_DECIMALS: u8 = 9;
struct Execute;
impl SplDiscriminate for Execute {
const SPL_DISCRIMINATOR: ArrayDiscriminator =
ArrayDiscriminator::new(EXECUTE_DISCRIMINATOR);
}
fn list(extras: &[ExtraAccountMeta]) -> Vec<u8> {
let mut data =
vec![0; ExtraAccountMetaList::size_of(extras.len()).unwrap()];
ExtraAccountMetaList::init::<Execute>(&mut data, extras).unwrap();
data
}
fn fixed(pubkey: Address) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_pubkey(&pubkey, false, false).unwrap()
}
fn pda(seeds: &[Seed]) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_seeds(seeds, false, false).unwrap()
}
const fn account_data(account_index: u8, data_index: u8, length: u8) -> Seed {
Seed::AccountData {
account_index,
data_index,
length,
}
}
const fn account_key(index: u8) -> Seed {
Seed::AccountKey { index }
}
fn pubkey_data(account: u8, offset: u8) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_pubkey_data(
&PubkeyData::AccountData {
account_index: account,
data_index: offset,
},
false,
false,
)
.unwrap()
}
fn hook_pda(seeds: &[&[u8]]) -> Address {
Address::find_program_address(seeds, &PROGRAM).0
}
fn validation(mint: &Address) -> Address {
find_validation_address(mint, &PROGRAM)
}
fn with_tail(mint: &Address, extras: Vec<AccountMeta>) -> Vec<AccountMeta> {
[
extras,
vec![
AccountMeta::new_readonly(PROGRAM, false),
AccountMeta::new_readonly(validation(mint), false),
],
]
.concat()
}
fn spl_mint() -> Vec<u8> {
let mut data = vec![0; 82];
data[44] = SPL_DECIMALS;
data[45] = 1;
data
}
fn token_2022_mint(extensions: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut data = vec![0; 165];
data[44] = TOKEN_DECIMALS;
data[45] = 1;
data.push(1);
for (extension_type, payload) in extensions {
data.extend_from_slice(&extension_type.to_le_bytes());
data.extend_from_slice(
&u16::try_from(payload.len()).unwrap().to_le_bytes(),
);
data.extend_from_slice(payload);
}
data
}
fn hook_extension(program_id: Address) -> (u16, Vec<u8>) {
(
ExtensionType::TransferHook.into(),
[[7; 32], program_id.to_bytes()].concat(),
)
}
fn pausable(paused: bool) -> (u16, Vec<u8>) {
let mut payload = vec![7; 32];
payload.push(u8::from(paused));
(ExtensionType::Pausable.into(), payload)
}
fn fee_extension(config: &TransferFeeConfig) -> (u16, Vec<u8>) {
(
ExtensionType::TransferFeeConfig.into(),
bytemuck::bytes_of(config).to_vec(),
)
}
fn entry(epoch: u64, basis_points: u16) -> TransferFee {
TransferFee {
epoch: epoch.into(),
maximum_fee: u64::MAX.into(),
transfer_fee_basis_points: basis_points.into(),
}
}
fn schedule(older_bps: u16, newer_bps: u16) -> TransferFeeConfig {
TransferFeeConfig {
older_transfer_fee: entry(0, older_bps),
newer_transfer_fee: entry(7, newer_bps),
..TransferFeeConfig::default()
}
}
fn flat(bps: u16) -> TransferFeeConfig {
TransferFeeConfig {
newer_transfer_fee: entry(0, bps),
..TransferFeeConfig::default()
}
}
fn view<'a>(owner: &'a Address, data: &'a [u8]) -> AccountView<'a> {
AccountView { owner, data }
}
const SYSVAR: Address =
solana_address::address!("Sysvar1111111111111111111111111111111111111");
fn clock_data(epoch: u64) -> Vec<u8> {
[1_u64, 2, epoch, epoch, 3]
.iter()
.flat_map(|field| field.to_le_bytes())
.collect()
}
fn load_at_clock(
address: Address,
mint: AccountView<'_>,
epoch: u64,
) -> Result<MintInfo, ProgramError> {
MintInfo::load(address, mint, view(&SYSVAR, &clock_data(epoch)))
}
fn token_2022_info(address: Address, data: &[u8]) -> MintInfo {
load_at_clock(address, view(&TOKEN_2022_PROGRAM_ID, data), 0).unwrap()
}
fn base() -> MintInfo {
MintInfo::plain(BASE, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS)
}
fn quote() -> MintInfo {
token_2022_info(QUOTE, &token_2022_mint(&[]))
}
fn hooked(address: Address, data: Vec<u8>) -> MintInfo {
let mut info =
token_2022_info(address, &token_2022_mint(&[hook_extension(PROGRAM)]));
info.insert(
validation(&address),
Some(FetchedAccount {
owner: PROGRAM,
data,
}),
);
info
}
const fn fee(bps: u16) -> MintFee {
MintFee {
bps,
maximum_fee: u64::MAX,
}
}
const fn flat_schedule(bps: u16) -> FeeSchedule {
FeeSchedule {
older: fee(bps),
newer: fee(bps),
newer_epoch: 0,
read_epoch: 0,
}
}
fn with_fee(info: &MintInfo, bps: u16) -> MintInfo {
MintInfo {
transfer_fee: Some(flat_schedule(bps)),
..info.clone()
}
}
fn buy(base: &MintInfo, quote: &MintInfo) -> Result<Instruction, BuildError> {
launchpad::trade::build_buy_exact_in_instruction(
&launchpad::trade::BuildBuyExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_in: 1_000_000,
min_amount_out: 1,
},
)
}
fn migrate(quote: &MintInfo) -> Result<Instruction, BuildError> {
migrate_with(&base(), quote)
}
fn migrate_with(
base: &MintInfo,
quote: &MintInfo,
) -> Result<Instruction, BuildError> {
launchpad::migrate::build_migrate_instruction(
&launchpad::migrate::BuildMigrateParams {
caller: &USER,
base_mint_info: base,
quote_mint_info: quote,
},
)
}
fn quotes(
base: &MintInfo,
quote: &MintInfo,
reserves: u64,
) -> Vec<Result<Instruction, BuildError>> {
use dex::trade as dt;
use launchpad::trade as lt;
vec![
lt::buy_exact_in(<::BuyExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
virtual_base_reserves: reserves,
virtual_quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
real_base_reserves: reserves,
quote_amount: 1_000_000_000,
})
.map(|(ix, _)| ix),
lt::buy_exact_out(<::BuyExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
virtual_base_reserves: reserves,
virtual_quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
real_base_reserves: reserves,
base_amount: 1_000_000_000,
})
.map(|(ix, _)| ix),
lt::sell_exact_in(<::SellExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
virtual_base_reserves: reserves,
virtual_quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
base_amount: 1_000_000_000,
})
.map(|(ix, _)| ix),
lt::sell_exact_out(<::SellExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
virtual_base_reserves: reserves,
virtual_quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
quote_amount: 100_000_000,
})
.map(|(ix, _)| ix),
dt::buy_exact_in(&dt::BuyExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
base_reserves: reserves,
quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
quote_amount: 1_000_000_000,
})
.map(|(ix, _)| ix),
dt::buy_exact_out(&dt::BuyExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
base_reserves: reserves,
quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
base_amount: 1_000_000_000,
})
.map(|(ix, _)| ix),
dt::sell_exact_in(&dt::SellExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
base_reserves: reserves,
quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
base_amount: 1_000_000_000,
})
.map(|(ix, _)| ix),
dt::sell_exact_out(&dt::SellExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
base_reserves: reserves,
quote_reserves: reserves,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
quote_amount: 100_000_000,
})
.map(|(ix, _)| ix),
]
}
const RESERVES: u64 = 30_000_000_000;
fn builds(
base: &MintInfo,
quote: &MintInfo,
) -> Vec<Result<Instruction, BuildError>> {
use dex::trade as dt;
use launchpad::trade as lt;
vec![
buy(base, quote),
lt::build_buy_exact_out_instruction(<::BuildBuyExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_out: 1_000_000,
max_amount_in: 9,
}),
lt::build_sell_exact_in_instruction(<::BuildSellExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_in: 1_000_000,
min_amount_out: 1,
}),
lt::build_sell_exact_out_instruction(<::BuildSellExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_out: 1_000_000,
max_amount_in: 9,
}),
dt::build_buy_exact_in_instruction(&dt::BuildBuyExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_in: 1_000_000,
min_amount_out: 1,
}),
dt::build_buy_exact_out_instruction(&dt::BuildBuyExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_out: 1_000_000,
max_amount_in: 9,
}),
dt::build_sell_exact_in_instruction(&dt::BuildSellExactInParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_in: 1_000_000,
min_amount_out: 1,
}),
dt::build_sell_exact_out_instruction(&dt::BuildSellExactOutParams {
user: &USER,
payer: None,
base_mint_info: base,
quote_mint_info: quote,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_out: 1_000_000,
max_amount_in: 9,
}),
]
}
fn every_trade(
base: &MintInfo,
quote: &MintInfo,
) -> Vec<Result<Instruction, BuildError>> {
[quotes(base, quote, RESERVES), builds(base, quote)].concat()
}
#[test]
fn load_reads_an_spl_token_mint() {
assert_eq!(
load_at_clock(WSOL_MINT, view(&TOKEN_PROGRAM_ID, &spl_mint()), 12),
Ok(MintInfo {
address: WSOL_MINT,
token_program: TOKEN_PROGRAM_ID,
decimals: SPL_DECIMALS,
transfer_fee: None,
hook_program: None,
hook: None,
})
);
}
#[test]
fn load_reads_a_token_2022_mint_with_a_fee_a_pause_and_a_hook() {
let config = schedule(100, 300);
let data = token_2022_mint(&[
fee_extension(&config),
pausable(true),
hook_extension(PROGRAM),
]);
let mut hook = HookState::default();
hook.insert(
QUOTE,
Some(FetchedAccount {
owner: TOKEN_2022_PROGRAM_ID,
data: data.clone(),
}),
);
assert_eq!(
load_at_clock(QUOTE, view(&TOKEN_2022_PROGRAM_ID, &data), 700),
Ok(MintInfo {
address: QUOTE,
token_program: TOKEN_2022_PROGRAM_ID,
decimals: TOKEN_DECIMALS,
transfer_fee: Some(FeeSchedule {
older: fee(100),
newer: fee(300),
newer_epoch: 7,
read_epoch: 700,
}),
hook_program: Some(PROGRAM),
hook: Some(hook),
})
);
}
#[test]
fn load_of_an_unset_hook_has_no_hook() {
let data =
token_2022_mint(&[hook_extension(Address::default()), pausable(false)]);
assert_eq!(
load_at_clock(QUOTE, view(&TOKEN_2022_PROGRAM_ID, &data), 0),
Ok(MintInfo::plain(
QUOTE,
TOKEN_2022_PROGRAM_ID,
TOKEN_DECIMALS
))
);
}
#[test]
fn load_refuses_what_mint_state_refuses() {
let data = token_2022_mint(&[hook_extension(PROGRAM)]);
assert_eq!(
load_at_clock(QUOTE, view(&PROGRAM, &data), 0),
Err(ProgramError::IncorrectProgramId)
);
let cut = &data[..data.len() - 1];
assert_eq!(
load_at_clock(QUOTE, view(&TOKEN_2022_PROGRAM_ID, cut), 0),
Err(ProgramError::InvalidAccountData)
);
assert_eq!(
load_at_clock(QUOTE, view(&TOKEN_PROGRAM_ID, &data), 0),
Err(ProgramError::InvalidAccountData)
);
}
#[test]
fn load_refuses_an_account_that_is_not_a_clock() {
let mint = spl_mint();
let mint = view(&TOKEN_PROGRAM_ID, &mint);
let clock = clock_data(0);
assert_eq!(
MintInfo::load(WSOL_MINT, mint, view(&TOKEN_PROGRAM_ID, &clock)),
Err(ProgramError::IncorrectProgramId)
);
let long = [clock.as_slice(), &[0]].concat();
for data in [&clock[..23], &clock[..33], long.as_slice()] {
assert_eq!(
MintInfo::load(WSOL_MINT, mint, view(&SYSVAR, data)),
Err(ProgramError::InvalidAccountData)
);
}
}
#[test]
fn plain_is_a_mint_with_no_fee_and_no_hook() {
assert_eq!(
MintInfo::plain(USDC_MINT, TOKEN_PROGRAM_ID, 6),
MintInfo {
address: USDC_MINT,
token_program: TOKEN_PROGRAM_ID,
decimals: 6,
transfer_fee: None,
hook_program: None,
hook: None,
}
);
assert_eq!(
MintInfo::plain(USDC_MINT, TOKEN_PROGRAM_ID, 6).validation_address(),
None
);
assert_eq!(
base(),
MintInfo::plain(BASE, TOKEN_2022_PROGRAM_ID, TOKEN_DECIMALS)
);
assert_eq!(MintInfo::launch(BASE), base());
}
#[test]
fn the_wsol_and_usdc_infos_are_plain() {
assert_eq!(
WSOL_MINT_INFO,
MintInfo::plain(WSOL_MINT, TOKEN_PROGRAM_ID, SPL_DECIMALS)
);
assert_eq!(
USDC_MINT_INFO,
MintInfo::plain(USDC_MINT, TOKEN_PROGRAM_ID, 6)
);
}
#[test]
fn missing_names_the_list_then_the_fixed_reads() {
let mut info =
token_2022_info(QUOTE, &token_2022_mint(&[hook_extension(PROGRAM)]));
assert_eq!(info.missing(), Ok(vec![validation("E)]));
info.insert(
validation("E),
Some(FetchedAccount {
owner: PROGRAM,
data: list(&[fixed(STATE), pubkey_data(5, 8)]),
}),
);
assert_eq!(info.missing(), Ok(vec![STATE]));
info.insert(
STATE,
Some(FetchedAccount {
owner: PROGRAM,
data: [[0; 8].as_slice(), NAMED.as_ref()].concat(),
}),
);
assert_eq!(info.missing(), Ok(Vec::new()));
}
#[test]
fn missing_of_a_mint_with_no_hook_is_empty() {
assert_eq!(quote().missing(), Ok(Vec::new()));
assert_eq!(base().missing(), Ok(Vec::new()));
}
#[test]
fn insert_records_into_the_hook() {
let mut info = hooked(QUOTE, list(&[fixed(RENT)]));
info.insert(STATE, None);
assert_eq!(
info.hook.as_ref().map(|hook| hook.fetched(&STATE)),
Some(Fetched::Missing)
);
}
#[test]
fn insert_without_a_hook_does_nothing() {
let mut info = quote();
info.insert(
STATE,
Some(FetchedAccount {
owner: PROGRAM,
data: vec![1],
}),
);
assert_eq!(info, quote());
}
#[test]
fn two_hook_free_mints_append_nothing() {
let built = buy(&base(), "e()).unwrap();
assert_eq!(built.accounts.len(), TRADE_ACCOUNTS);
}
#[test]
fn a_hook_program_with_no_hook_is_refused() {
let mut quote = hooked(QUOTE, list(&[fixed(RENT)]));
quote.hook = None;
assert_eq!(
buy(&base(), "e).err(),
Some(BuildError::HookNotRead(QUOTE))
);
}
#[test]
fn two_hooked_mints_each_resolve_their_own_list() {
let base = hooked(BASE, list(&[fixed(RENT)]));
let quote = hooked(QUOTE, list(&[fixed(STATE)]));
let built = buy(&base, "e).unwrap();
assert_eq!(
built.accounts[TRADE_ACCOUNTS..],
[
with_tail("E, vec![AccountMeta::new_readonly(STATE, false)]),
vec![
AccountMeta::new_readonly(RENT, false),
AccountMeta::new_readonly(validation(&BASE), false),
],
]
.concat()
);
}
#[test]
fn a_build_names_every_list_that_it_lacks() {
let unfilled = |address| {
token_2022_info(address, &token_2022_mint(&[hook_extension(PROGRAM)]))
};
assert_eq!(
buy(&unfilled(BASE), &unfilled(QUOTE)),
Err(BuildError::Hook(HookError::NotLoaded(vec![
validation(&BASE),
validation("E),
])))
);
}
#[test]
fn a_quote_list_reads_the_base_mint_from_its_own_snapshot() {
let base_data = token_2022_mint(&[]);
let mut quote =
hooked(QUOTE, list(&[fixed(BASE), pda(&[account_data(5, 44, 2)])]));
assert_eq!(quote.missing(), Ok(vec![BASE]));
assert_eq!(
buy(&base(), "e),
Err(BuildError::Hook(HookError::NotLoaded(vec![BASE])))
);
quote.insert(
BASE,
Some(FetchedAccount {
owner: TOKEN_2022_PROGRAM_ID,
data: base_data,
}),
);
assert_eq!(quote.missing(), Ok(Vec::new()));
let built = buy(&base(), "e).unwrap();
assert_eq!(
built.accounts[TRADE_ACCOUNTS..],
with_tail(
"E,
vec![
AccountMeta::new_readonly(BASE, false),
AccountMeta::new_readonly(
hook_pda(&[&[TOKEN_DECIMALS, 1]]),
false
),
]
)
);
}
#[test]
fn load_takes_the_fee_of_the_clock_epoch() {
let data = token_2022_mint(&[fee_extension(&schedule(100, 300))]);
let fee = |epoch| {
load_at_clock(QUOTE, view(&TOKEN_2022_PROGRAM_ID, &data), epoch)
.unwrap()
.transfer_fee
.map(|fee| fee.fee().bps)
};
assert_eq!([fee(6), fee(7), fee(8)], [Some(100), Some(300), Some(300)]);
assert_eq!(quote().transfer_fee, None);
}
#[test]
fn the_schedule_gives_the_fee_at_any_epoch() {
let data = token_2022_mint(&[fee_extension(&schedule(100, 300))]);
let schedule =
MintInfo::load_at(QUOTE, view(&TOKEN_2022_PROGRAM_ID, &data), 6)
.unwrap()
.transfer_fee
.unwrap();
assert_eq!(
schedule,
FeeSchedule {
older: fee(100),
newer: fee(300),
newer_epoch: 7,
read_epoch: 6,
}
);
assert_eq!(schedule.fee_at(6), fee(100));
assert_eq!(schedule.fee_at(7), fee(300));
assert_eq!(schedule.fee_at(8), fee(300));
assert_eq!(schedule.fee(), schedule.fee_at(6));
assert_eq!(schedule.fee(), fee(100));
}
#[test]
fn a_zero_bps_config_is_a_schedule() {
let data = token_2022_mint(&[fee_extension(&flat(0))]);
assert_eq!(
MintInfo::load_at(QUOTE, view(&TOKEN_2022_PROGRAM_ID, &data), 9)
.unwrap()
.transfer_fee,
Some(FeeSchedule {
older: MintFee {
bps: 0,
maximum_fee: 0,
},
newer: fee(0),
newer_epoch: 0,
read_epoch: 9,
})
);
}
#[test]
fn load_at_is_load_at_the_clock_epoch() {
let spl = spl_mint();
let hooked = token_2022_mint(&[
fee_extension(&schedule(100, 300)),
hook_extension(PROGRAM),
]);
for (owner, data) in [
(TOKEN_PROGRAM_ID, spl.as_slice()),
(TOKEN_2022_PROGRAM_ID, hooked.as_slice()),
] {
for epoch in [0, 6, 7, 8, 700, u64::MAX] {
assert_eq!(
MintInfo::load_at(QUOTE, view(&owner, data), epoch),
load_at_clock(QUOTE, view(&owner, data), epoch),
"{owner} at {epoch}"
);
}
}
assert_eq!(
MintInfo::load_at(QUOTE, view(&PROGRAM, &hooked), 0),
Err(ProgramError::IncorrectProgramId)
);
}
#[test]
fn load_sets_the_hook_program_of_a_hooked_mint_alone() {
let hooked =
token_2022_info(QUOTE, &token_2022_mint(&[hook_extension(PROGRAM)]));
assert_eq!(hooked.hook_program, Some(PROGRAM));
assert_eq!(hooked.validation_address(), Some(validation("E)));
assert_eq!(
hooked.validation_address(),
Some(
Address::find_program_address(
&[b"extra-account-metas", QUOTE.as_ref()],
&PROGRAM
)
.0
)
);
for hook_free in [
quote(),
token_2022_info(
QUOTE,
&token_2022_mint(&[hook_extension(Address::default())]),
),
load_at_clock(WSOL_MINT, view(&TOKEN_PROGRAM_ID, &spl_mint()), 0)
.unwrap(),
] {
assert_eq!(hook_free.hook_program, None);
assert_eq!(hook_free.hook, None);
assert_eq!(hook_free.validation_address(), None);
}
}
#[test]
fn the_trade_helpers_apply_each_mints_fee() {
let with = |base_bps, quote_bps| {
quotes(
&with_fee(&base(), base_bps),
&with_fee("e(), quote_bps),
RESERVES,
)
};
let fee_free = quotes(&base(), "e(), RESERVES);
for (index, built) in [with(100, 200), with(200, 100), with(300, 400)]
.into_iter()
.enumerate()
{
for (trade, (with_fees, without)) in
built.iter().zip(&fee_free).enumerate()
{
assert_ne!(with_fees, without, "{index}: trade {trade}");
}
}
for (trade, (one, swapped)) in
with(100, 200).iter().zip(&with(200, 100)).enumerate()
{
assert_ne!(one, swapped, "trade {trade}");
}
}
#[test]
fn every_trade_builder_refuses_a_base_mint_that_is_not_a_token_2022_mint() {
let spl_base = MintInfo::plain(BASE, TOKEN_PROGRAM_ID, SPL_DECIMALS);
for (trade, built) in
every_trade(&spl_base, "e()).into_iter().enumerate()
{
assert_eq!(
built,
Err(BuildError::BaseNotToken2022(BASE)),
"trade {trade}"
);
}
}
#[test]
fn every_trade_builder_takes_an_spl_token_quote_mint() {
for (trade, built) in every_trade(&base(), &WSOL_MINT_INFO)
.into_iter()
.enumerate()
{
assert_eq!(
built.map(|ix| ix.accounts.len()),
Ok(TRADE_ACCOUNTS),
"trade {trade}"
);
}
}
#[test]
fn the_trade_helpers_check_the_mints_before_the_math() {
for (trade, built) in quotes(&base(), "e(), 0).into_iter().enumerate() {
assert!(
matches!(built, Err(BuildError::Quote(_))),
"trade {trade}: {built:?}"
);
}
let spl_base = MintInfo::plain(BASE, TOKEN_PROGRAM_ID, SPL_DECIMALS);
for (trade, built) in quotes(&spl_base, "e(), 0).into_iter().enumerate()
{
assert_eq!(
built,
Err(BuildError::BaseNotToken2022(BASE)),
"trade {trade}"
);
}
}
#[test]
fn build_error_display() {
assert_eq!(
BuildError::BaseNotToken2022(USDC_MINT).to_string(),
"the base mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v is not a \
Token-2022 mint"
);
let hook = HookError::NotLoaded(vec![STATE]);
assert_eq!(BuildError::Hook(hook.clone()).to_string(), hook.to_string());
let quote = crate::math::amm::QuoteError::InvalidTransferFee;
assert_eq!(BuildError::Quote(quote).to_string(), quote.to_string());
}
#[test]
fn no_wrapper_error_has_a_source() {
use crate::math::amm::{AmmError, QuoteError};
use crate::utils::MarketQuoteError;
let source =
|error: &dyn std::error::Error| error.source().map(ToString::to_string);
let quote = QuoteError::Amm(AmmError::Overflow);
assert_eq!(quote.to_string(), AmmError::Overflow.to_string());
assert_eq!(source("e), None);
assert_eq!(source(&MarketQuoteError::Quote(quote)), None);
assert_eq!(source(&BuildError::Quote(quote)), None);
assert_eq!(source(&BuildError::Hook(HookError::NoList)), None);
assert_eq!(source(&BuildError::BaseNotToken2022(BASE)), None);
}
#[test]
fn migrate_refuses_a_base_mint_that_is_not_a_token_2022_mint() {
let spl_base = MintInfo::plain(BASE, TOKEN_PROGRAM_ID, SPL_DECIMALS);
assert_eq!(
migrate_with(&spl_base, "e()),
Err(BuildError::BaseNotToken2022(BASE))
);
}
#[test]
fn migrate_appends_the_hooked_base_mints_accounts() {
let built =
migrate_with(&hooked(BASE, list(&[fixed(RENT)])), "e()).unwrap();
assert_eq!(
built.accounts[..MIGRATE_ACCOUNTS],
migrate("e()).unwrap().accounts[..]
);
assert_eq!(
built.accounts[MIGRATE_ACCOUNTS..],
with_tail(&BASE, vec![AccountMeta::new_readonly(RENT, false)])
);
}
#[test]
fn migrate_appends_the_hooked_quote_mints_accounts() {
let built =
migrate(&hooked(QUOTE, list(&[fixed(RENT), pda(&[account_key(3)])])))
.unwrap();
let curve = built.accounts[MIGRATE_CURVE_INDEX].pubkey;
assert_eq!(
built.accounts[..MIGRATE_ACCOUNTS],
migrate("e()).unwrap().accounts[..]
);
assert_eq!(
built.accounts[MIGRATE_ACCOUNTS..],
with_tail(
"E,
vec![
AccountMeta::new_readonly(RENT, false),
AccountMeta::new_readonly(hook_pda(&[curve.as_ref()]), false),
]
)
);
}
#[test]
fn migrate_names_the_curve_a_list_reads() {
let mut quote = hooked(QUOTE, list(&[pda(&[account_data(3, 0, 8)])]));
let curve =
migrate(&self::quote()).unwrap().accounts[MIGRATE_CURVE_INDEX].pubkey;
assert_eq!(quote.missing(), Ok(Vec::new()));
assert_eq!(
migrate("e),
Err(BuildError::Hook(HookError::NotLoaded(vec![curve])))
);
quote.insert(
curve,
Some(FetchedAccount {
owner: crate::constants::LAUNCHPAD_PROGRAM_ID,
data: vec![5; 16],
}),
);
assert_eq!(
migrate("e).unwrap().accounts[MIGRATE_ACCOUNTS..],
with_tail(
"E,
vec![AccountMeta::new_readonly(hook_pda(&[&[5; 8]]), false)]
)
);
}