use crate::{
token::{get_current_transfer_fee, prepare_token_accounts_instructions, TokenAccountStrategy},
FUNDER, SLIPPAGE_TOLERANCE_BPS,
};
use fusionamm_client::{
get_tick_array_address, AccountsType, FusionPool, RemainingAccountsInfo, RemainingAccountsSlice, Swap, SwapInstructionArgs, TickArray,
};
use fusionamm_core::{
get_tick_array_start_tick_index, swap_quote_by_input_token, swap_quote_by_output_token, ExactInSwapQuote, ExactOutSwapQuote, TickArrayFacade,
TickFacade, TICK_ARRAY_SIZE,
};
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_instruction::{AccountMeta, Instruction};
use solana_keypair::Keypair;
use solana_pubkey::Pubkey;
use std::{error::Error, iter::zip};
#[derive(Debug, Clone, PartialEq)]
pub enum SwapType {
ExactIn,
ExactOut,
}
#[derive(Debug, Clone)]
pub enum SwapQuote {
ExactIn(ExactInSwapQuote),
ExactOut(ExactOutSwapQuote),
}
#[derive(Debug)]
pub struct SwapInstructions {
pub instructions: Vec<Instruction>,
pub quote: SwapQuote,
pub additional_signers: Vec<Keypair>,
}
fn uninitialized_tick_array(start_tick_index: i32) -> TickArrayFacade {
TickArrayFacade {
start_tick_index,
ticks: [TickFacade::default(); TICK_ARRAY_SIZE],
}
}
async fn fetch_tick_arrays_or_default(
rpc: &RpcClient,
fusion_pool_address: Pubkey,
fusion_pool: &FusionPool,
) -> Result<[(Pubkey, TickArrayFacade); 5], Box<dyn Error>> {
let tick_array_start_index = get_tick_array_start_tick_index(fusion_pool.tick_current_index, fusion_pool.tick_spacing);
let offset = fusion_pool.tick_spacing as i32 * TICK_ARRAY_SIZE as i32;
let tick_array_indexes = [
tick_array_start_index,
tick_array_start_index + offset,
tick_array_start_index + offset * 2,
tick_array_start_index - offset,
tick_array_start_index - offset * 2,
];
let tick_array_addresses: Vec<Pubkey> = tick_array_indexes
.iter()
.map(|&x| get_tick_array_address(&fusion_pool_address, x).map(|y| y.0))
.collect::<Result<Vec<Pubkey>, _>>()?;
let tick_array_infos = rpc.get_multiple_accounts(&tick_array_addresses).await?;
let maybe_tick_arrays: Vec<Option<TickArrayFacade>> = tick_array_infos
.iter()
.map(|x| x.as_ref().and_then(|y| TickArray::from_bytes(&y.data).ok()))
.map(|x| x.map(|y| y.into()))
.collect();
let tick_arrays: Vec<TickArrayFacade> = maybe_tick_arrays
.iter()
.enumerate()
.map(|(i, x)| x.unwrap_or(uninitialized_tick_array(tick_array_indexes[i])))
.collect::<Vec<TickArrayFacade>>();
let result: [(Pubkey, TickArrayFacade); 5] = zip(tick_array_addresses, tick_arrays)
.collect::<Vec<(Pubkey, TickArrayFacade)>>()
.try_into()
.map_err(|_| "Failed to convert tick arrays to array".to_string())?;
Ok(result)
}
#[cfg(not(doctest))]
pub async fn swap_instructions(
rpc: &RpcClient,
fusion_pool_address: Pubkey,
amount: u64,
specified_mint: Pubkey,
swap_type: SwapType,
slippage_tolerance_bps: Option<u16>,
signer: Option<Pubkey>,
) -> Result<SwapInstructions, Box<dyn Error>> {
let slippage_tolerance_bps = slippage_tolerance_bps.unwrap_or(*SLIPPAGE_TOLERANCE_BPS.try_lock()?);
let signer = signer.unwrap_or(*FUNDER.try_lock()?);
if signer == Pubkey::default() {
return Err("Signer must be provided".into());
}
let fusion_pool_info = rpc.get_account(&fusion_pool_address).await?;
let fusion_pool = FusionPool::from_bytes(&fusion_pool_info.data)?;
let specified_input = swap_type == SwapType::ExactIn;
let specified_token_a = specified_mint == fusion_pool.token_mint_a;
let a_to_b = specified_token_a == specified_input;
let tick_arrays = fetch_tick_arrays_or_default(rpc, fusion_pool_address, &fusion_pool).await?;
let mint_infos = rpc.get_multiple_accounts(&[fusion_pool.token_mint_a, fusion_pool.token_mint_b]).await?;
let mint_a_info = mint_infos[0].as_ref().ok_or(format!("Mint a not found: {}", fusion_pool.token_mint_a))?;
let mint_b_info = mint_infos[1].as_ref().ok_or(format!("Mint b not found: {}", fusion_pool.token_mint_b))?;
let current_epoch = rpc.get_epoch_info().await?.epoch;
let transfer_fee_a = get_current_transfer_fee(Some(mint_a_info), current_epoch);
let transfer_fee_b = get_current_transfer_fee(Some(mint_b_info), current_epoch);
let quote = match swap_type {
SwapType::ExactIn => SwapQuote::ExactIn(swap_quote_by_input_token(
amount,
specified_token_a,
slippage_tolerance_bps,
fusion_pool.clone().into(),
tick_arrays.map(|x| x.1).into(),
transfer_fee_a,
transfer_fee_b,
)?),
SwapType::ExactOut => SwapQuote::ExactOut(swap_quote_by_output_token(
amount,
specified_token_a,
slippage_tolerance_bps,
fusion_pool.clone().into(),
tick_arrays.map(|x| x.1).into(),
transfer_fee_a,
transfer_fee_b,
)?),
};
let max_in_amount = match quote {
SwapQuote::ExactIn(quote) => quote.token_in,
SwapQuote::ExactOut(quote) => quote.token_max_in,
};
let token_a_spec = if a_to_b {
TokenAccountStrategy::WithBalance(fusion_pool.token_mint_a, max_in_amount)
} else {
TokenAccountStrategy::WithoutBalance(fusion_pool.token_mint_a)
};
let token_b_spec = if a_to_b {
TokenAccountStrategy::WithoutBalance(fusion_pool.token_mint_b)
} else {
TokenAccountStrategy::WithBalance(fusion_pool.token_mint_b, max_in_amount)
};
let mut instructions: Vec<Instruction> = Vec::new();
let token_accounts = prepare_token_accounts_instructions(rpc, signer, vec![token_a_spec, token_b_spec]).await?;
instructions.extend(token_accounts.create_instructions);
let other_amount_threshold = match quote {
SwapQuote::ExactIn(quote) => quote.token_min_out,
SwapQuote::ExactOut(quote) => quote.token_max_in,
};
let token_owner_account_a = token_accounts
.token_account_addresses
.get(&fusion_pool.token_mint_a)
.ok_or("Token A owner account not found")?;
let token_owner_account_b = token_accounts
.token_account_addresses
.get(&fusion_pool.token_mint_b)
.ok_or("Token B owner account not found")?;
let swap_instruction = Swap {
token_program_a: mint_a_info.owner,
token_program_b: mint_b_info.owner,
memo_program: spl_memo::ID,
token_authority: signer,
fusion_pool: fusion_pool_address,
token_mint_a: fusion_pool.token_mint_a,
token_mint_b: fusion_pool.token_mint_b,
token_owner_account_a: *token_owner_account_a,
token_vault_a: fusion_pool.token_vault_a,
token_owner_account_b: *token_owner_account_b,
token_vault_b: fusion_pool.token_vault_b,
tick_array0: tick_arrays[0].0,
tick_array1: tick_arrays[1].0,
tick_array2: tick_arrays[2].0,
}
.instruction_with_remaining_accounts(
SwapInstructionArgs {
amount,
other_amount_threshold,
sqrt_price_limit: 0,
amount_specified_is_input: specified_input,
a_to_b,
remaining_accounts_info: Some(RemainingAccountsInfo {
slices: vec![RemainingAccountsSlice {
accounts_type: AccountsType::SupplementalTickArrays,
length: 2,
}],
}),
},
&[AccountMeta::new(tick_arrays[3].0, false), AccountMeta::new(tick_arrays[4].0, false)],
);
instructions.push(swap_instruction);
instructions.extend(token_accounts.cleanup_instructions);
Ok(SwapInstructions {
instructions,
quote,
additional_signers: token_accounts.additional_signers,
})
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use std::error::Error;
use rstest::rstest;
use serial_test::serial;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_keypair::Keypair;
use solana_program::program_pack::Pack;
use solana_program_test::tokio;
use solana_pubkey::Pubkey;
use solana_signer::Signer;
use spl_token::state::Account as TokenAccount;
use spl_token_2022::{extension::StateWithExtensionsOwned, state::Account as TokenAccount2022, ID as TOKEN_2022_PROGRAM_ID};
use crate::{
increase_liquidity_instructions, swap_instructions,
tests::{
setup_ata_te, setup_ata_with_amount, setup_fusion_pool, setup_mint_te, setup_mint_te_fee, setup_mint_with_decimals, setup_position,
RpcContext, SetupAtaConfig,
},
IncreaseLiquidityParam, SwapInstructions, SwapQuote, SwapType,
};
async fn get_token_balance(rpc: &RpcClient, address: Pubkey) -> Result<u64, Box<dyn Error>> {
let account_data = rpc.get_account(&address).await?;
if account_data.owner == TOKEN_2022_PROGRAM_ID {
let parsed = StateWithExtensionsOwned::<TokenAccount2022>::unpack(account_data.data)?;
Ok(parsed.base.amount)
} else {
let parsed = TokenAccount::unpack(&account_data.data)?;
Ok(parsed.amount)
}
}
async fn setup_all_mints(ctx: &RpcContext) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
let mint_a = setup_mint_with_decimals(ctx, 9).await?;
let mint_b = setup_mint_with_decimals(ctx, 9).await?;
let mint_te_a = setup_mint_te(ctx, &[]).await?;
let mint_te_b = setup_mint_te(ctx, &[]).await?;
let mint_tefee = setup_mint_te_fee(ctx).await?;
let mut out = HashMap::new();
out.insert("A", mint_a);
out.insert("B", mint_b);
out.insert("TEA", mint_te_a);
out.insert("TEB", mint_te_b);
out.insert("TEFee", mint_tefee);
Ok(out)
}
async fn setup_all_atas(ctx: &RpcContext, minted: &HashMap<&str, Pubkey>) -> Result<HashMap<&'static str, Pubkey>, Box<dyn Error>> {
let token_balance = 1_000_000_000;
let ata_a = setup_ata_with_amount(ctx, minted["A"], token_balance).await?;
let ata_b = setup_ata_with_amount(ctx, minted["B"], token_balance).await?;
let ata_te_a = setup_ata_te(ctx, minted["TEA"], Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
let ata_te_b = setup_ata_te(ctx, minted["TEB"], Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
let ata_tefee = setup_ata_te(ctx, minted["TEFee"], Some(SetupAtaConfig { amount: Some(token_balance) })).await?;
let mut out = HashMap::new();
out.insert("A", ata_a);
out.insert("B", ata_b);
out.insert("TEA", ata_te_a);
out.insert("TEB", ata_te_b);
out.insert("TEFee", ata_tefee);
Ok(out)
}
fn parse_pool_name(pool: &str) -> (&'static str, &'static str) {
match pool {
"A-B" => ("A", "B"),
"A-TEA" => ("A", "TEA"),
"TEA-TEB" => ("TEA", "TEB"),
"A-TEFee" => ("A", "TEFee"),
_ => panic!("Unknown pool combo: {}", pool),
}
}
async fn verify_swap(
ctx: &RpcContext,
swap_ix: &SwapInstructions,
user_ata_for_final_a: Pubkey,
user_ata_for_final_b: Pubkey,
a_to_b: bool,
) -> Result<(), Box<dyn Error>> {
let before_a = get_token_balance(&ctx.rpc, user_ata_for_final_a).await?;
let before_b = get_token_balance(&ctx.rpc, user_ata_for_final_b).await?;
let signers: Vec<&Keypair> = swap_ix.additional_signers.iter().collect();
ctx.send_transaction_with_signers(swap_ix.instructions.clone(), signers).await?;
let after_a = get_token_balance(&ctx.rpc, user_ata_for_final_a).await?;
let after_b = get_token_balance(&ctx.rpc, user_ata_for_final_b).await?;
let used_a = before_a.saturating_sub(after_a);
let used_b = before_b.saturating_sub(after_b);
let gained_a = after_a.saturating_sub(before_a);
let gained_b = after_b.saturating_sub(before_b);
match &swap_ix.quote {
SwapQuote::ExactIn(q) => {
if a_to_b {
assert_eq!(used_a, q.token_in, "Used A mismatch");
assert_eq!(gained_b, q.token_est_out, "Gained B mismatch");
} else {
assert_eq!(used_b, q.token_in, "Used B mismatch");
assert_eq!(gained_a, q.token_est_out, "Gained A mismatch");
}
}
SwapQuote::ExactOut(q) => {
if a_to_b {
assert_eq!(gained_b, q.token_out, "Gained B mismatch");
assert_eq!(used_a, q.token_est_in, "Used A mismatch");
} else {
assert_eq!(gained_a, q.token_out, "Gained A mismatch");
assert_eq!(used_b, q.token_est_in, "Used B mismatch");
}
}
}
Ok(())
}
#[rstest]
#[case("A-B", true, SwapType::ExactIn, 1000)]
#[case("A-B", true, SwapType::ExactOut, 500)]
#[case("A-B", false, SwapType::ExactIn, 200)]
#[case("A-B", false, SwapType::ExactOut, 100)]
#[case("A-TEA", true, SwapType::ExactIn, 1000)]
#[case("A-TEA", true, SwapType::ExactOut, 500)]
#[case("A-TEA", false, SwapType::ExactIn, 200)]
#[case("A-TEA", false, SwapType::ExactOut, 100)]
#[case("TEA-TEB", true, SwapType::ExactIn, 1000)]
#[case("TEA-TEB", true, SwapType::ExactOut, 500)]
#[case("TEA-TEB", false, SwapType::ExactIn, 200)]
#[case("TEA-TEB", false, SwapType::ExactOut, 100)]
#[case("A-TEFee", true, SwapType::ExactIn, 1000)]
#[case("A-TEFee", true, SwapType::ExactOut, 500)]
#[case("A-TEFee", false, SwapType::ExactIn, 200)]
#[case("A-TEFee", false, SwapType::ExactOut, 100)]
#[serial]
fn test_swap_scenarios(#[case] pool_name: &str, #[case] a_to_b: bool, #[case] swap_type: SwapType, #[case] amount: u64) {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let ctx = RpcContext::new().await;
let minted = setup_all_mints(&ctx).await.unwrap();
let user_atas = setup_all_atas(&ctx, &minted).await.unwrap();
let (mkey_a, mkey_b) = parse_pool_name(pool_name);
let pubkey_a = minted[mkey_a];
let pubkey_b = minted[mkey_b];
let tick_spacing = 64;
let (final_a, final_b) = if pubkey_a < pubkey_b {
(pubkey_a, pubkey_b)
} else {
(pubkey_b, pubkey_a)
};
let fee_rate = 300;
let pool_pubkey = setup_fusion_pool(&ctx, final_a, final_b, tick_spacing, fee_rate).await.unwrap();
let position_mint = setup_position(
&ctx,
pool_pubkey,
Some((-192, 192)), None,
)
.await
.unwrap();
let liq_ix = increase_liquidity_instructions(
&ctx.rpc,
position_mint,
IncreaseLiquidityParam::Liquidity(1_000_000),
Some(100), Some(ctx.signer.pubkey()),
)
.await
.unwrap();
ctx.send_transaction_with_signers(liq_ix.instructions, liq_ix.additional_signers.iter().collect())
.await
.unwrap();
let user_ata_for_final_a = if final_a == pubkey_a { user_atas[mkey_a] } else { user_atas[mkey_b] };
let user_ata_for_final_b = if final_b == pubkey_b { user_atas[mkey_b] } else { user_atas[mkey_a] };
let token_for_this_call = match swap_type {
SwapType::ExactIn => {
if a_to_b {
final_a
} else {
final_b
}
}
SwapType::ExactOut => {
if a_to_b {
final_b
} else {
final_a
}
}
};
let swap_ix = swap_instructions(
&ctx.rpc,
pool_pubkey,
amount,
token_for_this_call,
swap_type.clone(),
Some(100), Some(ctx.signer.pubkey()),
)
.await
.unwrap();
verify_swap(&ctx, &swap_ix, user_ata_for_final_a, user_ata_for_final_b, a_to_b)
.await
.unwrap();
});
}
}