diff --git a/Cargo.toml b/Cargo.toml index a536123..9e0935d 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" crate-type = ["cdylib", "rlib"] [dependencies] -solana-streamer-sdk = "0.3.1" +solana-streamer-sdk = "0.3.3" solana-sdk = "2.3.0" solana-client = "2.3.6" solana-program = "2.3.0" diff --git a/src/constants/raydium_cpmm.rs b/src/constants/raydium_cpmm.rs index 389966d..c66c959 100755 --- a/src/constants/raydium_cpmm.rs +++ b/src/constants/raydium_cpmm.rs @@ -25,6 +25,12 @@ pub mod accounts { pub const TOKEN_PROGRAM: Pubkey = spl_token::ID; pub const WSOL_TOKEN_ACCOUNT: Pubkey = pubkey!("So11111111111111111111111111111111111111112"); pub const RAYDIUM_CPMM: Pubkey = pubkey!("CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C"); + + pub const FEE_RATE_DENOMINATOR_VALUE: u128 = 1_000_000; + pub const TRADE_FEE_RATE: u64 = 2500; + pub const CREATOR_FEE_RATE: u64 = 0; + pub const PROTOCOL_FEE_RATE: u64 = 120000; + pub const FUND_FEE_RATE: u64 = 40000; } pub const SWAP_BASE_IN_DISCRIMINATOR: &[u8] = &[143, 190, 90, 218, 196, 30, 51, 222]; diff --git a/src/instruction/bonk.rs b/src/instruction/bonk.rs index 6dd05ca..9220970 100755 --- a/src/instruction/bonk.rs +++ b/src/instruction/bonk.rs @@ -10,16 +10,19 @@ use crate::{ trade::trade::DEFAULT_SLIPPAGE, }, trading::{ - bonk::common::{fetch_pool_state, get_amount_out, get_pool_pda, get_vault_pda}, + bonk::common::{get_pool_pda, get_vault_pda}, common::utils::get_token_balance, core::{ params::{BonkParams, BuyParams, SellParams}, traits::InstructionBuilder, }, }, + utils::calc::bonk::{ + get_buy_token_amount_from_sol_amount, get_sell_sol_amount_from_token_amount, + }, }; -/// Bonk协议的指令构建器 +/// Instruction builder for Bonk protocol pub struct BonkInstructionBuilder; #[async_trait::async_trait] @@ -37,7 +40,7 @@ impl InstructionBuilder for BonkInstructionBuilder { } impl BonkInstructionBuilder { - /// 使用提供的账户信息构建买入指令 + /// Build buy instructions with provided account information async fn build_buy_instructions_with_accounts( &self, params: &BuyParams, @@ -50,7 +53,7 @@ impl BonkInstructionBuilder { let pool_state = get_pool_pda(¶ms.mint, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); - // 创建用户代币账户 + // Create user token accounts let user_base_token_account = spl_associated_token_account::get_associated_token_address( ¶ms.payer.pubkey(), ¶ms.mint, @@ -60,31 +63,20 @@ impl BonkInstructionBuilder { &accounts::WSOL_TOKEN_ACCOUNT, ); - // 获取池的代币账户 + // Get pool token accounts let base_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap(); let quote_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); - let mut virtual_base = protocol_params.virtual_base.unwrap_or(0); - let mut virtual_quote = protocol_params.virtual_quote.unwrap_or(0); - let mut real_base = protocol_params.real_base.unwrap_or(0); - let mut real_quote = protocol_params.real_quote.unwrap_or(0); - - if virtual_base == 0 || virtual_quote == 0 || real_base == 0 || real_quote == 0 { - let pool = fetch_pool_state(params.rpc.as_ref().unwrap(), &pool_state).await?; - virtual_base = pool.virtual_base as u128; - virtual_quote = pool.virtual_quote as u128; - real_base = pool.real_base as u128; - real_quote = pool.real_quote as u128; - } + let virtual_base = protocol_params.virtual_base; + let virtual_quote = protocol_params.virtual_quote; + let real_base = protocol_params.real_base; + let real_quote = protocol_params.real_quote; let amount_in: u64 = params.sol_amount; let share_fee_rate: u64 = 0; - let minimum_amount_out: u64 = get_amount_out( + let minimum_amount_out: u64 = get_buy_token_amount_from_sol_amount( amount_in, - accounts::PROTOCOL_FEE_RATE, - accounts::PLATFORM_FEE_RATE, - accounts::SHARE_FEE_RATE, virtual_base, virtual_quote, real_base, @@ -95,9 +87,9 @@ impl BonkInstructionBuilder { let mut instructions = vec![]; if protocol_params.auto_handle_wsol { - // 插入wsol + // Handle wSOL instructions.push( - // 创建wSOL ATA账户,如果不存在 + // Create wSOL ATA account if it doesn't exist create_associated_token_account_idempotent( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), @@ -106,11 +98,11 @@ impl BonkInstructionBuilder { ), ); instructions.push( - // 将SOL转入wSOL ATA账户 + // Transfer SOL to wSOL ATA account transfer(¶ms.payer.pubkey(), &user_quote_token_account, amount_in), ); - // 同步wSOL余额 + // Sync wSOL balance instructions.push( spl_token::instruction::sync_native( &accounts::TOKEN_PROGRAM, @@ -120,7 +112,7 @@ impl BonkInstructionBuilder { ); } - // 创建用户的基础代币账户 + // Create user's base token account instructions.push(create_associated_token_account_idempotent( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), @@ -128,7 +120,7 @@ impl BonkInstructionBuilder { &accounts::TOKEN_PROGRAM, )); - // 创建买入指令 + // Create buy instruction let accounts = vec![ solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer) solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly) @@ -141,12 +133,15 @@ impl BonkInstructionBuilder { solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Base Token Mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly) - solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly( + protocol_params.mint_token_program, + false, + ), // Base Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // Event Authority (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::BONK, false), // Program (readonly) ]; - // 创建指令数据 + // Create instruction data let mut data = vec![]; data.extend_from_slice(&BUY_EXECT_IN_DISCRIMINATOR); data.extend_from_slice(&amount_in.to_le_bytes()); @@ -156,7 +151,7 @@ impl BonkInstructionBuilder { instructions.push(Instruction { program_id: accounts::BONK, accounts, data }); if protocol_params.auto_handle_wsol { - // 关闭wSOL ATA账户,回收租金 + // Close wSOL ATA account, reclaim rent instructions.push( spl_token::instruction::close_account( &accounts::TOKEN_PROGRAM, @@ -172,7 +167,7 @@ impl BonkInstructionBuilder { Ok(instructions) } - /// 使用提供的账户信息构建卖出指令 + /// Build sell instructions with provided account information async fn build_sell_instructions_with_accounts( &self, params: &SellParams, @@ -180,9 +175,16 @@ impl BonkInstructionBuilder { if params.rpc.is_none() { return Err(anyhow!("RPC is not set")); } + + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for Bonk"))?; + let rpc = params.rpc.as_ref().unwrap().clone(); - // 获取代币余额 + // Get token balance let mut amount = params.token_amount; if params.token_amount.is_none() || params.token_amount.unwrap_or(0) == 0 { let balance_u64 = @@ -195,12 +197,24 @@ impl BonkInstructionBuilder { return Err(anyhow!("Amount cannot be zero")); } - // 计算预期的SOL数量 - let minimum_amount_out: u64 = 1; - let pool_state = get_pool_pda(¶ms.mint, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); - // 创建用户代币账户 + let virtual_base = protocol_params.virtual_base; + let virtual_quote = protocol_params.virtual_quote; + let real_base = protocol_params.real_base; + let real_quote = protocol_params.real_quote; + + // Calculate expected SOL amount + let minimum_amount_out: u64 = get_sell_sol_amount_from_token_amount( + amount, + virtual_base, + virtual_quote, + real_base, + real_quote, + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE) as u128, + ); + + // Create user token accounts let user_base_token_account = spl_associated_token_account::get_associated_token_address( ¶ms.payer.pubkey(), ¶ms.mint, @@ -210,7 +224,7 @@ impl BonkInstructionBuilder { &accounts::WSOL_TOKEN_ACCOUNT, ); - // 获取池的代币账户 + // Get pool token accounts let base_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap(); let quote_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); @@ -219,9 +233,9 @@ impl BonkInstructionBuilder { let mut instructions = vec![]; - // 插入wsol + // Handle wSOL instructions.push( - // 创建wSOL ATA账户,如果不存在 + // Create wSOL ATA account if it doesn't exist create_associated_token_account_idempotent( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), @@ -230,7 +244,7 @@ impl BonkInstructionBuilder { ), ); - // 创建卖出指令 + // Create sell instruction let accounts = vec![ solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer) solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly) @@ -243,13 +257,16 @@ impl BonkInstructionBuilder { solana_sdk::instruction::AccountMeta::new(quote_vault_account, false), // Quote Vault solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Base Token Mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Quote Token Mint (readonly) - solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Base Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly( + protocol_params.mint_token_program, + false, + ), // Base Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Quote Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::EVENT_AUTHORITY, false), // Event Authority (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::BONK, false), // Program (readonly) ]; - // 创建指令数据 + // Create instruction data let mut data = vec![]; data.extend_from_slice(&SELL_EXECT_IN_DISCRIMINATOR); data.extend_from_slice(&amount.to_le_bytes()); @@ -258,12 +275,6 @@ impl BonkInstructionBuilder { instructions.push(Instruction { program_id: accounts::BONK, accounts, data }); - let protocol_params = params - .protocol_params - .as_any() - .downcast_ref::() - .ok_or_else(|| anyhow!("Invalid protocol params for Bonk"))?; - if protocol_params.auto_handle_wsol { instructions.push( close_account( diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 75b3be2..e3d19be 100755 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -1,5 +1,5 @@ use anyhow::{anyhow, Result}; -use solana_sdk::{instruction::Instruction, native_token::sol_str_to_lamports}; +use solana_sdk::instruction::Instruction; use spl_associated_token_account::{ get_associated_token_address, instruction::create_associated_token_account, }; @@ -10,6 +10,10 @@ use crate::{ trading::pumpfun::common::{ get_bonding_curve_pda, get_global_volume_accumulator_pda, get_user_volume_accumulator_pda, }, + utils::calc::{ + common::{calculate_with_slippage_buy, calculate_with_slippage_sell}, + pumpfun::{get_buy_token_amount_from_sol_amount, get_sell_sol_amount_from_token_amount}, + }, }; use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Keypair, signer::Signer}; @@ -17,21 +21,20 @@ use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey, signature::Keypair, s use crate::{ constants::pumpfun::global_constants::FEE_RECIPIENT, constants::trade::trade::DEFAULT_SLIPPAGE, - trading::common::utils::calculate_with_slippage_buy, trading::core::{ params::{BuyParams, PumpFunParams, SellParams}, traits::InstructionBuilder, }, - trading::pumpfun::common::{get_buy_token_amount_from_sol_amount, get_creator_vault_pda}, + trading::pumpfun::common::get_creator_vault_pda, }; -/// PumpFun协议的指令构建器 +/// Instruction builder for PumpFun protocol pub struct PumpFunInstructionBuilder; #[async_trait::async_trait] impl InstructionBuilder for PumpFunInstructionBuilder { async fn build_buy_instructions(&self, params: &BuyParams) -> Result> { - // 获取PumpFun特定参数 + // Get PumpFun specific parameters let protocol_params = params .protocol_params .as_any() @@ -42,11 +45,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { return Err(anyhow!("Amount cannot be zero")); } - let bonding_curve = if protocol_params.bonding_curve.is_some() { - protocol_params.bonding_curve.clone().unwrap() - } else { - return Err(anyhow!("Bonding curve not found")); - }; + let bonding_curve = protocol_params.bonding_curve.clone(); let max_sol_cost = calculate_with_slippage_buy( params.sol_amount, @@ -54,19 +53,17 @@ impl InstructionBuilder for PumpFunInstructionBuilder { ); let creator_vault_pda = bonding_curve.get_creator_vault_pda(); - let mut buy_token_amount = - get_buy_token_amount_from_sol_amount(&bonding_curve, params.sol_amount); - if buy_token_amount <= 100 * 1_000_000_u64 { - buy_token_amount = if max_sol_cost > sol_str_to_lamports("0.01").unwrap_or(0) { - 25547619 * 1_000_000_u64 - } else { - 255476 * 1_000_000_u64 - }; - } + let buy_token_amount = get_buy_token_amount_from_sol_amount( + bonding_curve.virtual_token_reserves as u128, + bonding_curve.virtual_sol_reserves as u128, + bonding_curve.real_token_reserves as u128, + bonding_curve.creator, + params.sol_amount, + ); let mut instructions = vec![]; - // 创建关联代币账户 + // Create associated token account instructions.push(create_associated_token_account( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), @@ -74,23 +71,29 @@ impl InstructionBuilder for PumpFunInstructionBuilder { &constants::pumpfun::accounts::TOKEN_PROGRAM, )); - // 创建买入指令 + // Create buy instruction instructions.push(buy( params.payer.as_ref(), ¶ms.mint, &bonding_curve.account, &creator_vault_pda, &FEE_RECIPIENT, - Buy { - _amount: buy_token_amount, - _max_sol_cost: max_sol_cost, - }, + Buy { _amount: buy_token_amount, _max_sol_cost: max_sol_cost }, )); Ok(instructions) } async fn build_sell_instructions(&self, params: &SellParams) -> Result> { + // Get PumpFun specific parameters + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + + let bonding_curve = protocol_params.bonding_curve.clone(); + let token_amount = if let Some(amount) = params.token_amount { if amount == 0 { return Err(anyhow!("Amount cannot be zero")); @@ -102,35 +105,27 @@ impl InstructionBuilder for PumpFunInstructionBuilder { let creator_vault_pda = get_creator_vault_pda(¶ms.creator).unwrap(); let ata = get_associated_token_address(¶ms.payer.pubkey(), ¶ms.mint); - // 获取代币余额 - let balance_u64 = if let Some(rpc) = ¶ms.rpc { - let balance = rpc.get_token_account_balance(&ata).await?; - balance - .amount - .parse::() - .map_err(|_| anyhow!("Failed to parse token balance"))? - } else { - return Err(anyhow!("RPC client is required to get token balance")); - }; - - let mut token_amount = token_amount; - if token_amount > balance_u64 { - token_amount = balance_u64; - } + let sol_amount = get_sell_sol_amount_from_token_amount( + bonding_curve.virtual_token_reserves as u128, + bonding_curve.virtual_sol_reserves as u128, + bonding_curve.creator, + token_amount, + ); + let min_sol_output = calculate_with_slippage_sell( + sol_amount, + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ); let mut instructions = vec![sell( params.payer.as_ref(), ¶ms.mint, &creator_vault_pda, &FEE_RECIPIENT, - Sell { - _amount: token_amount, - _min_sol_output: 1, - }, + Sell { _amount: token_amount, _min_sol_output: min_sol_output }, )]; - // 如果卖出全部代币,关闭账户 - if token_amount >= balance_u64 { + // If selling all tokens, close the account + if protocol_params.close_token_account_when_sell.unwrap_or(false) { instructions.push(close_account( &spl_token::ID, &ata, diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index e2a0f3a..74c05dd 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -10,20 +10,16 @@ use crate::{ trade::trade::DEFAULT_SLIPPAGE, }, trading::{ - common::utils::{ - calculate_with_slippage_buy, calculate_with_slippage_sell, get_token_balance, - }, core::{ params::{BuyParams, PumpSwapParams, SellParams}, traits::InstructionBuilder, }, - pumpswap::{ - self, - common::{ - coin_creator_vault_ata, coin_creator_vault_authority, fee_recipient_ata, fetch_pool, find_pool, get_global_volume_accumulator_pda, get_token_amount, get_user_volume_accumulator_pda, get_wsol_amount - }, + pumpswap::common::{ + coin_creator_vault_ata, coin_creator_vault_authority, fee_recipient_ata, + get_global_volume_accumulator_pda, get_user_volume_accumulator_pda, }, }, + utils::calc::pumpswap::{buy_quote_input_internal, sell_base_input_internal}, }; /// Instruction builder for PumpSwap protocol @@ -44,40 +40,21 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { } // Build instructions based on whether account information is provided - match (&protocol_params.pool,) { - (Some(pool),) => { - let mut base_mint = params.mint; - let mut quote_mint = accounts::WSOL_TOKEN_ACCOUNT; - let mut pool_base_token_reserves = 0; - let mut pool_quote_token_reserves = 0; + let base_mint = protocol_params.base_mint; + let quote_mint = protocol_params.quote_mint; + let pool_base_token_reserves = protocol_params.pool_base_token_reserves; + let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves; - if let Some(p_base_mint) = protocol_params.base_mint { - base_mint = p_base_mint; - } - if let Some(p_quote_mint) = protocol_params.quote_mint { - quote_mint = p_quote_mint; - } - if let Some(p_pool_base_token_reserves) = protocol_params.pool_base_token_reserves { - pool_base_token_reserves = p_pool_base_token_reserves; - } - if let Some(p_pool_quote_token_reserves) = protocol_params.pool_quote_token_reserves - { - pool_quote_token_reserves = p_pool_quote_token_reserves; - } - - self.build_buy_instructions_with_accounts( - params, - *pool, - base_mint, - quote_mint, - pool_base_token_reserves, - pool_quote_token_reserves, - protocol_params.auto_handle_wsol, - ) - .await - } - _ => self.build_buy_instructions_auto_discover(params).await, - } + self.build_buy_instructions_with_accounts( + params, + protocol_params.pool, + base_mint, + quote_mint, + pool_base_token_reserves, + pool_quote_token_reserves, + protocol_params.auto_handle_wsol, + ) + .await } async fn build_sell_instructions(&self, params: &SellParams) -> Result> { @@ -88,104 +65,25 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for PumpSwap"))?; // Build instructions based on whether account information is provided - match (&protocol_params.pool,) { - (Some(pool),) => { - let mut base_mint = params.mint; - let mut quote_mint = accounts::WSOL_TOKEN_ACCOUNT; - let mut pool_base_token_reserves = 0; - let mut pool_quote_token_reserves = 0; - if let Some(p_base_mint) = protocol_params.base_mint { - base_mint = p_base_mint; - } - if let Some(p_quote_mint) = protocol_params.quote_mint { - quote_mint = p_quote_mint; - } - if let Some(p_pool_base_token_reserves) = protocol_params.pool_base_token_reserves { - pool_base_token_reserves = p_pool_base_token_reserves; - } - if let Some(p_pool_quote_token_reserves) = protocol_params.pool_quote_token_reserves - { - pool_quote_token_reserves = p_pool_quote_token_reserves; - } - self.build_sell_instructions_with_accounts( - params, - *pool, - base_mint, - quote_mint, - pool_base_token_reserves, - pool_quote_token_reserves, - protocol_params.auto_handle_wsol, - ) - .await - } - _ => self.build_sell_instructions_auto_discover(params).await, - } + let base_mint = protocol_params.base_mint; + let quote_mint = protocol_params.quote_mint; + let pool_base_token_reserves = protocol_params.pool_base_token_reserves; + let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves; + + self.build_sell_instructions_with_accounts( + params, + protocol_params.pool, + base_mint, + quote_mint, + pool_base_token_reserves, + pool_quote_token_reserves, + protocol_params.auto_handle_wsol, + ) + .await } } impl PumpSwapInstructionBuilder { - /// Auto-discover pool and account information and build buy instructions - async fn build_buy_instructions_auto_discover( - &self, - params: &BuyParams, - ) -> Result> { - if params.rpc.is_none() { - return Err(anyhow!("RPC is not set")); - } - println!("❗️Going through RPC request, increasing instruction building time"); - let rpc = params.rpc.as_ref().unwrap().clone(); - // Find pool - let pool = find_pool(rpc.as_ref(), ¶ms.mint).await?; - let pool_data = fetch_pool(rpc.as_ref(), &pool).await?; - let pool_base_token_reserves = - get_token_balance(rpc.as_ref(), &pool, &pool_data.base_mint).await?; - let pool_quote_token_reserves = - get_token_balance(rpc.as_ref(), &pool, &pool_data.quote_mint).await?; - let mut params = params.clone(); - params.creator = pool_data.coin_creator; - self.build_buy_instructions_with_accounts( - ¶ms, - pool, - pool_data.base_mint, - pool_data.quote_mint, - pool_base_token_reserves, - pool_quote_token_reserves, - true, - ) - .await - } - - /// Auto-discover pool and account information and build sell instructions - async fn build_sell_instructions_auto_discover( - &self, - params: &SellParams, - ) -> Result> { - if params.rpc.is_none() { - return Err(anyhow!("RPC is not set")); - } - println!("❗️Going through RPC request, increasing instruction building time"); - let rpc = params.rpc.as_ref().unwrap().clone(); - // Find pool - let pool = find_pool(rpc.as_ref(), ¶ms.mint).await?; - let pool_data = fetch_pool(rpc.as_ref(), &pool).await?; - let pool_base_token_reserves = - get_token_balance(rpc.as_ref(), &pool, &pool_data.base_mint).await?; - let pool_quote_token_reserves = - get_token_balance(rpc.as_ref(), &pool, &pool_data.quote_mint).await?; - let mut params = params.clone(); - params.creator = pool_data.coin_creator; - self.build_sell_instructions_with_accounts( - ¶ms, - pool, - pool_data.base_mint, - pool_data.quote_mint, - pool_base_token_reserves, - pool_quote_token_reserves, - true, - ) - .await - } - /// Build buy instructions with provided account information async fn build_buy_instructions_with_accounts( &self, @@ -201,38 +99,36 @@ impl PumpSwapInstructionBuilder { return Err(anyhow!("RPC is not set")); } let quote_mint_is_wsol = quote_mint == accounts::WSOL_TOKEN_ACCOUNT; - // Calculate token amount - let mut token_amount = get_token_amount( - quote_mint_is_wsol, - pool_base_token_reserves, - pool_quote_token_reserves, - params.sol_amount, - accounts::LP_FEE_BASIS_POINTS, - accounts::PROTOCOL_FEE_BASIS_POINTS, - if params.creator == Pubkey::default() { - 0 - } else { - accounts::COIN_CREATOR_FEE_BASIS_POINTS - }, - ) - .await?; - if !quote_mint_is_wsol { - // min_quote_amount_out - token_amount = calculate_with_slippage_sell( - token_amount, - params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); - } - let sol_amount = if quote_mint_is_wsol { - // max_quote_amount_in - calculate_with_slippage_buy( + + let mut token_amount = 0; + let mut sol_amount = 0; + if quote_mint_is_wsol { + let result = buy_quote_input_internal( params.sol_amount, params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + pool_base_token_reserves, + pool_quote_token_reserves, + ¶ms.creator, ) + .unwrap(); + // base_amount_out + token_amount = result.base; + // max_quote_amount_in + sol_amount = result.max_quote; } else { + let result = sell_base_input_internal( + params.sol_amount, + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + pool_base_token_reserves, + pool_quote_token_reserves, + ¶ms.creator, + ) + .unwrap(); + // min_quote_amount_out + token_amount = result.min_quote; // base_amount_in - params.sol_amount - }; + sol_amount = params.sol_amount; + } // Create user token accounts let user_base_token_account = spl_associated_token_account::get_associated_token_address( @@ -351,11 +247,15 @@ impl PumpSwapInstructionBuilder { let mut data = vec![]; if quote_mint_is_wsol { data.extend_from_slice(&BUY_DISCRIMINATOR); + // base_amount_out data.extend_from_slice(&token_amount.to_le_bytes()); + // max_quote_amount_in data.extend_from_slice(&sol_amount.to_le_bytes()); } else { data.extend_from_slice(&SELL_DISCRIMINATOR); + // base_amount_in data.extend_from_slice(&sol_amount.to_le_bytes()); + // min_quote_amount_out data.extend_from_slice(&token_amount.to_le_bytes()); } @@ -394,27 +294,42 @@ impl PumpSwapInstructionBuilder { if params.rpc.is_none() { return Err(anyhow!("RPC is not set")); } + if params.token_amount.is_none() { + return Err(anyhow!("Token amount is not set")); + } let quote_mint_is_wsol = quote_mint == accounts::WSOL_TOKEN_ACCOUNT; - let mut sol_amount = get_wsol_amount( - quote_mint_is_wsol, - pool_base_token_reserves, - pool_quote_token_reserves, - params.token_amount.unwrap_or(0), - accounts::LP_FEE_BASIS_POINTS, - accounts::PROTOCOL_FEE_BASIS_POINTS, - if params.creator == Pubkey::default() { - 0 - } else { - accounts::COIN_CREATOR_FEE_BASIS_POINTS - }, - ) - .await?; - sol_amount = calculate_with_slippage_sell( - sol_amount, - params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), - ); - let token_amount = params.token_amount.unwrap_or(0); + + let mut token_amount = 0; + let mut sol_amount = 0; + + if quote_mint_is_wsol { + let result = sell_base_input_internal( + params.token_amount.unwrap(), + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + pool_base_token_reserves, + pool_quote_token_reserves, + ¶ms.creator, + ) + .unwrap(); + // base_amount_in + token_amount = params.token_amount.unwrap(); + // min_quote_amount_out + sol_amount = result.min_quote; + } else { + let result = buy_quote_input_internal( + params.token_amount.unwrap(), + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + pool_base_token_reserves, + pool_quote_token_reserves, + ¶ms.creator, + ) + .unwrap(); + // max_quote_amount_in + token_amount = result.max_quote; + // base_amount_out + sol_amount = result.base; + } let coin_creator_vault_ata = coin_creator_vault_ata(params.creator, quote_mint); let coin_creator_vault_authority = coin_creator_vault_authority(params.creator); @@ -502,11 +417,15 @@ impl PumpSwapInstructionBuilder { let mut data = vec![]; if quote_mint_is_wsol { data.extend_from_slice(&SELL_DISCRIMINATOR); + // base_amount_in data.extend_from_slice(&token_amount.to_le_bytes()); + // min_quote_amount_out data.extend_from_slice(&sol_amount.to_le_bytes()); } else { data.extend_from_slice(&BUY_DISCRIMINATOR); + // base_amount_out data.extend_from_slice(&sol_amount.to_le_bytes()); + // max_quote_amount_in data.extend_from_slice(&token_amount.to_le_bytes()); } diff --git a/src/instruction/raydium_cpmm.rs b/src/instruction/raydium_cpmm.rs index 12679d8..29b1edc 100755 --- a/src/instruction/raydium_cpmm.rs +++ b/src/instruction/raydium_cpmm.rs @@ -5,20 +5,21 @@ use spl_associated_token_account::instruction::create_associated_token_account_i use spl_token::instruction::close_account; use crate::{ - constants::raydium_cpmm::{accounts, SWAP_BASE_IN_DISCRIMINATOR}, - constants::trade::trade::DEFAULT_SLIPPAGE, - trading::common::utils::get_token_balance, - trading::core::{ - params::{BuyParams, RaydiumCpmmParams, SellParams}, - traits::InstructionBuilder, + constants::{ + raydium_cpmm::{accounts, SWAP_BASE_IN_DISCRIMINATOR}, + trade::trade::DEFAULT_SLIPPAGE, }, - trading::raydium_cpmm::{ - common::{get_observation_state_pda, get_pool_pda, get_vault_pda}, - // pool::Pool, + trading::{ + core::{ + params::{BuyParams, RaydiumCpmmParams, SellParams}, + traits::InstructionBuilder, + }, + raydium_cpmm::common::{get_observation_state_pda, get_pool_pda, get_vault_pda}, }, + utils::calc::raydium_cpmm::compute_swap_amount, }; -/// RaydiumCpmm协议的指令构建器 +/// Instruction builder for RaydiumCpmm protocol pub struct RaydiumCpmmInstructionBuilder; #[async_trait::async_trait] @@ -36,7 +37,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder { } impl RaydiumCpmmInstructionBuilder { - /// 使用提供的账户信息构建买入指令 + /// Build buy instructions with provided account information async fn build_buy_instructions_with_accounts( &self, params: &BuyParams, @@ -47,26 +48,12 @@ impl RaydiumCpmmInstructionBuilder { .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?; - let pool_state = if protocol_params.pool_state.is_some() { - protocol_params.pool_state.unwrap() - } else { - let mint_token_in_pool_state_index = - protocol_params.mint_token_in_pool_state_index.unwrap_or(1); - get_pool_pda( - &accounts::AMM_CONFIG, - if mint_token_in_pool_state_index == 1 { - &accounts::WSOL_TOKEN_ACCOUNT - } else { - ¶ms.mint - }, - if mint_token_in_pool_state_index == 1 { - ¶ms.mint - } else { - &accounts::WSOL_TOKEN_ACCOUNT - }, - ) - .unwrap() - }; + let pool_state = get_pool_pda( + &accounts::AMM_CONFIG, + &protocol_params.base_mint, + &protocol_params.quote_mint, + ) + .unwrap(); let wsol_token_account = spl_associated_token_account::get_associated_token_address( ¶ms.payer.pubkey(), @@ -77,36 +64,29 @@ impl RaydiumCpmmInstructionBuilder { ¶ms.mint, ); - // 获取池的代币账户 + // Get pool token accounts let wsol_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); let mint_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap(); let observation_state_account = get_observation_state_pda(&pool_state).unwrap(); + let is_base_in = protocol_params.base_mint == accounts::WSOL_TOKEN_ACCOUNT; let amount_in: u64 = params.sol_amount; - let mut minimum_amount_out: u64 = if protocol_params.minimum_amount_out.is_some() { - protocol_params.minimum_amount_out.unwrap() - } else { - println!("未提供minimum_amount_out,使用默认值0"); - 0 - }; - if minimum_amount_out != 0 { - let slippage_basis_points: u64 = if params.slippage_basis_points.is_some() { - params.slippage_basis_points.unwrap() - } else { - DEFAULT_SLIPPAGE - } as u64; - minimum_amount_out = minimum_amount_out * (10000 - slippage_basis_points) / 10000; - println!("slippage_basis_points: {}", slippage_basis_points); - } - println!("minimum_amount_out: {}", minimum_amount_out); + let result = compute_swap_amount( + protocol_params.base_reserve, + protocol_params.quote_reserve, + is_base_in, + amount_in, + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ); + let minimum_amount_out = result.min_amount_out; let mut instructions = vec![]; if protocol_params.auto_handle_wsol { - // 插入wsol + // Handle wSOL instructions.push( - // 创建wSOL ATA账户,如果不存在 + // Create wSOL ATA account if it doesn't exist create_associated_token_account_idempotent( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), @@ -115,26 +95,31 @@ impl RaydiumCpmmInstructionBuilder { ), ); instructions.push( - // 将SOL转入wSOL ATA账户 + // Transfer SOL to wSOL ATA account transfer(¶ms.payer.pubkey(), &wsol_token_account, amount_in), ); - // 同步wSOL余额 + // Sync wSOL balance instructions.push( spl_token::instruction::sync_native(&accounts::TOKEN_PROGRAM, &wsol_token_account) .unwrap(), ); } - // 创建用户的基础代币账户 + let mint_token_program = if is_base_in { + protocol_params.quote_token_program + } else { + protocol_params.base_token_program + }; + instructions.push(create_associated_token_account_idempotent( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), ¶ms.mint, - &protocol_params.mint_token_program.unwrap_or(accounts::TOKEN_PROGRAM), + &mint_token_program, )); - // 创建买入指令 + // Create buy instruction let accounts = vec![ solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer) solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly) @@ -145,15 +130,12 @@ impl RaydiumCpmmInstructionBuilder { solana_sdk::instruction::AccountMeta::new(wsol_vault_account, false), // Input Vault Account solana_sdk::instruction::AccountMeta::new(mint_vault_account, false), // Output Vault Account solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Input Token Program (readonly) - solana_sdk::instruction::AccountMeta::new_readonly( - protocol_params.mint_token_program.unwrap_or(accounts::TOKEN_PROGRAM), - false, - ), // Output Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(mint_token_program, false), // Output Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Input token mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Output token mint (readonly) solana_sdk::instruction::AccountMeta::new(observation_state_account, false), // Observation State Account ]; - // 创建指令数据 + // Create instruction data let mut data = vec![]; data.extend_from_slice(&SWAP_BASE_IN_DISCRIMINATOR); data.extend_from_slice(&amount_in.to_le_bytes()); @@ -162,7 +144,7 @@ impl RaydiumCpmmInstructionBuilder { instructions.push(Instruction { program_id: accounts::RAYDIUM_CPMM, accounts, data }); if protocol_params.auto_handle_wsol { - // 关闭wSOL ATA账户,回收租金 + // Close wSOL ATA account, reclaim rent instructions.push( spl_token::instruction::close_account( &accounts::TOKEN_PROGRAM, @@ -178,7 +160,7 @@ impl RaydiumCpmmInstructionBuilder { Ok(instructions) } - /// 使用提供的账户信息构建卖出指令 + /// Build sell instructions with provided account information async fn build_sell_instructions_with_accounts( &self, params: &SellParams, @@ -189,61 +171,27 @@ impl RaydiumCpmmInstructionBuilder { .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?; - if params.rpc.is_none() { - return Err(anyhow!("RPC is not set")); - } - let rpc = params.rpc.as_ref().unwrap().clone(); - - // 获取代币余额 - let mut amount = params.token_amount; if params.token_amount.is_none() || params.token_amount.unwrap_or(0) == 0 { - let balance_u64 = - get_token_balance(rpc.as_ref(), ¶ms.payer.pubkey(), ¶ms.mint).await?; - amount = Some(balance_u64); - } - let amount = amount.unwrap_or(0); - - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); + return Err(anyhow!("Token amount is not set")); } - let mut minimum_amount_out: u64 = if protocol_params.minimum_amount_out.is_some() { - protocol_params.minimum_amount_out.unwrap() - } else { - println!("未提供minimum_amount_out,使用默认值0"); - 0 - }; - if minimum_amount_out != 0 { - let slippage_basis_points: u64 = if params.slippage_basis_points.is_some() { - params.slippage_basis_points.unwrap() - } else { - DEFAULT_SLIPPAGE - } as u64; - minimum_amount_out = minimum_amount_out * (10000 - slippage_basis_points) / 10000; - println!("slippage_basis_points: {}", slippage_basis_points); - } - println!("minimum_amount_out: {}", minimum_amount_out); + let is_base_in = protocol_params.base_mint == params.mint; - let pool_state = if protocol_params.pool_state.is_some() { - protocol_params.pool_state.unwrap() - } else { - let mint_token_in_pool_state_index = - protocol_params.mint_token_in_pool_state_index.unwrap_or(1); - get_pool_pda( - &accounts::AMM_CONFIG, - if mint_token_in_pool_state_index == 1 { - &accounts::WSOL_TOKEN_ACCOUNT - } else { - ¶ms.mint - }, - if mint_token_in_pool_state_index == 1 { - ¶ms.mint - } else { - &accounts::WSOL_TOKEN_ACCOUNT - }, - ) - .unwrap() - }; + let minimum_amount_out: u64 = compute_swap_amount( + protocol_params.base_reserve, + protocol_params.quote_reserve, + is_base_in, + params.token_amount.unwrap_or(0), + params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), + ) + .min_amount_out; + + let pool_state = get_pool_pda( + &accounts::AMM_CONFIG, + &protocol_params.base_mint, + &protocol_params.quote_mint, + ) + .unwrap(); let wsol_token_account = spl_associated_token_account::get_associated_token_address( ¶ms.payer.pubkey(), @@ -254,7 +202,7 @@ impl RaydiumCpmmInstructionBuilder { ¶ms.mint, ); - // 获取池的代币账户 + // Get pool token accounts let wsol_vault_account = get_vault_pda(&pool_state, &accounts::WSOL_TOKEN_ACCOUNT).unwrap(); let mint_vault_account = get_vault_pda(&pool_state, ¶ms.mint).unwrap(); @@ -262,18 +210,24 @@ impl RaydiumCpmmInstructionBuilder { let mut instructions = vec![]; - // 插入wsol + // Handle wSOL instructions.push( - // 创建wSOL ATA账户,如果不存在 + // Create wSOL ATA account if it doesn't exist create_associated_token_account_idempotent( ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), &accounts::WSOL_TOKEN_ACCOUNT, - &protocol_params.mint_token_program.unwrap(), + &spl_token::ID, ), ); - // 创建卖出指令 + let mint_token_program = if is_base_in { + protocol_params.base_token_program + } else { + protocol_params.quote_token_program + }; + + // Create sell instruction let accounts = vec![ solana_sdk::instruction::AccountMeta::new(params.payer.pubkey(), true), // Payer (signer) solana_sdk::instruction::AccountMeta::new_readonly(accounts::AUTHORITY, false), // Authority (readonly) @@ -283,19 +237,16 @@ impl RaydiumCpmmInstructionBuilder { solana_sdk::instruction::AccountMeta::new(wsol_token_account, false), // Output Token Account solana_sdk::instruction::AccountMeta::new(mint_vault_account, false), // Input Vault Account solana_sdk::instruction::AccountMeta::new(wsol_vault_account, false), // Output Vault Account - solana_sdk::instruction::AccountMeta::new_readonly( - protocol_params.mint_token_program.unwrap_or(accounts::TOKEN_PROGRAM), - false, - ), // Input Token Program (readonly) + solana_sdk::instruction::AccountMeta::new_readonly(mint_token_program, false), // Input Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::TOKEN_PROGRAM, false), // Output Token Program (readonly) solana_sdk::instruction::AccountMeta::new_readonly(params.mint, false), // Input token mint (readonly) solana_sdk::instruction::AccountMeta::new_readonly(accounts::WSOL_TOKEN_ACCOUNT, false), // Output token mint (readonly) solana_sdk::instruction::AccountMeta::new(observation_state_account, false), // Observation State Account ]; - // 创建指令数据 + // Create instruction data let mut data = vec![]; data.extend_from_slice(&SWAP_BASE_IN_DISCRIMINATOR); - data.extend_from_slice(&amount.to_le_bytes()); + data.extend_from_slice(¶ms.token_amount.unwrap_or(0).to_le_bytes()); data.extend_from_slice(&minimum_amount_out.to_le_bytes()); instructions.push(Instruction { program_id: accounts::RAYDIUM_CPMM, accounts, data }); diff --git a/src/lib.rs b/src/lib.rs index b9c7be0..19d0934 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -175,22 +175,11 @@ impl SolanaTrade { slippage_basis_points: Option, recent_blockhash: Hash, custom_buy_tip_fee: Option, - extension_params: Option>, + extension_params: Box, lookup_table_key: Option, ) -> Result<(), anyhow::Error> { let executor = TradeFactory::create_executor(dex_type.clone()); - let protocol_params = if let Some(params) = extension_params { - params - } else { - match dex_type { - DexType::PumpFun => Box::new(PumpFunParams::default()) as Box, - DexType::PumpSwap => Box::new(PumpSwapParams::default()) as Box, - DexType::Bonk => Box::new(BonkParams::default()) as Box, - DexType::RaydiumCpmm => { - Box::new(RaydiumCpmmParams::default()) as Box - } - } - }; + let protocol_params = extension_params; let final_lookup_table_key = lookup_table_key.or(self.trade_config.lookup_table_key); @@ -308,22 +297,11 @@ impl SolanaTrade { recent_blockhash: Hash, custom_buy_tip_fee: Option, with_tip: bool, - extension_params: Option>, + extension_params: Box, lookup_table_key: Option, ) -> Result<(), anyhow::Error> { let executor = TradeFactory::create_executor(dex_type.clone()); - let protocol_params = if let Some(params) = extension_params { - params - } else { - match dex_type { - DexType::PumpFun => Box::new(PumpFunParams::default()) as Box, - DexType::PumpSwap => Box::new(PumpSwapParams::default()) as Box, - DexType::Bonk => Box::new(BonkParams::default()) as Box, - DexType::RaydiumCpmm => { - Box::new(RaydiumCpmmParams::default()) as Box - } - } - }; + let protocol_params = extension_params; let final_lookup_table_key = lookup_table_key.or(self.trade_config.lookup_table_key); @@ -454,7 +432,7 @@ impl SolanaTrade { recent_blockhash: Hash, custom_buy_tip_fee: Option, with_tip: bool, - extension_params: Option>, + extension_params: Box, lookup_table_key: Option, ) -> Result<(), anyhow::Error> { if percent == 0 || percent > 100 { diff --git a/src/main.rs b/src/main.rs index f270a89..ad16275 100755 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,5 @@ use std::{str::FromStr, sync::Arc}; -use sol_trade_sdk::{ - common::{bonding_curve::BondingCurveAccount, AnyResult, PriorityFee, TradeConfig}, - swqos::{SwqosConfig, SwqosRegion}, - trading::{core::params::{BonkParams, PumpFunParams, PumpSwapParams, RaydiumCpmmParams}, factory::DexType, raydium_cpmm::common::{get_buy_token_amount, get_sell_sol_amount}}, - SolanaTrade, -}; use sol_trade_sdk::solana_streamer_sdk::{ match_event, streaming::{ @@ -16,15 +10,33 @@ use sol_trade_sdk::solana_streamer_sdk::{ pumpswap::{ PumpSwapBuyEvent, PumpSwapCreatePoolEvent, PumpSwapDepositEvent, PumpSwapSellEvent, PumpSwapWithdrawEvent, - }, raydium_cpmm::RaydiumCpmmSwapEvent, + }, + raydium_cpmm::RaydiumCpmmSwapEvent, }, Protocol, UnifiedEvent, }, ShredStreamGrpc, YellowstoneGrpc, }, }; +use sol_trade_sdk::{ + common::{AnyResult, PriorityFee, TradeConfig}, + swqos::{SwqosConfig, SwqosRegion}, + trading::{ + core::params::{BonkParams, PumpFunParams, PumpSwapParams, RaydiumCpmmParams}, + factory::DexType, + }, + SolanaTrade, +}; use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, signature::Keypair}; -use solana_streamer_sdk::streaming::{event_parser::protocols::{bonk::parser::BONK_PROGRAM_ID, pumpfun::parser::PUMPFUN_PROGRAM_ID, pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID, raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID}, yellowstone_grpc::{AccountFilter, TransactionFilter}}; +use solana_streamer_sdk::streaming::{ + event_parser::protocols::{ + bonk::parser::BONK_PROGRAM_ID, pumpfun::parser::PUMPFUN_PROGRAM_ID, + pumpswap::parser::PUMPSWAP_PROGRAM_ID, raydium_amm_v4::parser::RAYDIUM_AMM_V4_PROGRAM_ID, + raydium_clmm::parser::RAYDIUM_CLMM_PROGRAM_ID, + raydium_cpmm::parser::RAYDIUM_CPMM_PROGRAM_ID, + }, + yellowstone_grpc::{AccountFilter, TransactionFilter}, +}; #[tokio::main] async fn main() -> Result<(), Box> { @@ -37,7 +49,8 @@ async fn main() -> Result<(), Box> { Ok(()) } -/// 创建 SolanaTrade 客户端 +/// Create SolanaTrade client +/// Initializes a new SolanaTrade client with configuration async fn test_create_solana_trade_client() -> AnyResult { println!("Creating SolanaTrade client..."); @@ -57,7 +70,7 @@ fn create_swqos_configs(rpc_url: &str) -> Vec { vec![ SwqosConfig::Jito("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::NextBlock("your api_token".to_string(), SwqosRegion::Frankfurt), - SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Default(rpc_url.to_string()), @@ -83,39 +96,40 @@ async fn test_pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> Any let buy_sol_cost = 100_000; let slippage_basis_points = Some(100); let recent_blockhash = client.rpc.get_latest_blockhash().await?; - let bonding_curve = BondingCurveAccount::from_trade(&trade_info); // Buy tokens println!("Buying tokens from PumpFun..."); - client.buy( - DexType::PumpFun, - mint_pubkey, - Some(creator), - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - Some(Box::new(PumpFunParams { - bonding_curve: Some(Arc::new(bonding_curve.clone())), - })), - None, - ).await?; + client + .buy( + DexType::PumpFun, + mint_pubkey, + Some(creator), + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + None, + Box::new(PumpFunParams::from_trade(&trade_info, None)), + None, + ) + .await?; - // Sell tokens + // Sell tokens println!("Selling tokens from PumpFun..."); let amount_token = 0; - client.sell( - DexType::PumpFun, - mint_pubkey, - Some(creator), - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - None, - None, - ).await?; + client + .sell( + DexType::PumpFun, + mint_pubkey, + Some(creator), + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + Box::new(PumpFunParams::from_trade(&trade_info, None)), + None, + ) + .await?; Ok(()) } @@ -133,45 +147,52 @@ async fn test_pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> let slippage_basis_points = Some(100); let recent_blockhash = client.rpc.get_latest_blockhash().await?; - let bonding_curve = BondingCurveAccount::from_dev_trade( - &mint_pubkey, - trade_info.token_amount, - trade_info.max_sol_cost, - creator, - ); - // Buy tokens println!("Buying tokens from PumpFun..."); let buy_sol_amount = 100_000; - client.buy( - DexType::PumpFun, - mint_pubkey, - Some(creator), - buy_sol_amount, - slippage_basis_points, - recent_blockhash, - None, - Some(Box::new(PumpFunParams { - bonding_curve: Some(Arc::new(bonding_curve.clone())), - })), - None, - ).await?; + client + .buy( + DexType::PumpFun, + mint_pubkey, + Some(creator), + buy_sol_amount, + slippage_basis_points, + recent_blockhash, + None, + Box::new(PumpFunParams::from_dev_trade( + &mint_pubkey, + trade_info.token_amount, + trade_info.max_sol_cost, + creator, + None, + )), + None, + ) + .await?; // Sell tokens println!("Selling tokens from PumpFun..."); let amount_token = 0; - client.sell( - DexType::PumpFun, - mint_pubkey, - Some(creator), - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - None, - None, - ).await?; + client + .sell( + DexType::PumpFun, + mint_pubkey, + Some(creator), + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + Box::new(PumpFunParams::from_dev_trade( + &mint_pubkey, + trade_info.token_amount, + trade_info.max_sol_cost, + creator, + None, + )), + None, + ) + .await?; Ok(()) } @@ -186,60 +207,46 @@ async fn test_pumpswap() -> AnyResult<()> { let slippage_basis_points = Some(100); let recent_blockhash = client.rpc.get_latest_blockhash().await?; let pool_address = Pubkey::from_str("xxxxxxx")?; - let base_mint = Pubkey::from_str("2zMMhcVQEXDtdE6vsFS7S7D5oUodfJHE8vd1gnBouauv")?; - let quote_mint = Pubkey::from_str("So11111111111111111111111111111111111111112")?; - let pool_base_token_reserves = 0; // Input the correct value - let pool_quote_token_reserves = 0; // Input the correct value // Buy tokens println!("Buying tokens from PumpSwap..."); - client.buy( - DexType::PumpSwap, - mint_pubkey, - Some(creator), - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - Some(Box::new(PumpSwapParams { - pool: Some(pool_address), - base_mint: Some(base_mint), - quote_mint: Some(quote_mint), - pool_base_token_reserves: Some(pool_base_token_reserves), - pool_quote_token_reserves: Some(pool_quote_token_reserves), - auto_handle_wsol: true, - })), - None, - ).await?; + client + .buy( + DexType::PumpSwap, + mint_pubkey, + Some(creator), + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + None, + // 经过 rpc,增加耗时,可以通过from_buy_trade或者自行初始化PumpSwapParams参数来优化耗时 + Box::new(PumpSwapParams::from_pool_address_by_rpc(&client.rpc, &pool_address).await?), + None, + ) + .await?; // Sell tokens println!("Selling tokens from PumpSwap..."); let amount_token = 0; - client.sell( - DexType::PumpSwap, - mint_pubkey, - Some(creator), - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - Some(Box::new(PumpSwapParams { - pool: Some(pool_address), - base_mint: Some(base_mint), - quote_mint: Some(quote_mint), - pool_base_token_reserves: Some(pool_base_token_reserves), - pool_quote_token_reserves: Some(pool_quote_token_reserves), - auto_handle_wsol: true, - })), - None, - ).await?; + client + .sell( + DexType::PumpSwap, + mint_pubkey, + Some(creator), + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + // 经过 rpc,增加耗时,可以通过from_sell_trade或者自行初始化PumpSwapParams参数来优化耗时 + Box::new(PumpSwapParams::from_pool_address_by_rpc(&client.rpc, &pool_address).await?), + None, + ) + .await?; Ok(()) } - - async fn test_bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()> { println!("Testing Bonk trading..."); @@ -251,33 +258,37 @@ async fn test_bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult // Buy tokens println!("Buying tokens from letsbonk.fun..."); - client.buy( - DexType::Bonk, - mint_pubkey, - None, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - Some(Box::new(BonkParams::from_trade(trade_info))), - None, - ).await?; + client + .buy( + DexType::Bonk, + mint_pubkey, + None, + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + None, + Box::new(BonkParams::from_trade(trade_info.clone())), + None, + ) + .await?; // Sell tokens println!("Selling tokens from letsbonk.fun..."); let amount_token = 0; - client.sell( - DexType::Bonk, - mint_pubkey, - None, - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - None, - None, - ).await?; + client + .sell( + DexType::Bonk, + mint_pubkey, + None, + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + Box::new(BonkParams::from_trade(trade_info)), + None, + ) + .await?; Ok(()) } @@ -297,38 +308,41 @@ async fn test_bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyRe // Buy tokens println!("Buying tokens from letsbonk.fun..."); - client.buy( - DexType::Bonk, - mint_pubkey, - None, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - Some(Box::new(BonkParams::from_dev_trade(trade_info))), - None, - ).await?; + client + .buy( + DexType::Bonk, + mint_pubkey, + None, + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + None, + Box::new(BonkParams::from_dev_trade(trade_info.clone())), + None, + ) + .await?; // Sell tokens println!("Selling tokens from letsbonk.fun..."); let amount_token = 0; - client.sell( - DexType::Bonk, - mint_pubkey, - None, - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - None, - None, - ).await?; + client + .sell( + DexType::Bonk, + mint_pubkey, + None, + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + Box::new(BonkParams::from_dev_trade(trade_info)), + None, + ) + .await?; Ok(()) } - async fn test_bonk() -> Result<(), Box> { println!("Testing Bonk trading..."); @@ -340,38 +354,43 @@ async fn test_bonk() -> Result<(), Box> { // Buy tokens println!("Buying tokens from letsbonk.fun..."); - client.buy( - DexType::Bonk, - mint_pubkey, - None, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - None, - None, - ).await?; + client + .buy( + DexType::Bonk, + mint_pubkey, + None, + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + None, + // 经过 rpc,增加耗时,可以通过from_trade或者自行初始化BonkParams参数来优化耗时 + Box::new(BonkParams::from_mint_by_rpc(&client.rpc, &mint_pubkey).await?), + None, + ) + .await?; // Sell tokens println!("Selling tokens from letsbonk.fun..."); let amount_token = 0; - client.sell( - DexType::Bonk, - mint_pubkey, - None, - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - None, - None, - ).await?; + client + .sell( + DexType::Bonk, + mint_pubkey, + None, + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + // 经过 rpc,增加耗时,可以通过from_trade或者自行初始化BonkParams参数来优化耗时 + Box::new(BonkParams::from_mint_by_rpc(&client.rpc, &mint_pubkey).await?), + None, + ) + .await?; Ok(()) } - async fn test_raydium_cpmm() -> Result<(), Box> { println!("Testing Raydium Cpmm trading..."); @@ -380,56 +399,52 @@ async fn test_raydium_cpmm() -> Result<(), Box> { let buy_sol_cost = 100_000; let slippage_basis_points = Some(100); let recent_blockhash = client.rpc.get_latest_blockhash().await?; - let pool_state = Pubkey::from_str("xxxxxxx")?; - let buy_amount_out = get_buy_token_amount(&client.rpc, &pool_state, buy_sol_cost).await?; + let pool_address = Pubkey::from_str("xxxxxxx")?; // Buy tokens println!("Buying tokens from Raydium Cpmm..."); - client.buy( - DexType::RaydiumCpmm, - mint_pubkey, - None, - buy_sol_cost, - slippage_basis_points, - recent_blockhash, - None, - Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // 如果不传,会自动计算 - mint_token_program: Some(spl_token::ID), // spl_token_2022::ID - mint_token_in_pool_state_index: Some(1), // mint_token 在 pool_state 中的索引,默认在索引1 - minimum_amount_out: Some(buy_amount_out), // 如果不传、默认为0 - auto_handle_wsol: true, - })), - None, - ).await?; + client + .buy( + DexType::RaydiumCpmm, + mint_pubkey, + None, + buy_sol_cost, + slippage_basis_points, + recent_blockhash, + None, + // 经过 rpc,增加耗时,或者自行初始化RaydiumCpmmParams参数 + Box::new( + RaydiumCpmmParams::from_pool_address_by_rpc(&client.rpc, &pool_address).await?, + ), + None, + ) + .await?; // Sell tokens println!("Selling tokens from Raydium Cpmm..."); let amount_token = 0; - let sell_sol_amount = get_sell_sol_amount(&client.rpc, &pool_state, amount_token).await?; - client.sell( - DexType::RaydiumCpmm, - mint_pubkey, - None, - amount_token, - slippage_basis_points, - recent_blockhash, - None, - false, - Some(Box::new(RaydiumCpmmParams { - pool_state: Some(pool_state), // 如果不传,会自动计算 - mint_token_program: Some(spl_token::ID), // spl_token_2022::ID - mint_token_in_pool_state_index: Some(1), // mint_token 在 pool_state 中的索引,默认在索引1 - minimum_amount_out: Some(sell_sol_amount), // 如果不传、默认为0 - auto_handle_wsol: true, - })), - None, - ).await?; + client + .sell( + DexType::RaydiumCpmm, + mint_pubkey, + None, + amount_token, + slippage_basis_points, + recent_blockhash, + None, + false, + // 经过 rpc,增加耗时,或者自行初始化RaydiumCpmmParams参数 + Box::new( + RaydiumCpmmParams::from_pool_address_by_rpc(&client.rpc, &pool_address).await?, + ), + None, + ) + .await?; Ok(()) } async fn test_grpc() -> Result<(), Box> { - println!("正在订阅 GRPC 事件..."); + println!("Subscribing to GRPC events..."); let grpc = YellowstoneGrpc::new( "https://solana-yellowstone-grpc.publicnode.com:443".to_string(), @@ -437,37 +452,39 @@ async fn test_grpc() -> Result<(), Box> { )?; let callback = create_event_callback(); - let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm]; + let protocols = + vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk, Protocol::RaydiumCpmm]; // Filter accounts let account_include = vec![ - PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID - PUMPSWAP_PROGRAM_ID.to_string(), // Listen to pumpswap program ID - BONK_PROGRAM_ID.to_string(), // Listen to bonk program ID - RAYDIUM_CPMM_PROGRAM_ID.to_string(), // Listen to raydium_cpmm program ID - RAYDIUM_CLMM_PROGRAM_ID.to_string(), // Listen to raydium_clmm program ID + PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID + PUMPSWAP_PROGRAM_ID.to_string(), // Listen to pumpswap program ID + BONK_PROGRAM_ID.to_string(), // Listen to bonk program ID + RAYDIUM_CPMM_PROGRAM_ID.to_string(), // Listen to raydium_cpmm program ID + RAYDIUM_CLMM_PROGRAM_ID.to_string(), // Listen to raydium_clmm program ID RAYDIUM_AMM_V4_PROGRAM_ID.to_string(), // Listen to raydium_amm_v4 program ID - "xxxxxxxx".to_string(), // Listen to xxxxx account + "xxxxxxxx".to_string(), // Listen to xxxxx account ]; let account_exclude = vec![]; let account_required = vec![]; - // 监听交易数据 - let transaction_filter = TransactionFilter { + // Listen to transaction data + let transaction_filter = TransactionFilter { account_include: account_include.clone(), account_exclude, account_required, }; - // 监听属于owner程序的账号数据 -> 账号事件监听 + // Listen to account data belonging to owner programs -> account event monitoring let account_filter = AccountFilter { account: vec![], owner: account_include.clone() }; - println!("开始监听事件,按 Ctrl+C 停止..."); + println!("Starting to listen for events, press Ctrl+C to stop..."); grpc.subscribe_events_immediate( protocols, None, transaction_filter, account_filter, None, + None, callback, ) .await?; @@ -476,14 +493,14 @@ async fn test_grpc() -> Result<(), Box> { } async fn test_shreds() -> Result<(), Box> { - println!("正在订阅 ShredStream 事件..."); + println!("Subscribing to ShredStream events..."); let shred_stream = ShredStreamGrpc::new("http://127.0.0.1:10800".to_string()).await?; let callback = create_event_callback(); let protocols = vec![Protocol::PumpFun, Protocol::PumpSwap, Protocol::Bonk]; - println!("开始监听事件,按 Ctrl+C 停止..."); - shred_stream.shredstream_subscribe(protocols, None, callback).await?; + println!("Starting to listen for events, press Ctrl+C to stop..."); + shred_stream.shredstream_subscribe(protocols, None, None, callback).await?; Ok(()) } @@ -521,8 +538,8 @@ fn create_event_callback() -> impl Fn(Box) { RaydiumCpmmSwapEvent => |e: RaydiumCpmmSwapEvent| { println!("RaydiumCpmmSwapEvent: {:?}", e); }, - // ..... - // 更多的事件和说明请参考 https://github.com/0xfnzero/solana-streamer + // ..... + // For more events and documentation, please refer to https://github.com/0xfnzero/solana-streamer }); } } diff --git a/src/trading/common/utils.rs b/src/trading/common/utils.rs index 548f950..7a38c88 100644 --- a/src/trading/common/utils.rs +++ b/src/trading/common/utils.rs @@ -28,22 +28,6 @@ pub async fn get_sol_balance( Ok(balance) } -// Calculate slippage for buy operations -#[inline] -pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { - amount + (amount * basis_points / 10000) -} - -// Calculate slippage for sell operations -#[inline] -pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { - if amount <= basis_points / 10000 { - 1 - } else { - amount - (amount * basis_points / 10000) - } -} - pub async fn transfer_sol( rpc: &SolanaRpcClient, payer: &Keypair, @@ -75,38 +59,39 @@ pub async fn transfer_sol( Ok(()) } -/// 关闭代币账户 +/// Close token account /// -/// 此函数用于关闭指定代币的关联代币账户,将账户中的代币余额转移给账户所有者。 +/// This function is used to close the associated token account for a specified token, +/// transferring the token balance in the account to the account owner. /// -/// # 参数 +/// # Parameters /// -/// * `rpc` - Solana RPC客户端 -/// * `payer` - 支付交易费用的账户 -/// * `mint` - 代币的Mint地址 +/// * `rpc` - Solana RPC client +/// * `payer` - Account that pays transaction fees +/// * `mint` - Token mint address /// -/// # 返回值 +/// # Returns /// -/// 返回一个Result,成功时返回(),失败时返回错误 +/// Returns a Result, success returns (), failure returns error pub async fn close_token_account( rpc: &SolanaRpcClient, payer: &Keypair, mint: &Pubkey, ) -> Result<(), anyhow::Error> { - // 获取关联代币账户地址 + // Get associated token account address let ata = get_associated_token_address(&payer.pubkey(), mint); - // 检查账户是否存在 + // Check if account exists let account_exists = rpc.get_account(&ata).await.is_ok(); if !account_exists { - return Ok(()); // 如果账户不存在,直接返回成功 + return Ok(()); // If account doesn't exist, return success directly } - // 构建关闭账户指令 + // Build close account instruction let close_account_ix = close_account(&spl_token::ID, &ata, &payer.pubkey(), &payer.pubkey(), &[&payer.pubkey()])?; - // 构建交易 + // Build transaction let recent_blockhash = rpc.get_latest_blockhash().await?; let transaction = Transaction::new_signed_with_payer( &[close_account_ix], @@ -115,7 +100,7 @@ pub async fn close_token_account( recent_blockhash, ); - // 发送交易 + // Send transaction rpc.send_and_confirm_transaction(&transaction).await?; Ok(()) diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index e4bd262..60a4cf8 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -1,17 +1,26 @@ use solana_hash::Hash; use solana_sdk::{pubkey::Pubkey, signature::Keypair}; +use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; +use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::{ + PumpSwapBuyEvent, PumpSwapSellEvent, +}; use std::sync::Arc; use super::traits::ProtocolParams; use crate::common::bonding_curve::BondingCurveAccount; use crate::common::{PriorityFee, SolanaRpcClient}; -use crate::constants::bonk::accounts::{PLATFORM_FEE_RATE, PROTOCOL_FEE_RATE, SHARE_FEE_RATE}; +use crate::constants::bonk::accounts::{ + self, PLATFORM_FEE_RATE, PROTOCOL_FEE_RATE, SHARE_FEE_RATE, +}; use crate::solana_streamer_sdk::streaming::event_parser::common::EventType; use crate::solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent; use crate::swqos::SwqosClient; use crate::trading::bonk::common::{get_amount_in, get_amount_in_net, get_amount_out}; +use crate::trading::pumpswap::common::get_token_balances; +use crate::trading::raydium_cpmm::common::get_pool_token_balances; -/// 通用买入参数 +/// Common buy parameters +/// Contains all necessary information for executing buy transactions #[derive(Clone)] pub struct BuyParams { pub rpc: Option>, @@ -27,7 +36,8 @@ pub struct BuyParams { pub protocol_params: Box, } -/// 带MEV服务的买入参数 +/// Buy parameters with MEV service support +/// Extends BuyParams with MEV client configurations for transaction acceleration #[derive(Clone)] pub struct BuyWithTipParams { pub rpc: Option>, @@ -44,7 +54,8 @@ pub struct BuyWithTipParams { pub protocol_params: Box, } -/// 通用卖出参数 +/// Common sell parameters +/// Contains all necessary information for executing sell transactions #[derive(Clone)] pub struct SellParams { pub rpc: Option>, @@ -59,7 +70,8 @@ pub struct SellParams { pub protocol_params: Box, } -/// 带MEV服务的卖出参数 +/// Sell parameters with MEV service support +/// Extends SellParams with MEV client configurations for transaction acceleration #[derive(Clone)] pub struct SellWithTipParams { pub rpc: Option>, @@ -75,16 +87,39 @@ pub struct SellWithTipParams { pub protocol_params: Box, } -/// PumpFun协议特定参数 +/// PumpFun protocol specific parameters +/// Configuration parameters specific to PumpFun trading protocol #[derive(Clone)] pub struct PumpFunParams { - pub bonding_curve: Option>, + pub bonding_curve: Arc, + /// Whether to close token account when selling, only effective during sell operations + pub close_token_account_when_sell: Option, } impl PumpFunParams { - pub fn default() -> Self { + pub fn from_dev_trade( + mint: &Pubkey, + dev_token_amount: u64, + dev_sol_amount: u64, + creator: Pubkey, + close_token_account_when_sell: Option, + ) -> Self { + let bonding_curve = + BondingCurveAccount::from_dev_trade(mint, dev_token_amount, dev_sol_amount, creator); Self { - bonding_curve: None, + bonding_curve: Arc::new(bonding_curve), + close_token_account_when_sell: close_token_account_when_sell, + } + } + + pub fn from_trade( + event: &PumpFunTradeEvent, + close_token_account_when_sell: Option, + ) -> Self { + let bonding_curve = BondingCurveAccount::from_trade(event); + Self { + bonding_curve: Arc::new(bonding_curve), + close_token_account_when_sell: close_token_account_when_sell, } } } @@ -110,41 +145,61 @@ impl ProtocolParams for PumpFunParams { #[derive(Clone)] pub struct PumpSwapParams { /// Liquidity pool address - /// If None, it will be queried via RPC, which adds latency - pub pool: Option, - + pub pool: Pubkey, /// Base token mint address /// The mint account address of the base token in the trading pair - /// If None, it will be queried via RPC, which adds latency - pub base_mint: Option, - + pub base_mint: Pubkey, /// Quote token mint address /// The mint account address of the quote token in the trading pair, usually SOL or USDC - /// If None, it will be queried via RPC, which adds latency - pub quote_mint: Option, - + pub quote_mint: Pubkey, /// Base token reserves in the pool - pub pool_base_token_reserves: Option, - + pub pool_base_token_reserves: u64, /// Quote token reserves in the pool - pub pool_quote_token_reserves: Option, - + pub pool_quote_token_reserves: u64, /// Automatically handle WSOL wrapping /// When true, automatically handles wrapping and unwrapping operations between SOL and WSOL pub auto_handle_wsol: bool, } impl PumpSwapParams { - pub fn default() -> Self { + pub fn from_buy_trade(event: &PumpSwapBuyEvent) -> Self { Self { - pool: None, - base_mint: None, - quote_mint: None, - pool_base_token_reserves: None, - pool_quote_token_reserves: None, + pool: event.pool, + base_mint: event.base_mint, + quote_mint: event.quote_mint, + pool_base_token_reserves: event.pool_base_token_reserves, + pool_quote_token_reserves: event.pool_quote_token_reserves, auto_handle_wsol: true, } } + + pub fn from_sell_trade(event: &PumpSwapSellEvent) -> Self { + Self { + pool: event.pool, + base_mint: event.base_mint, + quote_mint: event.quote_mint, + pool_base_token_reserves: event.pool_base_token_reserves, + pool_quote_token_reserves: event.pool_quote_token_reserves, + auto_handle_wsol: true, + } + } + + pub async fn from_pool_address_by_rpc( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let pool_data = crate::trading::pumpswap::common::fetch_pool(rpc, pool_address).await?; + let (pool_base_token_reserves, pool_quote_token_reserves) = + get_token_balances(&pool_data, rpc).await?; + Ok(Self { + pool: pool_address.clone(), + base_mint: pool_data.base_mint, + quote_mint: pool_data.quote_mint, + pool_base_token_reserves: pool_base_token_reserves, + pool_quote_token_reserves: pool_quote_token_reserves, + auto_handle_wsol: true, + }) + } } impl ProtocolParams for PumpSwapParams { @@ -157,32 +212,28 @@ impl ProtocolParams for PumpSwapParams { } } -/// Bonk协议特定参数 +/// Bonk protocol specific parameters +/// Configuration parameters specific to Bonk trading protocol #[derive(Clone)] pub struct BonkParams { - pub virtual_base: Option, - pub virtual_quote: Option, - pub real_base: Option, - pub real_quote: Option, + pub virtual_base: u128, + pub virtual_quote: u128, + pub real_base: u128, + pub real_quote: u128, + /// Token program ID + /// Specifies the program used by the token, usually spl_token::ID or spl_token_2022::ID + pub mint_token_program: Pubkey, pub auto_handle_wsol: bool, } impl BonkParams { - pub fn default() -> Self { - Self { - virtual_base: None, - virtual_quote: None, - real_base: None, - real_quote: None, - auto_handle_wsol: true, - } - } pub fn from_trade(trade_info: BonkTradeEvent) -> Self { Self { - virtual_base: Some(trade_info.virtual_base as u128), - virtual_quote: Some(trade_info.virtual_quote as u128), - real_base: Some(trade_info.real_base_after as u128), - real_quote: Some(trade_info.real_quote_after as u128), + virtual_base: trade_info.virtual_base as u128, + virtual_quote: trade_info.virtual_quote as u128, + real_base: trade_info.real_base_after as u128, + real_quote: trade_info.real_quote_after as u128, + mint_token_program: trade_info.base_token_program, auto_handle_wsol: true, } } @@ -205,12 +256,9 @@ impl BonkParams { 0, ) }; - let real_quote = get_amount_in_net( - amount_in, - PROTOCOL_FEE_RATE, - PLATFORM_FEE_RATE, - SHARE_FEE_RATE, - ) as u128; + let real_quote = + get_amount_in_net(amount_in, PROTOCOL_FEE_RATE, PLATFORM_FEE_RATE, SHARE_FEE_RATE) + as u128; let amount_out = if trade_info.metadata.event_type == EventType::BonkBuyExactIn { get_amount_out( trade_info.amount_in, @@ -228,13 +276,33 @@ impl BonkParams { }; let real_base = amount_out; Self { - virtual_base: Some(DEFAULT_VIRTUAL_BASE), - virtual_quote: Some(DEFAULT_VIRTUAL_QUOTE), - real_base: Some(real_base), - real_quote: Some(real_quote), + virtual_base: DEFAULT_VIRTUAL_BASE, + virtual_quote: DEFAULT_VIRTUAL_QUOTE, + real_base: real_base, + real_quote: real_quote, + mint_token_program: trade_info.base_token_program, auto_handle_wsol: true, } } + + pub async fn from_mint_by_rpc( + rpc: &SolanaRpcClient, + mint: &Pubkey, + ) -> Result { + let pool_address = + crate::trading::bonk::common::get_pool_pda(mint, &accounts::WSOL_TOKEN_ACCOUNT) + .unwrap(); + let pool_data = crate::trading::bonk::common::fetch_pool_state(rpc, &pool_address).await?; + let token_account = rpc.get_account(&pool_data.base_mint).await?; + Ok(Self { + virtual_base: pool_data.virtual_base as u128, + virtual_quote: pool_data.virtual_quote as u128, + real_base: pool_data.real_base as u128, + real_quote: pool_data.real_quote as u128, + mint_token_program: token_account.owner, + auto_handle_wsol: true, + }) + } } impl ProtocolParams for BonkParams { @@ -247,30 +315,45 @@ impl ProtocolParams for BonkParams { } } -/// RaydiumCpmm协议特定参数 +/// RaydiumCpmm protocol specific parameters +/// Configuration parameters specific to Raydium CPMM trading protocol #[derive(Clone)] pub struct RaydiumCpmmParams { - /// 池子状态账户地址 - pub pool_state: Option, - /// 代币程序ID - /// 指定代币使用的程序,通常为 spl_token::ID 或 spl_token_2022::ID - pub mint_token_program: Option, - /// 指定 mint_token 在 pool_state 账户数据中的索引位置 - /// 默认值为1,表示在索引1的位置 - pub mint_token_in_pool_state_index: Option, - pub minimum_amount_out: Option, + /// Base token mint address + pub base_mint: Pubkey, + /// Quote token mint address + pub quote_mint: Pubkey, + /// Base token reserve amount in the pool + pub base_reserve: u64, + /// Quote token reserve amount in the pool + pub quote_reserve: u64, + /// Base token program ID (usually spl_token::ID or spl_token_2022::ID) + pub base_token_program: Pubkey, + /// Quote token program ID (usually spl_token::ID or spl_token_2022::ID) + pub quote_token_program: Pubkey, + /// Whether to automatically handle wSOL wrapping and unwrapping pub auto_handle_wsol: bool, } impl RaydiumCpmmParams { - pub fn default() -> Self { - Self { - pool_state: None, - mint_token_program: Some(spl_token::ID), - mint_token_in_pool_state_index: Some(1), - minimum_amount_out: None, + pub async fn from_pool_address_by_rpc( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let pool = + crate::trading::raydium_cpmm::common::fetch_pool_state(rpc, pool_address).await?; + let (token0_balance, token1_balance) = + get_pool_token_balances(rpc, pool_address, &pool.token0_mint, &pool.token1_mint) + .await?; + Ok(Self { + base_mint: pool.token0_mint, + quote_mint: pool.token1_mint, + base_reserve: token0_balance, + quote_reserve: token1_balance, + base_token_program: pool.token0_program, + quote_token_program: pool.token1_program, auto_handle_wsol: true, - } + }) } } @@ -285,7 +368,8 @@ impl ProtocolParams for RaydiumCpmmParams { } impl BuyParams { - /// 转换为BuyWithTipParams + /// Convert to BuyWithTipParams + /// Transforms basic buy parameters into MEV-enabled parameters pub fn with_tip(self, swqos_clients: Vec>) -> BuyWithTipParams { BuyWithTipParams { rpc: self.rpc, @@ -305,7 +389,8 @@ impl BuyParams { } impl SellParams { - /// 转换为SellWithTipParams + /// Convert to SellWithTipParams + /// Transforms basic sell parameters into MEV-enabled parameters pub fn with_tip(self, swqos_clients: Vec>) -> SellWithTipParams { SellWithTipParams { rpc: self.rpc, diff --git a/src/trading/pumpfun/common.rs b/src/trading/pumpfun/common.rs index 07ce482..9d07769 100755 --- a/src/trading/pumpfun/common.rs +++ b/src/trading/pumpfun/common.rs @@ -1,19 +1,16 @@ -use anyhow::anyhow; -use tokio::sync::RwLock; -use std::{collections::HashMap, sync::Arc}; -use solana_sdk::{ - compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey -}; +use crate::solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; use crate::{ common::{ - bonding_curve::BondingCurveAccount, global::GlobalAccount, PriorityFee, SolanaRpcClient + bonding_curve::BondingCurveAccount, global::GlobalAccount, PriorityFee, SolanaRpcClient, }, - constants::{ - self, pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, trade::trade::DEFAULT_SLIPPAGE - }, - trading::common::calculate_with_slippage_buy + constants::{self, trade::trade::DEFAULT_SLIPPAGE}, }; -use crate::solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; +use anyhow::anyhow; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, instruction::Instruction, pubkey::Pubkey, +}; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::RwLock; lazy_static::lazy_static! { static ref ACCOUNT_CACHE: RwLock>> = RwLock::new(HashMap::new()); @@ -24,15 +21,18 @@ pub fn create_priority_fee_instructions(priority_fee: PriorityFee) -> Vec Pubkey { static GLOBAL_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - Pubkey::find_program_address(&[constants::pumpfun::seeds::GLOBAL_SEED], &constants::pumpfun::accounts::PUMPFUN).0 + Pubkey::find_program_address( + &[constants::pumpfun::seeds::GLOBAL_SEED], + &constants::pumpfun::accounts::PUMPFUN, + ) + .0 }); *GLOBAL_PDA } @@ -40,7 +40,11 @@ pub fn get_global_pda() -> Pubkey { #[inline] pub fn get_mint_authority_pda() -> Pubkey { static MINT_AUTHORITY_PDA: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { - Pubkey::find_program_address(&[constants::pumpfun::seeds::MINT_AUTHORITY_SEED], &constants::pumpfun::accounts::PUMPFUN).0 + Pubkey::find_program_address( + &[constants::pumpfun::seeds::MINT_AUTHORITY_SEED], + &constants::pumpfun::accounts::PUMPFUN, + ) + .0 }); *MINT_AUTHORITY_PDA } @@ -63,7 +67,8 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { #[inline] pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option { - let seeds: &[&[u8]; 2] = &[constants::pumpfun::seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()]; + let seeds: &[&[u8]; 2] = + &[constants::pumpfun::seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()]; let program_id: &Pubkey = &constants::pumpfun::accounts::PUMPFUN; let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id); pda.map(|pubkey| pubkey.0) @@ -85,48 +90,35 @@ pub fn get_metadata_pda(mint: &Pubkey) -> Pubkey { constants::pumpfun::accounts::MPL_TOKEN_METADATA.as_ref(), mint.as_ref(), ], - &constants::pumpfun::accounts::MPL_TOKEN_METADATA - ).0 + &constants::pumpfun::accounts::MPL_TOKEN_METADATA, + ) + .0 } #[inline] -pub async fn get_global_account(/*rpc: &SolanaRpcClient*/) -> Result, anyhow::Error> { +pub async fn get_global_account(/*rpc: &SolanaRpcClient*/ +) -> Result, anyhow::Error> { let global_account = GlobalAccount::new(); let global_account = Arc::new(global_account); Ok(global_account) } #[inline] -pub async fn get_initial_buy_price(global_account: &Arc, amount_sol: u64) -> Result { +pub async fn get_initial_buy_price( + global_account: &Arc, + amount_sol: u64, +) -> Result { let buy_amount = global_account.get_initial_buy_price(amount_sol); Ok(buy_amount) } -#[inline] -pub async fn get_bonding_curve_account( - rpc: &SolanaRpcClient, - mint: &Pubkey, -) -> Result<(Arc, Pubkey), anyhow::Error> { - let bonding_curve_pda = get_bonding_curve_pda(mint) - .ok_or(anyhow!("Bonding curve not found"))?; - - let account = rpc.get_account(&bonding_curve_pda).await?; - if account.data.is_empty() { - return Err(anyhow!("Bonding curve not found")); - } - - let bonding_curve = Arc::new(bincode::deserialize::(&account.data)?); - - Ok((bonding_curve, bonding_curve_pda)) -} - #[inline] pub async fn fetch_bonding_curve_account( rpc: &SolanaRpcClient, mint: &Pubkey, -) -> Result<(Arc, Pubkey), anyhow::Error> { - let bonding_curve_pda: Pubkey = get_bonding_curve_pda(mint) - .ok_or(anyhow!("Bonding curve not found"))?; +) -> Result<(Arc, Pubkey), anyhow::Error>{ + let bonding_curve_pda: Pubkey = + get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?; let account = rpc.get_account(&bonding_curve_pda).await?; if account.data.is_empty() { @@ -139,63 +131,6 @@ pub async fn fetch_bonding_curve_account( Ok((Arc::new(bonding_curve), bonding_curve_pda)) } -#[inline] -pub fn get_buy_token_amount( - bonding_curve_account: &BondingCurveAccount, - buy_sol_cost: u64, - slippage_basis_points: Option, -) -> anyhow::Result<(u64, u64)> { - let buy_token = bonding_curve_account.get_buy_price(buy_sol_cost).map_err(|e| anyhow!(e))?; - - let max_sol_cost = calculate_with_slippage_buy(buy_sol_cost, slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE)); - - Ok((buy_token, max_sol_cost)) -} - -pub fn get_buy_token_amount_from_sol_amount( - bonding_curve: &BondingCurveAccount, - amount: u64, -) -> u64 { - if amount == 0 { - return 0; - } - - if bonding_curve.virtual_token_reserves == 0 { - return 0; - } - - let total_fee_basis_points = FEE_BASIS_POINTS - + if bonding_curve.creator != Pubkey::default() { - CREATOR_FEE - } else { - 0 - }; - - // 转为 u128 防止溢出 - let amount_128 = amount as u128; - let total_fee_basis_points_128 = total_fee_basis_points as u128; - let input_amount = amount_128 - .checked_mul(10_000) - .unwrap() - .checked_div(total_fee_basis_points_128 + 10_000) - .unwrap(); - - let virtual_token_reserves = bonding_curve.virtual_token_reserves as u128; - let virtual_sol_reserves = bonding_curve.virtual_sol_reserves as u128; - let real_token_reserves = bonding_curve.real_token_reserves as u128; - - let denominator = virtual_sol_reserves + input_amount; - - let tokens_received = input_amount - .checked_mul(virtual_token_reserves) - .unwrap() - .checked_div(denominator) - .unwrap(); - - tokens_received.min(real_token_reserves) as u64 -} - - #[inline] pub async fn init_bonding_curve_account( mint: &Pubkey, @@ -203,7 +138,8 @@ pub async fn init_bonding_curve_account( dev_sol_cost: u64, creator: Pubkey, ) -> Result, anyhow::Error> { - let bonding_curve = BondingCurveAccount::from_dev_trade(mint, dev_buy_token, dev_sol_cost, creator); + let bonding_curve = + BondingCurveAccount::from_dev_trade(mint, dev_buy_token, dev_sol_cost, creator); let bonding_curve = Arc::new(bonding_curve); Ok(bonding_curve) } @@ -220,11 +156,12 @@ pub fn get_buy_price(amount: u64, trade_info: &PumpFunTradeEvent) -> u64 { return 0; } - let n: u128 = (trade_info.virtual_sol_reserves as u128) * (trade_info.virtual_token_reserves as u128); + let n: u128 = + (trade_info.virtual_sol_reserves as u128) * (trade_info.virtual_token_reserves as u128); let i: u128 = (trade_info.virtual_sol_reserves as u128) + (amount as u128); let r: u128 = n / i + 1; let s: u128 = (trade_info.virtual_token_reserves as u128) - r; let s_u64 = s as u64; - + s_u64.min(trade_info.real_token_reserves) } diff --git a/src/trading/pumpswap/common.rs b/src/trading/pumpswap/common.rs index d694ab8..f0f0fe0 100755 --- a/src/trading/pumpswap/common.rs +++ b/src/trading/pumpswap/common.rs @@ -11,118 +11,6 @@ pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result Result { - let product = pool_base_token_reserves as u128 * pool_quote_token_reserves as u128; - if quote_mint_is_wsol { - // base_amount_out - let mut sol_amount = sol_amount as u128; - sol_amount = sol_amount - .checked_mul(10000) - .unwrap() - .checked_div( - (10000 - + lp_fee_basis_points - + protocol_fee_basis_points - + coin_creator_fee_basis_points) as u128, - ) - .unwrap() - .checked_sub(1) - .unwrap(); - let new_quote_amount = pool_quote_token_reserves as u128 + sol_amount as u128; - let new_base_amount = product / new_quote_amount; - let token_amount = pool_base_token_reserves as u128 - new_base_amount; - return Ok(token_amount as u64); - } else { - // min_quote_amount_out - let new_base_amount = pool_base_token_reserves as u128 + sol_amount as u128; - let new_quote_amount = product / new_base_amount; - let token_amount = pool_quote_token_reserves as u128 - new_quote_amount; - let lp_fee = token_amount - .checked_mul(lp_fee_basis_points as u128) - .unwrap() - .checked_div(10000) - .unwrap(); - let protocol_fee = token_amount - .checked_mul(protocol_fee_basis_points as u128) - .unwrap() - .checked_div(10000) - .unwrap(); - let coin_creator_fee = token_amount - .checked_mul(coin_creator_fee_basis_points as u128) - .unwrap() - .checked_div(10000) - .unwrap(); - let token_amount = token_amount.checked_sub(lp_fee).unwrap(); - let token_amount = token_amount.checked_sub(protocol_fee).unwrap(); - let token_amount = token_amount.checked_sub(coin_creator_fee).unwrap(); - return Ok(token_amount as u64); - } -} - -pub async fn get_wsol_amount( - quote_mint_is_wsol: bool, - pool_base_token_reserves: u64, - pool_quote_token_reserves: u64, - token_amount: u64, - lp_fee_basis_points: u64, - protocol_fee_basis_points: u64, - coin_creator_fee_basis_points: u64, -) -> Result { - let product = pool_base_token_reserves as u128 * pool_quote_token_reserves as u128; - if !quote_mint_is_wsol { - // base_amount_out - let mut token_amount = token_amount as u128; - token_amount = token_amount - .checked_mul(10000) - .unwrap() - .checked_div( - (10000 - + lp_fee_basis_points - + protocol_fee_basis_points - + coin_creator_fee_basis_points) as u128, - ) - .unwrap() - .checked_sub(1) - .unwrap(); - let new_quote_amount = pool_quote_token_reserves as u128 + token_amount as u128; - let new_base_amount = product / new_quote_amount; - let wsol_amount = pool_base_token_reserves as u128 - new_base_amount; - Ok(wsol_amount as u64) - } else { - // min_quote_amount_out - let new_base_amount = pool_base_token_reserves as u128 + token_amount as u128; - let new_quote_amount = product / new_base_amount; - let token_amount = pool_quote_token_reserves as u128 - new_quote_amount; - let lp_fee = token_amount - .checked_mul(lp_fee_basis_points as u128) - .unwrap() - .checked_div(10000) - .unwrap(); - let protocol_fee = token_amount - .checked_mul(protocol_fee_basis_points as u128) - .unwrap() - .checked_div(10000) - .unwrap(); - let coin_creator_fee = token_amount - .checked_mul(coin_creator_fee_basis_points as u128) - .unwrap() - .checked_div(10000) - .unwrap(); - let wsol_amount = token_amount.checked_sub(lp_fee).unwrap(); - let wsol_amount = wsol_amount.checked_sub(protocol_fee).unwrap(); - let wsol_amount = wsol_amount.checked_sub(coin_creator_fee).unwrap(); - Ok(wsol_amount as u64) - } -} - pub(crate) fn coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey { let (pump_pool_authority, _) = Pubkey::find_program_address( &[b"creator_vault", &coin_creator.to_bytes()], @@ -276,37 +164,3 @@ pub async fn get_token_balances( Ok((base_amount, quote_amount)) } - -pub async fn calculate_buy_amount( - pool: &Pool, - rpc: &SolanaRpcClient, - sol_amount: u64, -) -> Result { - let (base_amount, quote_amount) = get_token_balances(pool, rpc).await?; - - // 使用常数乘积公式 (x * y = k) 计算 - let product = base_amount as u128 * quote_amount as u128; - let new_quote_amount = quote_amount as u128 + sol_amount as u128; - let new_base_amount = product / new_quote_amount; - - let token_amount = base_amount as u128 - new_base_amount; - - Ok(token_amount as u64) -} - -pub async fn calculate_sell_amount( - pool: &Pool, - rpc: &SolanaRpcClient, - token_amount: u64, -) -> Result { - let (base_amount, quote_amount) = get_token_balances(pool, rpc).await?; - - // 使用常数乘积公式 (x * y = k) 计算 - let product = base_amount as u128 * quote_amount as u128; - let new_base_amount = base_amount as u128 + token_amount as u128; - let new_quote_amount = product / new_base_amount; - - let sol_amount = quote_amount as u128 - new_quote_amount; - - Ok(sol_amount as u64) -} diff --git a/src/trading/raydium_cpmm/common.rs b/src/trading/raydium_cpmm/common.rs index f321e80..f35eff3 100755 --- a/src/trading/raydium_cpmm/common.rs +++ b/src/trading/raydium_cpmm/common.rs @@ -2,7 +2,7 @@ use crate::{ common::SolanaRpcClient, constants::{ self, - raydium_cpmm::accounts::{self, WSOL_TOKEN_ACCOUNT}, + raydium_cpmm::accounts::{self}, }, }; use anyhow::anyhow; @@ -52,94 +52,6 @@ pub fn get_observation_state_pda(pool_state: &Pubkey) -> Option { pda.map(|pubkey| pubkey.0) } -pub async fn get_buy_token_amount( - rpc: &SolanaRpcClient, - pool_state: &Pubkey, - sol_amount: u64, -) -> Result { - let pool = fetch_pool_state(rpc, pool_state).await?; - let is_token0_input = if pool.token0_mint == WSOL_TOKEN_ACCOUNT { true } else { false }; - let (token0_balance, token1_balance) = - get_pool_token_balances(rpc, pool_state, &pool.token0_mint, &pool.token1_mint).await?; - - // 使用恒定乘积公式计算 - - let (reserve_in, reserve_out) = if is_token0_input { - (token0_balance, token1_balance) - } else { - (token1_balance, token0_balance) - }; - - if reserve_in == 0 || reserve_out == 0 { - return Err(anyhow!("池子储备金为零,无法进行交换")); - } - - // 使用 u128 防止溢出 - let amount_in_128 = sol_amount as u128; - let reserve_in_128 = reserve_in as u128; - let reserve_out_128 = reserve_out as u128; - - // 恒定乘积公式: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in) - let numerator = amount_in_128 * reserve_out_128; - let denominator = reserve_in_128 + amount_in_128; - - if denominator == 0 { - return Err(anyhow!("分母为零,计算错误")); - } - - let amount_out = numerator / denominator; - - // 检查是否超出储备金 - if amount_out >= reserve_out_128 { - return Err(anyhow!("输出数量超过池子储备金")); - } - - Ok(amount_out as u64) -} - -pub async fn get_sell_sol_amount( - rpc: &SolanaRpcClient, - pool_state: &Pubkey, - token_amount: u64, -) -> Result { - let pool = fetch_pool_state(rpc, pool_state).await?; - let is_token0_sol = if pool.token0_mint == WSOL_TOKEN_ACCOUNT { true } else { false }; - let (token0_balance, token1_balance) = - get_pool_token_balances(rpc, pool_state, &pool.token0_mint, &pool.token1_mint).await?; - - let (reserve_in, reserve_out) = if is_token0_sol { - (token1_balance, token0_balance) - } else { - (token0_balance, token1_balance) - }; - - if reserve_in == 0 || reserve_out == 0 { - return Err(anyhow!("池子储备金为零,无法进行交换")); - } - - // 使用 u128 防止溢出 - let amount_in_128 = token_amount as u128; - let reserve_in_128 = reserve_in as u128; - let reserve_out_128 = reserve_out as u128; - - // 恒定乘积公式: amount_out = (amount_in * reserve_out) / (reserve_in + amount_in) - let numerator = amount_in_128 * reserve_out_128; - let denominator = reserve_in_128 + amount_in_128; - - if denominator == 0 { - return Err(anyhow!("分母为零,计算错误")); - } - - let amount_out = numerator / denominator; - - // 检查是否超出储备金 - if amount_out >= reserve_out_128 { - return Err(anyhow!("输出数量超过池子储备金")); - } - - Ok(amount_out as u64) -} - /// 获取池子中两个代币的余额 /// /// # 返回值 diff --git a/src/utils/calc/bonk.rs b/src/utils/calc/bonk.rs new file mode 100644 index 0000000..5432344 --- /dev/null +++ b/src/utils/calc/bonk.rs @@ -0,0 +1,112 @@ +use crate::constants::bonk::accounts; + +/// Calculates the amount of tokens to receive when buying with SOL +/// +/// This function implements the constant product formula (x * y = k) for token swaps, +/// taking into account various fees and slippage protection. +/// +/// # Arguments +/// +/// * `amount_in` - The amount of SOL to spend (in lamports) +/// * `virtual_base` - Virtual base token reserves +/// * `virtual_quote` - Virtual quote token (SOL) reserves +/// * `real_base` - Real base token reserves +/// * `real_quote` - Real quote token (SOL) reserves +/// * `slippage_basis_points` - Maximum slippage tolerance in basis points (e.g., 100 = 1%) +/// +/// # Returns +/// +/// The minimum amount of tokens that will be received after fees and slippage +pub fn get_buy_token_amount_from_sol_amount( + amount_in: u64, + virtual_base: u128, + virtual_quote: u128, + real_base: u128, + real_quote: u128, + slippage_basis_points: u128, +) -> u64 { + let amount_in_u128 = amount_in as u128; + + // Calculate various fees deducted from input amount + let protocol_fee = (amount_in_u128 * accounts::PROTOCOL_FEE_RATE / 10000) as u128; + let platform_fee = (amount_in_u128 * accounts::PLATFORM_FEE_RATE / 10000) as u128; + let share_fee = (amount_in_u128 * accounts::SHARE_FEE_RATE / 10000) as u128; + + // Calculate net input amount after deducting all fees + let amount_in_net = amount_in_u128 + .checked_sub(protocol_fee) + .unwrap() + .checked_sub(platform_fee) + .unwrap() + .checked_sub(share_fee) + .unwrap(); + + // Calculate total reserves (virtual + real) + let input_reserve = virtual_quote.checked_add(real_quote).unwrap(); + let output_reserve = virtual_base.checked_sub(real_base).unwrap(); + + // Apply constant product formula: amount_out = (amount_in * output_reserve) / (input_reserve + amount_in) + let numerator = amount_in_net.checked_mul(output_reserve).unwrap(); + let denominator = input_reserve.checked_add(amount_in_net).unwrap(); + let mut amount_out = numerator.checked_div(denominator).unwrap(); + + // Apply slippage protection + amount_out = amount_out - (amount_out * slippage_basis_points) / 10000; + amount_out as u64 +} + +/// Calculates the amount of SOL to receive when selling tokens +/// +/// This function implements the constant product formula (x * y = k) for token swaps, +/// calculating the SOL output for a given token input amount, accounting for fees and slippage. +/// +/// # Arguments +/// +/// * `amount_in` - The amount of tokens to sell +/// * `virtual_base` - Virtual base token reserves +/// * `virtual_quote` - Virtual quote token (SOL) reserves +/// * `real_base` - Real base token reserves +/// * `real_quote` - Real quote token (SOL) reserves +/// * `slippage_basis_points` - Maximum slippage tolerance in basis points (e.g., 100 = 1%) +/// +/// # Returns +/// +/// The minimum amount of SOL that will be received after fees and slippage +pub fn get_sell_sol_amount_from_token_amount( + amount_in: u64, + virtual_base: u128, + virtual_quote: u128, + real_base: u128, + real_quote: u128, + slippage_basis_points: u128, +) -> u64 { + let amount_in_u128 = amount_in as u128; + + // For sell operation, input_reserve is token reserves, output_reserve is SOL reserves + let input_reserve = virtual_base.checked_add(real_base).unwrap(); + let output_reserve = virtual_quote.checked_add(real_quote).unwrap(); + + // Use constant product formula to calculate SOL amount received from selling tokens + let numerator = amount_in_u128.checked_mul(output_reserve).unwrap(); + let denominator = input_reserve.checked_add(amount_in_u128).unwrap(); + let sol_amount_out = numerator.checked_div(denominator).unwrap(); + + // Calculate various fees + let protocol_fee = (sol_amount_out * accounts::PROTOCOL_FEE_RATE / 10000) as u128; + let platform_fee = (sol_amount_out * accounts::PLATFORM_FEE_RATE / 10000) as u128; + let share_fee = (sol_amount_out * accounts::SHARE_FEE_RATE / 10000) as u128; + + // Net SOL amount after deducting fees + let sol_amount_net = sol_amount_out + .checked_sub(protocol_fee) + .unwrap() + .checked_sub(platform_fee) + .unwrap() + .checked_sub(share_fee) + .unwrap(); + + // Apply slippage protection + let final_amount = sol_amount_net - (sol_amount_net * slippage_basis_points) / 10000; + + final_amount as u64 +} diff --git a/src/utils/calc/common.rs b/src/utils/calc/common.rs new file mode 100644 index 0000000..88fedb5 --- /dev/null +++ b/src/utils/calc/common.rs @@ -0,0 +1,63 @@ +/// Calculate transaction fee based on amount and fee basis points +/// +/// # Parameters +/// * `amount` - Transaction amount +/// * `fee_basis_points` - Fee basis points, 1 basis point = 0.01% +/// +/// # Examples +/// * fee_basis_points = 1 -> 0.01% fee +/// * fee_basis_points = 10 -> 0.1% fee +/// * fee_basis_points = 25 -> 0.25% fee (common exchange rate) +/// * fee_basis_points = 100 -> 1% fee +pub fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 { + ceil_div(amount * fee_basis_points, 10_000) +} + +/// Ceiling division implementation +/// Ceiling division that ensures results are not lost due to integer division precision +/// +/// # Parameters +/// * `a` - Dividend +/// * `b` - Divisor +/// +/// # Returns +/// Returns the ceiling result of a/b +pub fn ceil_div(a: u128, b: u128) -> u128 { + (a + b - 1) / b +} + +/// Calculate buy amount with slippage protection +/// Add slippage percentage to the amount to ensure successful purchase +/// +/// # Parameters +/// * `amount` - Original transaction amount +/// * `basis_points` - Slippage basis points, 1 basis point = 0.01% +/// +/// # Examples +/// * basis_points = 1 -> 0.01% slippage +/// * basis_points = 10 -> 0.1% slippage +/// * basis_points = 100 -> 1% slippage +/// * basis_points = 500 -> 5% slippage +pub fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 { + amount + (amount * basis_points / 10000) +} + +/// Calculate sell amount with slippage protection +/// Subtract slippage percentage from the amount to ensure successful sale +/// +/// # Parameters +/// * `amount` - Original transaction amount +/// * `basis_points` - Slippage basis points, 1 basis point = 0.01% +/// +/// # Examples +/// * basis_points = 1 -> 0.01% slippage +/// * basis_points = 10 -> 0.1% slippage +/// * basis_points = 100 -> 1% slippage +/// * basis_points = 500 -> 5% slippage +pub fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 { + if amount <= basis_points / 10000 { + 1 + } else { + amount - (amount * basis_points / 10000) + } +} diff --git a/src/utils/calc/mod.rs b/src/utils/calc/mod.rs new file mode 100644 index 0000000..14d86c0 --- /dev/null +++ b/src/utils/calc/mod.rs @@ -0,0 +1,6 @@ +pub mod pumpfun; +pub mod common; +pub mod pumpswap; +pub mod bonk; +pub mod raydium_amm_v4; +pub mod raydium_cpmm; \ No newline at end of file diff --git a/src/utils/calc/pumpfun.rs b/src/utils/calc/pumpfun.rs new file mode 100644 index 0000000..275a14e --- /dev/null +++ b/src/utils/calc/pumpfun.rs @@ -0,0 +1,108 @@ +use solana_sdk::{native_token::sol_str_to_lamports, pubkey::Pubkey}; + +use crate::{ + constants::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS}, + utils::calc::common::compute_fee, +}; + +/// Calculates the amount of tokens that can be purchased with a given SOL amount +/// using the bonding curve formula. +/// +/// # Arguments +/// * `virtual_token_reserves` - Virtual token reserves in the bonding curve +/// * `virtual_sol_reserves` - Virtual SOL reserves in the bonding curve +/// * `real_token_reserves` - Actual token reserves available for purchase +/// * `creator` - Creator's public key (affects fee calculation) +/// * `amount` - SOL amount to spend (in lamports) +/// +/// # Returns +/// The amount of tokens that will be received (in token's smallest unit) +pub fn get_buy_token_amount_from_sol_amount( + virtual_token_reserves: u128, + virtual_sol_reserves: u128, + real_token_reserves: u128, + creator: Pubkey, + amount: u64, +) -> u64 { + if amount == 0 { + return 0; + } + + if virtual_token_reserves == 0 { + return 0; + } + + let total_fee_basis_points = + FEE_BASIS_POINTS + if creator != Pubkey::default() { CREATOR_FEE } else { 0 }; + + // Convert to u128 to prevent overflow + let amount_128 = amount as u128; + let total_fee_basis_points_128 = total_fee_basis_points as u128; + + let input_amount = amount_128 + .checked_mul(10_000) + .unwrap() + .checked_div(total_fee_basis_points_128 + 10_000) + .unwrap(); + + let denominator = virtual_sol_reserves + input_amount; + + let mut tokens_received = + input_amount.checked_mul(virtual_token_reserves).unwrap().checked_div(denominator).unwrap(); + + tokens_received = tokens_received.min(real_token_reserves); + + if tokens_received <= 100 * 1_000_000_u128 { + tokens_received = if amount > sol_str_to_lamports("0.01").unwrap_or(0) { + 25547619 * 1_000_000_u128 + } else { + 255476 * 1_000_000_u128 + }; + } + + tokens_received as u64 +} + +/// Calculates the amount of SOL that will be received when selling a given token amount +/// using the bonding curve formula with transaction fees deducted. +/// +/// # Arguments +/// * `virtual_token_reserves` - Virtual token reserves in the bonding curve +/// * `virtual_sol_reserves` - Virtual SOL reserves in the bonding curve +/// * `creator` - Creator's public key (affects fee calculation) +/// * `amount` - Token amount to sell (in token's smallest unit) +/// +/// # Returns +/// The amount of SOL that will be received after fees (in lamports) +pub fn get_sell_sol_amount_from_token_amount( + virtual_token_reserves: u128, + virtual_sol_reserves: u128, + creator: Pubkey, + amount: u64, +) -> u64 { + if amount == 0 { + return 0; + } + + // migrated bonding curve + if virtual_token_reserves == 0 { + return 0; + } + + let amount_128 = amount as u128; + + // Calculate SOL amount received from selling tokens using constant product formula + let numerator = amount_128.checked_mul(virtual_sol_reserves).unwrap_or(0); + let denominator = virtual_token_reserves.checked_add(amount_128).unwrap_or(1); + + let sol_cost = numerator.checked_div(denominator).unwrap_or(0); + + let total_fee_basis_points = + FEE_BASIS_POINTS + if creator != Pubkey::default() { CREATOR_FEE } else { 0 }; + let total_fee_basis_points_128 = total_fee_basis_points as u128; + + // Calculate transaction fee + let fee = compute_fee(sol_cost, total_fee_basis_points_128); + + sol_cost.saturating_sub(fee) as u64 +} diff --git a/src/utils/calc/pumpswap.rs b/src/utils/calc/pumpswap.rs new file mode 100644 index 0000000..1364819 --- /dev/null +++ b/src/utils/calc/pumpswap.rs @@ -0,0 +1,275 @@ +use super::common::{ + calculate_with_slippage_buy, calculate_with_slippage_sell, ceil_div, compute_fee, +}; +use crate::constants::pumpswap::accounts::{ + COIN_CREATOR_FEE_BASIS_POINTS, LP_FEE_BASIS_POINTS, PROTOCOL_FEE_BASIS_POINTS, +}; +use solana_sdk::pubkey::Pubkey; + +/// Result for buying base tokens with base amount input +#[derive(Clone, Debug)] +pub struct BuyBaseInputResult { + /// Raw quote amount needed before fees + pub internal_quote_amount: u64, + /// Total quote amount including all fees + pub ui_quote: u64, + /// Maximum quote amount with slippage protection + pub max_quote: u64, +} + +/// Result for buying base tokens with quote amount input +#[derive(Clone, Debug)] +pub struct BuyQuoteInputResult { + /// Amount of base tokens received + pub base: u64, + /// Effective quote amount after fee deduction + pub internal_quote_without_fees: u64, + /// Maximum quote amount with slippage protection + pub max_quote: u64, +} + +/// Result for selling base tokens with base amount input +#[derive(Clone, Debug)] +pub struct SellBaseInputResult { + /// Final quote amount received after fees + pub ui_quote: u64, + /// Minimum quote amount with slippage protection + pub min_quote: u64, + /// Raw quote amount before fee deduction + pub internal_quote_amount_out: u64, +} + +/// Result for selling base tokens with quote amount input +#[derive(Clone, Debug)] +pub struct SellQuoteInputResult { + /// Raw quote amount including fees + pub internal_raw_quote: u64, + /// Amount of base tokens needed to sell + pub base: u64, + /// Minimum quote amount with slippage protection + pub min_quote: u64, +} + +/// Calculate quote amount needed to buy a specific amount of base tokens +/// +/// # Arguments +/// * `base` - Amount of base tokens to buy +/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%) +/// * `base_reserve` - Base token reserves in the pool +/// * `quote_reserve` - Quote token reserves in the pool +/// * `coin_creator` - Token creator address +/// +/// # Returns +/// * `BuyBaseInputResult` containing quote amounts and slippage calculations +pub fn buy_base_input_internal( + base: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + coin_creator: &Pubkey, +) -> Result { + if base_reserve == 0 || quote_reserve == 0 { + return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); + } + if base > base_reserve { + return Err("Cannot buy more base tokens than the pool reserves.".to_string()); + } + + // Calculate required quote amount using constant product formula + let numerator = (quote_reserve as u128) * (base as u128); + let denominator = base_reserve - base; + + if denominator == 0 { + return Err("Pool would be depleted; denominator is zero.".to_string()); + } + + let quote_amount_in = ceil_div(numerator, denominator as u128) as u64; + + // Calculate fees + let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64; + let protocol_fee = + compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64; + let coin_creator_fee = if *coin_creator == Pubkey::default() { + 0 + } else { + compute_fee(quote_amount_in as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64 + }; + let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee; + + // Calculate max quote with slippage + let max_quote = calculate_with_slippage_buy(total_quote, slippage_basis_points); + + Ok(BuyBaseInputResult { + internal_quote_amount: quote_amount_in, + ui_quote: total_quote, + max_quote, + }) +} + +/// Calculate base tokens received for a specific quote amount +/// +/// # Arguments +/// * `quote` - Amount of quote tokens to spend +/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%) +/// * `base_reserve` - Base token reserves in the pool +/// * `quote_reserve` - Quote token reserves in the pool +/// * `coin_creator` - Token creator address +/// +/// # Returns +/// * `BuyQuoteInputResult` containing base amount and slippage calculations +pub fn buy_quote_input_internal( + quote: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + coin_creator: &Pubkey, +) -> Result { + if base_reserve == 0 || quote_reserve == 0 { + return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); + } + + // Calculate total fee basis points + let total_fee_bps = LP_FEE_BASIS_POINTS + + PROTOCOL_FEE_BASIS_POINTS + + if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS }; + let denominator = 10_000 + total_fee_bps; + + // Calculate effective quote amount after fees + let effective_quote = (quote as u128 * 10_000) / denominator as u128; + + // Calculate base amount out using constant product formula + let numerator = (base_reserve as u128) * effective_quote; + let denominator_effective = (quote_reserve as u128) + effective_quote; + + if denominator_effective == 0 { + return Err("Pool would be depleted; denominator is zero.".to_string()); + } + + let base_amount_out = (numerator / denominator_effective) as u64; + + // Calculate max quote with slippage + let max_quote = calculate_with_slippage_buy(quote, slippage_basis_points); + + Ok(BuyQuoteInputResult { + base: base_amount_out, + internal_quote_without_fees: effective_quote as u64, + max_quote, + }) +} + +/// Calculate quote tokens received for selling a specific amount of base tokens +/// +/// # Arguments +/// * `base` - Amount of base tokens to sell +/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%) +/// * `base_reserve` - Base token reserves in the pool +/// * `quote_reserve` - Quote token reserves in the pool +/// * `coin_creator` - Token creator address +/// +/// # Returns +/// * `SellBaseInputResult` containing quote amounts and slippage calculations +pub fn sell_base_input_internal( + base: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + coin_creator: &Pubkey, +) -> Result { + if base_reserve == 0 || quote_reserve == 0 { + return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); + } + + // Calculate quote amount out using constant product formula + let quote_amount_out = ((quote_reserve as u128) * (base as u128) + / ((base_reserve as u128) + (base as u128))) as u64; + + // Calculate fees + let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64; + let protocol_fee = + compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64; + let coin_creator_fee = if *coin_creator == Pubkey::default() { + 0 + } else { + compute_fee(quote_amount_out as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64 + }; + + // Calculate final quote after fees + let total_fees = lp_fee + protocol_fee + coin_creator_fee; + if total_fees > quote_amount_out { + return Err("Fees exceed total output; final quote is negative.".to_string()); + } + let final_quote = quote_amount_out - total_fees; + + // Calculate min quote with slippage + let min_quote = calculate_with_slippage_sell(final_quote, slippage_basis_points); + + Ok(SellBaseInputResult { + ui_quote: final_quote, + min_quote, + internal_quote_amount_out: quote_amount_out, + }) +} + +const MAX_FEE_BASIS_POINTS: u64 = 10_000; + +/// Calculate quote amount out including fees +fn calculate_quote_amount_out( + user_quote_amount_out: u64, + lp_fee_basis_points: u64, + protocol_fee_basis_points: u64, + coin_creator_fee_basis_points: u64, +) -> u64 { + let total_fee_basis_points = + lp_fee_basis_points + protocol_fee_basis_points + coin_creator_fee_basis_points; + let denominator = MAX_FEE_BASIS_POINTS - total_fee_basis_points; + ceil_div((user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128), denominator as u128) + as u64 +} + +/// Calculate base tokens needed to receive a specific amount of quote tokens +/// +/// # Arguments +/// * `quote` - Desired amount of quote tokens to receive +/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%) +/// * `base_reserve` - Base token reserves in the pool +/// * `quote_reserve` - Quote token reserves in the pool +/// * `coin_creator` - Token creator address +/// +/// # Returns +/// * `SellQuoteInputResult` containing base amount and slippage calculations +pub fn sell_quote_input_internal( + quote: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + coin_creator: &Pubkey, +) -> Result { + if base_reserve == 0 || quote_reserve == 0 { + return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); + } + if quote > quote_reserve { + return Err("Cannot receive more quote tokens than the pool quote reserves.".to_string()); + } + + // Calculate raw quote amount including fees + let raw_quote = calculate_quote_amount_out( + quote, + LP_FEE_BASIS_POINTS, + PROTOCOL_FEE_BASIS_POINTS, + if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS }, + ); + + // Calculate base amount needed using inverse constant product formula + if raw_quote >= quote_reserve { + return Err("Invalid input: Desired quote amount exceeds available reserve.".to_string()); + } + + let base_amount_in = + ceil_div((base_reserve as u128) * (raw_quote as u128), (quote_reserve - raw_quote) as u128) + as u64; + + // Calculate min quote with slippage + let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points); + + Ok(SellQuoteInputResult { internal_raw_quote: raw_quote, base: base_amount_in, min_quote }) +} diff --git a/src/utils/calc/raydium_amm_v4.rs b/src/utils/calc/raydium_amm_v4.rs new file mode 100644 index 0000000..70b786d --- /dev/null +++ b/src/utils/calc/raydium_amm_v4.rs @@ -0,0 +1 @@ +// TODO diff --git a/src/utils/calc/raydium_cpmm.rs b/src/utils/calc/raydium_cpmm.rs new file mode 100644 index 0000000..2b85c67 --- /dev/null +++ b/src/utils/calc/raydium_cpmm.rs @@ -0,0 +1,190 @@ +use crate::constants::raydium_cpmm::accounts::{ + CREATOR_FEE_RATE, FEE_RATE_DENOMINATOR_VALUE, FUND_FEE_RATE, PROTOCOL_FEE_RATE, TRADE_FEE_RATE, +}; + +/// Computes trading fee using ceiling division. +/// +/// # Arguments +/// * `amount` - The amount to calculate fee for +/// * `fee_rate` - The fee rate to apply +/// +/// # Returns +/// The calculated trading fee +fn compute_trading_fee(amount: u64, fee_rate: u64) -> u64 { + let numerator = (amount as u128) * (fee_rate as u128); + ((numerator + FEE_RATE_DENOMINATOR_VALUE - 1) / FEE_RATE_DENOMINATOR_VALUE) as u64 +} + +/// Computes protocol or fund fee using floor division. +/// +/// # Arguments +/// * `amount` - The amount to calculate fee for +/// * `fee_rate` - The fee rate to apply +/// +/// # Returns +/// The calculated protocol or fund fee +fn compute_protocol_fund_fee(amount: u64, fee_rate: u64) -> u64 { + let numerator = (amount as u128) * (fee_rate as u128); + (numerator / FEE_RATE_DENOMINATOR_VALUE) as u64 +} + +/// Computes creator fee using ceiling division. +/// +/// # Arguments +/// * `amount` - The amount to calculate fee for +/// * `fee_rate` - The fee rate to apply +/// +/// # Returns +/// The calculated creator fee +fn compute_creator_fee_new(amount: u64, fee_rate: u64) -> u64 { + let numerator = (amount as u128) * (fee_rate as u128); + ((numerator + FEE_RATE_DENOMINATOR_VALUE - 1) / FEE_RATE_DENOMINATOR_VALUE) as u64 +} + +/// Parameters for computing swap amounts and fees. +#[derive(Debug, Clone)] +pub struct ComputeSwapParams { + /// Whether the entire input amount is traded + pub all_trade: bool, + /// The input amount for the swap + pub amount_in: u64, + /// The expected output amount from the swap + pub amount_out: u64, + /// The minimum acceptable output amount (considering slippage_basis_points) + pub min_amount_out: u64, + /// The trading fee amount + pub fee: u64, +} + +/// Result of a swap calculation containing all relevant amounts and fees. +#[derive(Debug, Clone)] +pub struct SwapResult { + /// The new amount in the input vault after the swap + pub new_input_vault_amount: u64, + /// The new amount in the output vault after the swap + pub new_output_vault_amount: u64, + /// The actual input amount used in the swap + pub input_amount: u64, + /// The actual output amount received from the swap + pub output_amount: u64, + /// The trading fee charged + pub trade_fee: u64, + /// The protocol fee charged + pub protocol_fee: u64, + /// The fund fee charged + pub fund_fee: u64, + /// The creator fee charged + pub creator_fee: u64, +} + +/// Performs a swap calculation based on input amount. +/// +/// Calculates the output amount and all associated fees when swapping a specific input amount. +/// +/// # Arguments +/// * `input_amount` - The amount of input tokens to swap +/// * `input_vault_amount` - Current amount in the input token vault +/// * `output_vault_amount` - Current amount in the output token vault +/// * `trade_fee_rate` - The trading fee rate +/// * `creator_fee_rate` - The creator fee rate +/// * `protocol_fee_rate` - The protocol fee rate +/// * `fund_fee_rate` - The fund fee rate +/// * `is_creator_fee_on_input` - Whether creator fee is charged on input tokens +/// +/// # Returns +/// A `SwapResult` containing all swap calculations and fees +fn swap_base_input( + input_amount: u64, + input_vault_amount: u64, + output_vault_amount: u64, + trade_fee_rate: u64, + creator_fee_rate: u64, + protocol_fee_rate: u64, + fund_fee_rate: u64, + is_creator_fee_on_input: bool, +) -> SwapResult { + let mut creator_fee = 0u64; + + let trade_fee = compute_trading_fee(input_amount, trade_fee_rate); + + let input_amount_less_fees = if is_creator_fee_on_input { + creator_fee = compute_creator_fee_new(input_amount, creator_fee_rate); + input_amount.saturating_sub(trade_fee).saturating_sub(creator_fee) + } else { + input_amount.saturating_sub(trade_fee) + }; + + let protocol_fee = compute_protocol_fund_fee(trade_fee, protocol_fee_rate); + let fund_fee = compute_protocol_fund_fee(trade_fee, fund_fee_rate); + + let output_amount_swapped = ((output_vault_amount as u128) + .saturating_mul(input_amount_less_fees as u128) + / (input_vault_amount as u128).saturating_add(input_amount_less_fees as u128)) + as u64; + + let output_amount = if is_creator_fee_on_input { + output_amount_swapped + } else { + creator_fee = compute_creator_fee_new(output_amount_swapped, creator_fee_rate); + output_amount_swapped.saturating_sub(creator_fee) + }; + + SwapResult { + new_input_vault_amount: input_vault_amount.saturating_add(input_amount_less_fees), + new_output_vault_amount: output_vault_amount.saturating_sub(output_amount_swapped), + input_amount, + output_amount, + trade_fee, + protocol_fee, + fund_fee, + creator_fee, + } +} + +/// Computes swap parameters including amounts, fees, and slippage protection. +/// +/// This function calculates the expected output amount, minimum output amount (with slippage), +/// and trading fees for a given input amount in a CPMM (Constant Product Market Maker) pool. +/// +/// # Arguments +/// * `base_reserve` - The current reserve amount of the base token in the pool +/// * `quote_reserve` - The current reserve amount of the quote token in the pool +/// * `is_base_in` - Whether the input token is the base token (true) or quote token (false) +/// * `amount_in` - The amount of input tokens to swap +/// * `slippage_basis_points` - The acceptable slippage in basis points (e.g., 100 for 1%) +/// +/// # Returns +/// A `ComputeSwapParams` struct containing all computed swap parameters +pub fn compute_swap_amount( + base_reserve: u64, + quote_reserve: u64, + is_base_in: bool, + amount_in: u64, + slippage_basis_points: u64, +) -> ComputeSwapParams { + let (input_reserve, output_reserve) = + if is_base_in { (base_reserve, quote_reserve) } else { (quote_reserve, base_reserve) }; + + let swap_result = swap_base_input( + amount_in, + input_reserve, + output_reserve, + TRADE_FEE_RATE, + CREATOR_FEE_RATE, + PROTOCOL_FEE_RATE, + FUND_FEE_RATE, + true, + ); + + let min_amount_out = ((swap_result.output_amount as f64) * (1.0 - (slippage_basis_points as f64) / 10000.0)) as u64; + + let all_trade = swap_result.input_amount == amount_in; + + ComputeSwapParams { + all_trade, + amount_in, + amount_out: swap_result.output_amount, + min_amount_out, + fee: swap_result.trade_fee, + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d3769f7..6f8c671 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ pub mod price; +pub mod calc; use crate::solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent; use crate::trading;