use std::error::Error;
use fusionamm_client::FusionPool;
use fusionamm_client::{get_fusion_pool_address, get_fusion_pools_config_address, get_token_badge_address};
use fusionamm_client::{InitializePool, InitializePoolInstructionArgs};
use fusionamm_core::price_to_sqrt_price;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_keypair::Keypair;
use solana_program::rent::Rent;
use solana_program::sysvar::SysvarId;
use solana_program::{instruction::Instruction, pubkey::Pubkey};
use solana_sdk_ids::system_program;
use solana_signer::Signer;
use spl_token_2022::extension::StateWithExtensions;
use spl_token_2022::state::Mint;
use crate::{get_account_data_size, get_rent, order_mints, FUNDER};
pub struct CreatePoolInstructions {
pub instructions: Vec<Instruction>,
pub initialization_cost: u64,
pub pool_address: Pubkey,
pub additional_signers: Vec<Keypair>,
}
pub async fn create_fusion_pool_instructions(
rpc: &RpcClient,
token_a: Pubkey,
token_b: Pubkey,
tick_spacing: u16,
fee_rate: u16,
initial_price: Option<f64>,
funder: Option<Pubkey>,
) -> Result<CreatePoolInstructions, Box<dyn Error>> {
let initial_price = initial_price.unwrap_or(1.0);
let funder = funder.unwrap_or(*FUNDER.try_lock()?);
if funder == Pubkey::default() {
return Err("Funder must be provided".into());
}
if order_mints(token_a, token_b)[0] != token_a {
return Err("Token order needs to be flipped to match the canonical ordering (i.e. sorted on the byte repr. of the mint pubkeys)".into());
}
let rent = get_rent(rpc).await?;
let account_infos = rpc.get_multiple_accounts(&[token_a, token_b]).await?;
let mint_a_info = account_infos[0].as_ref().ok_or(format!("Mint {} not found", token_a))?;
let mint_a = StateWithExtensions::<Mint>::unpack(&mint_a_info.data)?;
let decimals_a = mint_a.base.decimals;
let token_program_a = mint_a_info.owner;
let mint_b_info = account_infos[1].as_ref().ok_or(format!("Mint {} not found", token_b))?;
let mint_b = StateWithExtensions::<Mint>::unpack(&mint_b_info.data)?;
let decimals_b = mint_b.base.decimals;
let token_program_b = mint_b_info.owner;
let initial_sqrt_price: u128 = price_to_sqrt_price(initial_price, decimals_a, decimals_b);
let pool_address = get_fusion_pool_address(&token_a, &token_b, tick_spacing)?.0;
let token_badge_a = get_token_badge_address(&token_a)?.0;
let token_badge_b = get_token_badge_address(&token_b)?.0;
let token_vault_a = Keypair::new();
let token_vault_b = Keypair::new();
let mut instructions = vec![];
let mut initialization_cost: u64 = 0;
instructions.push(
InitializePool {
fusion_pools_config: get_fusion_pools_config_address()?.0,
token_mint_a: token_a,
token_mint_b: token_b,
token_badge_a,
token_badge_b,
funder,
fusion_pool: pool_address,
token_vault_a: token_vault_a.pubkey(),
token_vault_b: token_vault_b.pubkey(),
token_program_a,
token_program_b,
system_program: system_program::id(),
rent: Rent::id(),
}
.instruction(InitializePoolInstructionArgs {
tick_spacing,
fee_rate,
initial_sqrt_price,
}),
);
initialization_cost += rent.minimum_balance(FusionPool::LEN);
let token_a_space = get_account_data_size(token_program_a, mint_a_info)?;
initialization_cost += rent.minimum_balance(token_a_space);
let token_b_space = get_account_data_size(token_program_b, mint_b_info)?;
initialization_cost += rent.minimum_balance(token_b_space);
Ok(CreatePoolInstructions {
instructions,
initialization_cost,
pool_address,
additional_signers: vec![token_vault_a, token_vault_b],
})
}
#[cfg(test)]
mod tests {
use crate::tests::{setup_mint, setup_mint_te, setup_mint_te_fee, RpcContext};
use super::*;
use serial_test::serial;
async fn fetch_pool(rpc: &RpcClient, pool_address: Pubkey) -> Result<FusionPool, Box<dyn Error>> {
let account = rpc.get_account(&pool_address).await?;
FusionPool::from_bytes(&account.data).map_err(|e| e.into())
}
#[tokio::test]
#[serial]
async fn test_error_if_no_funder() {
let ctx = RpcContext::new().await;
let mint_a = setup_mint(&ctx).await.unwrap();
let mint_b = setup_mint(&ctx).await.unwrap();
let result = create_fusion_pool_instructions(&ctx.rpc, mint_a, mint_b, 64, 300, Some(1.0), None).await;
assert!(result.is_err());
}
#[tokio::test]
#[serial]
async fn test_error_if_tokens_not_ordered() {
let ctx = RpcContext::new().await;
let mint_a = setup_mint(&ctx).await.unwrap();
let mint_b = setup_mint(&ctx).await.unwrap();
let result = create_fusion_pool_instructions(&ctx.rpc, mint_b, mint_a, 64, 300, Some(1.0), Some(ctx.signer.pubkey())).await;
assert!(result.is_err());
}
#[tokio::test]
#[serial]
async fn test_create_concentrated_liquidity_pool() {
let ctx = RpcContext::new().await;
let mint_a = setup_mint(&ctx).await.unwrap();
let mint_b = setup_mint(&ctx).await.unwrap();
let price = 10.0;
let fee_rate = 300;
let sqrt_price = price_to_sqrt_price(price, 9, 9);
let result = create_fusion_pool_instructions(&ctx.rpc, mint_a, mint_b, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
.await
.unwrap();
let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
assert!(pool_before.is_err());
let instructions = result.instructions;
ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
.await
.unwrap();
let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let balance_change = balance_before - balance_after;
let tx_fee = 15000; let min_rent_exempt = balance_change - tx_fee;
assert_eq!(result.initialization_cost, min_rent_exempt);
assert_eq!(sqrt_price, pool_after.sqrt_price);
assert_eq!(mint_a, pool_after.token_mint_a);
assert_eq!(mint_b, pool_after.token_mint_b);
assert_eq!(64, pool_after.tick_spacing);
assert_eq!(300, pool_after.fee_rate);
}
#[tokio::test]
#[serial]
async fn test_create_concentrated_liquidity_pool_with_one_te_token() {
let ctx = RpcContext::new().await;
let mint = setup_mint(&ctx).await.unwrap();
let mint_te = setup_mint_te(&ctx, &[]).await.unwrap();
let price = 10.0;
let fee_rate = 300;
let sqrt_price = price_to_sqrt_price(price, 9, 6);
let result = create_fusion_pool_instructions(&ctx.rpc, mint, mint_te, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
.await
.unwrap();
let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
assert!(pool_before.is_err());
let instructions = result.instructions;
ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
.await
.unwrap();
let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let balance_change = balance_before - balance_after;
let tx_fee = 15000; let min_rent_exempt = balance_change - tx_fee;
assert_eq!(result.initialization_cost, min_rent_exempt);
assert_eq!(sqrt_price, pool_after.sqrt_price);
assert_eq!(mint, pool_after.token_mint_a);
assert_eq!(mint_te, pool_after.token_mint_b);
assert_eq!(64, pool_after.tick_spacing);
assert_eq!(300, pool_after.fee_rate);
}
#[tokio::test]
#[serial]
async fn test_create_concentrated_liquidity_pool_with_two_te_tokens() {
let ctx = RpcContext::new().await;
let mint_te_a = setup_mint_te(&ctx, &[]).await.unwrap();
let mint_te_b = setup_mint_te(&ctx, &[]).await.unwrap();
let price = 10.0;
let fee_rate = 300;
let sqrt_price = price_to_sqrt_price(price, 6, 6);
let result = create_fusion_pool_instructions(&ctx.rpc, mint_te_a, mint_te_b, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
.await
.unwrap();
let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
assert!(pool_before.is_err());
let instructions = result.instructions;
ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
.await
.unwrap();
let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let balance_change = balance_before - balance_after;
let tx_fee = 15000; let min_rent_exempt = balance_change - tx_fee;
assert_eq!(result.initialization_cost, min_rent_exempt);
assert_eq!(sqrt_price, pool_after.sqrt_price);
assert_eq!(mint_te_a, pool_after.token_mint_a);
assert_eq!(mint_te_b, pool_after.token_mint_b);
assert_eq!(64, pool_after.tick_spacing);
assert_eq!(300, pool_after.fee_rate);
}
#[tokio::test]
#[serial]
async fn test_create_concentrated_liquidity_pool_with_transfer_fee() {
let ctx = RpcContext::new().await;
let mint = setup_mint(&ctx).await.unwrap();
let mint_te_fee = setup_mint_te_fee(&ctx).await.unwrap();
let price = 10.0;
let fee_rate = 300;
let sqrt_price = price_to_sqrt_price(price, 9, 6);
let result = create_fusion_pool_instructions(&ctx.rpc, mint, mint_te_fee, 64, fee_rate, Some(price), Some(ctx.signer.pubkey()))
.await
.unwrap();
let balance_before = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let pool_before = fetch_pool(&ctx.rpc, result.pool_address).await;
assert!(pool_before.is_err());
let instructions = result.instructions;
ctx.send_transaction_with_signers(instructions, result.additional_signers.iter().collect())
.await
.unwrap();
let pool_after = fetch_pool(&ctx.rpc, result.pool_address).await.unwrap();
let balance_after = ctx.rpc.get_account(&ctx.signer.pubkey()).await.unwrap().lamports;
let balance_change = balance_before - balance_after;
let tx_fee = 15000; let min_rent_exempt = balance_change - tx_fee;
assert_eq!(result.initialization_cost, min_rent_exempt);
assert_eq!(sqrt_price, pool_after.sqrt_price);
assert_eq!(mint, pool_after.token_mint_a);
assert_eq!(mint_te_fee, pool_after.token_mint_b);
assert_eq!(64, pool_after.tick_spacing);
assert_eq!(300, pool_after.fee_rate);
}
}