From 47cef59d15ee3f0506e88e6a3a2f1b4532a51310 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Tue, 30 Jun 2026 21:40:44 +0800 Subject: [PATCH] fix: align PumpSwap dynamic fee parameters --- Cargo.toml | 2 +- examples/pumpswap_trading/src/main.rs | 23 +- src/instruction/pumpswap.rs | 62 +++-- src/instruction/utils/pumpswap.rs | 313 +++++++++++++++++++++++++- src/trading/core/params/pumpswap.rs | 139 +++++++++++- src/utils/calc/pumpswap.rs | 136 +++++++++-- 6 files changed, 620 insertions(+), 55 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fd020d0..3bb3734 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.21" +version = "4.0.22" edition = "2021" authors = [ "William ", diff --git a/examples/pumpswap_trading/src/main.rs b/examples/pumpswap_trading/src/main.rs index 3eb7d6a..039f996 100644 --- a/examples/pumpswap_trading/src/main.rs +++ b/examples/pumpswap_trading/src/main.rs @@ -1,6 +1,5 @@ use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed; use sol_trade_sdk::common::TradeConfig; -use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool; use sol_trade_sdk::TradeTokenType; use sol_trade_sdk::{ common::AnyResult, @@ -148,8 +147,7 @@ async fn create_solana_trade_client() -> AnyResult { async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> AnyResult<()> { let client = create_solana_trade_client().await?; - let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?; - let params = PumpSwapParams::from_trade( + let params = PumpSwapParams::from_trade_with_fee_basis_points( trade_info.pool, trade_info.base_mint, trade_info.quote_mint, @@ -162,9 +160,13 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any trade_info.base_token_program, trade_info.quote_token_program, trade_info.protocol_fee_recipient, - pool_data.coin_creator, - pool_data.is_cashback_coin, + Pubkey::default(), + trade_info.coin_creator, + false, 0, + trade_info.lp_fee_basis_points, + trade_info.protocol_fee_basis_points, + trade_info.coin_creator_fee_basis_points, ); let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT @@ -179,8 +181,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> { let client = create_solana_trade_client().await?; - let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?; - let params = PumpSwapParams::from_trade( + let params = PumpSwapParams::from_trade_with_fee_basis_points( trade_info.pool, trade_info.base_mint, trade_info.quote_mint, @@ -193,9 +194,13 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A trade_info.base_token_program, trade_info.quote_token_program, trade_info.protocol_fee_recipient, - pool_data.coin_creator, - pool_data.is_cashback_coin, + Pubkey::default(), + trade_info.coin_creator, + false, 0, + trade_info.lp_fee_basis_points, + trade_info.protocol_fee_basis_points, + trade_info.coin_creator_fee_basis_points, ); let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index 3d7c18f..a72b816 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -20,7 +20,9 @@ use crate::{ params::{PumpSwapParams, SwapParams}, traits::InstructionBuilder, }, - utils::calc::pumpswap::{buy_quote_input_internal, sell_base_input_internal}, + utils::calc::pumpswap::{ + buy_quote_input_internal_with_fees, sell_base_input_internal_with_fees, + }, }; use anyhow::{anyhow, Result}; use solana_sdk::{ @@ -85,34 +87,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { let output_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint }; let output_trade_token_program = if quote_is_wsol_or_usdc { base_token_program } else { quote_token_program }; - let mut creator = Pubkey::default(); - if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY { - creator = params_coin_creator_vault_authority; - } - let cashback_fee_bps = protocol_params.cashback_fee_basis_points; + let fee_basis_points = protocol_params.fee_basis_points; let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount { (output_amount, params.input_amount.unwrap_or(0)) } else if quote_is_wsol_or_usdc { - let result = buy_quote_input_internal( + let result = buy_quote_input_internal_with_fees( params.input_amount.unwrap_or(0), params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), pool_base_token_reserves, pool_quote_token_reserves, - &creator, - cashback_fee_bps, + &fee_basis_points, ) .unwrap(); // base_amount_out, max_quote_amount_in (result.base, result.max_quote) } else { - let result = sell_base_input_internal( + let result = sell_base_input_internal_with_fees( params.input_amount.unwrap_or(0), params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), pool_base_token_reserves, pool_quote_token_reserves, - &creator, - cashback_fee_bps, + &fee_basis_points, ) .unwrap(); // min_quote_amount_out, base_amount_in @@ -321,34 +317,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { let output_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint }; let output_stable_token_program = if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program }; - let mut creator = Pubkey::default(); - if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY { - creator = params_coin_creator_vault_authority; - } - let cashback_fee_bps = protocol_params.cashback_fee_basis_points; + let fee_basis_points = protocol_params.fee_basis_points; let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount { (params.input_amount.unwrap(), output_amount) } else if quote_is_wsol_or_usdc { - let result = sell_base_input_internal( + let result = sell_base_input_internal_with_fees( params.input_amount.unwrap(), params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), pool_base_token_reserves, pool_quote_token_reserves, - &creator, - cashback_fee_bps, + &fee_basis_points, ) .unwrap(); // base_amount_in, min_quote_amount_out (params.input_amount.unwrap(), result.min_quote) } else { - let result = buy_quote_input_internal( + let result = buy_quote_input_internal_with_fees( params.input_amount.unwrap(), params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), pool_base_token_reserves, pool_quote_token_reserves, - &creator, - cashback_fee_bps, + &fee_basis_points, ) .unwrap(); // max_quote_amount_in, base_amount_out @@ -657,4 +647,30 @@ mod tests { assert_eq!(create_ix.program_id, crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID); assert_eq!(create_ix.accounts[3].pubkey, crate::constants::USDC_TOKEN_ACCOUNT); } + + #[tokio::test] + async fn pumpswap_buy_uses_fee_basis_points_from_params_without_rpc() { + let mut params = swap_params(TradeType::Buy, None); + params.input_amount = Some(1_000_000); + params.use_exact_sol_amount = Some(false); + params.protocol_params = + DexParamEnum::PumpSwap(pumpswap_params().with_fee_basis_points(20, 5, 75)); + + let instructions = + PumpSwapInstructionBuilder.build_buy_instructions(¶ms).await.unwrap(); + let ix = instructions.last().unwrap(); + + assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR); + let base_amount_out = u64::from_le_bytes(ix.data[8..16].try_into().unwrap()); + + let expected = crate::utils::calc::pumpswap::buy_quote_input_internal_with_fees( + 1_000_000, + 100, + 1_000_000_000, + 2_000_000_000, + &crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0), + ) + .unwrap(); + assert_eq!(base_amount_out, expected.base); + } } diff --git a/src/instruction/utils/pumpswap.rs b/src/instruction/utils/pumpswap.rs index 8a5c7a1..57e60ea 100644 --- a/src/instruction/utils/pumpswap.rs +++ b/src/instruction/utils/pumpswap.rs @@ -179,21 +179,76 @@ pub mod accounts { }; } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PumpSwapFeeBasisPoints { + pub lp_fee_basis_points: u64, + pub protocol_fee_basis_points: u64, + pub coin_creator_fee_basis_points: u64, +} + +impl PumpSwapFeeBasisPoints { + #[inline] + pub const fn new( + lp_fee_basis_points: u64, + protocol_fee_basis_points: u64, + coin_creator_fee_basis_points: u64, + ) -> Self { + Self { lp_fee_basis_points, protocol_fee_basis_points, coin_creator_fee_basis_points } + } + + #[inline] + pub const fn legacy_default() -> Self { + Self::new( + accounts::LP_FEE_BASIS_POINTS, + accounts::PROTOCOL_FEE_BASIS_POINTS, + accounts::COIN_CREATOR_FEE_BASIS_POINTS, + ) + } +} + +impl Default for PumpSwapFeeBasisPoints { + #[inline] + fn default() -> Self { + Self::legacy_default() + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PumpSwapFeeTier { + pub market_cap_lamports_threshold: u128, + pub fees: PumpSwapFeeBasisPoints, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PumpSwapFeeConfig { + pub flat_fees: PumpSwapFeeBasisPoints, + pub fee_tiers: Vec, + pub stable_fee_tiers: Vec, +} + pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234]; pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112]; pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173]; const PUMPSWAP_GLOBAL_CONFIG_TTL: Duration = Duration::from_secs(90); const PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT: Duration = Duration::from_millis(180); +const PUMPSWAP_FEE_CONFIG_TTL: Duration = Duration::from_secs(300); +const PUMPSWAP_FEE_CONFIG_RPC_TIMEOUT: Duration = Duration::from_millis(180); const PUBKEY_LEN: usize = 32; const U64_LEN: usize = 8; const U8_LEN: usize = 1; const BOOL_LEN: usize = 1; const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8; +const FEE_CONFIG_DISCRIMINATOR_LEN: usize = 8; +const FEE_CONFIG_BUMP_LEN: usize = 1; +const FEE_TIER_LEN: usize = 16 + U64_LEN * 3; #[derive(Clone, Debug)] pub struct GlobalConfig { + pub lp_fee_basis_points: u64, + pub protocol_fee_basis_points: u64, + pub coin_creator_fee_basis_points: u64, pub protocol_fee_recipients: [Pubkey; 8], pub reserved_fee_recipient: Pubkey, pub reserved_fee_recipients: [Pubkey; 7], @@ -206,9 +261,16 @@ struct CachedGlobalConfig { config: GlobalConfig, } +#[derive(Clone)] +struct CachedFeeConfig { + fetched_at: Instant, + config: PumpSwapFeeConfig, +} + static GLOBAL_CONFIG_CACHE: Lazy>> = Lazy::new(|| RwLock::new(None)); static GLOBAL_CONFIG_REFRESH_IN_FLIGHT: AtomicBool = AtomicBool::new(false); +static FEE_CONFIG_CACHE: Lazy>> = Lazy::new(|| RwLock::new(None)); fn read_pubkey(data: &[u8], offset: usize) -> Option { let bytes = data.get(offset..offset + PUBKEY_LEN)?; @@ -223,15 +285,34 @@ fn read_pubkey_array(data: &[u8], offset: usize) -> Option<[Pubk Some(keys) } +fn read_u64(data: &[u8], offset: usize) -> Option { + let bytes = data.get(offset..offset + U64_LEN)?; + Some(u64::from_le_bytes(bytes.try_into().ok()?)) +} + +fn read_u128(data: &[u8], offset: usize) -> Option { + let bytes = data.get(offset..offset + 16)?; + Some(u128::from_le_bytes(bytes.try_into().ok()?)) +} + +fn read_u32(data: &[u8], offset: usize) -> Option { + let bytes = data.get(offset..offset + 4)?; + Some(u32::from_le_bytes(bytes.try_into().ok()?)) +} + fn decode_global_config(data: &[u8]) -> Option { let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN; offset += PUBKEY_LEN; // admin - offset += U64_LEN * 2; // lp_fee_basis_points + protocol_fee_basis_points + let lp_fee_basis_points = read_u64(data, offset)?; + offset += U64_LEN; + let protocol_fee_basis_points = read_u64(data, offset)?; + offset += U64_LEN; offset += U8_LEN; // disable_flags let protocol_fee_recipients = read_pubkey_array::<8>(data, offset)?; offset += PUBKEY_LEN * 8; - offset += U64_LEN; // coin_creator_fee_basis_points + let coin_creator_fee_basis_points = read_u64(data, offset)?; + offset += U64_LEN; offset += PUBKEY_LEN; // admin_set_coin_creator_authority offset += PUBKEY_LEN; // whitelist_pda @@ -246,6 +327,9 @@ fn decode_global_config(data: &[u8]) -> Option { let buyback_fee_recipients = read_pubkey_array::<8>(data, offset)?; Some(GlobalConfig { + lp_fee_basis_points, + protocol_fee_basis_points, + coin_creator_fee_basis_points, protocol_fee_recipients, reserved_fee_recipient, reserved_fee_recipients, @@ -253,6 +337,46 @@ fn decode_global_config(data: &[u8]) -> Option { }) } +fn decode_fees(data: &[u8], offset: usize) -> Option { + Some(PumpSwapFeeBasisPoints::new( + read_u64(data, offset)?, + read_u64(data, offset + U64_LEN)?, + read_u64(data, offset + U64_LEN * 2)?, + )) +} + +fn decode_fee_tiers(data: &[u8], offset: &mut usize) -> Option> { + let len = read_u32(data, *offset)? as usize; + *offset += 4; + let byte_len = len.checked_mul(FEE_TIER_LEN)?; + let end = (*offset).checked_add(byte_len)?; + data.get(*offset..end)?; + + let mut tiers = Vec::with_capacity(len); + for _ in 0..len { + let market_cap_lamports_threshold = read_u128(data, *offset)?; + *offset += 16; + let fees = decode_fees(data, *offset)?; + *offset += U64_LEN * 3; + tiers.push(PumpSwapFeeTier { market_cap_lamports_threshold, fees }); + } + Some(tiers) +} + +pub fn decode_fee_config(data: &[u8]) -> Option { + let mut offset = FEE_CONFIG_DISCRIMINATOR_LEN; + offset += FEE_CONFIG_BUMP_LEN; + offset += PUBKEY_LEN; // admin + + let flat_fees = decode_fees(data, offset)?; + offset += U64_LEN * 3; + + let fee_tiers = decode_fee_tiers(data, &mut offset)?; + let stable_fee_tiers = decode_fee_tiers(data, &mut offset)?; + + Some(PumpSwapFeeConfig { flat_fees, fee_tiers, stable_fee_tiers }) +} + async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option { let account = match tokio::time::timeout( PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT, @@ -289,6 +413,42 @@ async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option Option { + let account = match tokio::time::timeout( + PUMPSWAP_FEE_CONFIG_RPC_TIMEOUT, + rpc.get_account(&accounts::FEE_CONFIG), + ) + .await + { + Ok(Ok(account)) => account, + Ok(Err(e)) => { + warn!(target: "pumpswap_fee_config", "PumpSwap FeeConfig 读取失败: {}", e); + return None; + } + Err(_) => { + warn!( + target: "pumpswap_fee_config", + timeout_ms = PUMPSWAP_FEE_CONFIG_RPC_TIMEOUT.as_millis(), + "PumpSwap FeeConfig 读取超时" + ); + return None; + } + }; + + let Some(config) = decode_fee_config(&account.data) else { + warn!( + target: "pumpswap_fee_config", + data_len = account.data.len(), + "PumpSwap FeeConfig 解析失败" + ); + return None; + }; + + *FEE_CONFIG_CACHE.write() = + Some(CachedFeeConfig { fetched_at: Instant::now(), config: config.clone() }); + Some(config) +} + pub async fn warm_pumpswap_global_config(rpc: Option<&Arc>) { let Some(rpc) = rpc else { return; @@ -302,6 +462,7 @@ pub async fn warm_pumpswap_global_config(rpc: Option<&Arc>) { let rpc = Arc::clone(rpc); tokio::spawn(async move { let _ = refresh_global_config_once(rpc.as_ref()).await; + let _ = refresh_fee_config_once(rpc.as_ref()).await; GLOBAL_CONFIG_REFRESH_IN_FLIGHT.store(false, Ordering::Release); }); } @@ -313,6 +474,93 @@ fn cached_global_config() -> Option { (cached.fetched_at.elapsed() <= PUMPSWAP_GLOBAL_CONFIG_TTL).then(|| cached.config.clone()) } +fn cached_fee_config() -> Option { + let guard = FEE_CONFIG_CACHE.read(); + let cached = guard.as_ref()?; + (cached.fetched_at.elapsed() <= PUMPSWAP_FEE_CONFIG_TTL).then(|| cached.config.clone()) +} + +pub async fn fetch_fee_config(rpc: &SolanaRpcClient) -> Option { + if let Some(config) = cached_fee_config() { + return Some(config); + } + refresh_fee_config_once(rpc).await +} + +#[inline] +pub fn global_fee_basis_points() -> PumpSwapFeeBasisPoints { + cached_global_config() + .map(|config| { + PumpSwapFeeBasisPoints::new( + config.lp_fee_basis_points, + config.protocol_fee_basis_points, + config.coin_creator_fee_basis_points, + ) + }) + .unwrap_or_default() +} + +#[inline] +pub fn is_canonical_pump_pool(base_mint: &Pubkey, pool_creator: &Pubkey) -> bool { + get_pump_pool_authority_pda(base_mint) == *pool_creator +} + +#[inline] +pub fn pool_market_cap_lamports( + base_mint_supply: u64, + base_reserve: u64, + quote_reserve: u64, +) -> Option { + if base_reserve == 0 { + return None; + } + Some((quote_reserve as u128) * (base_mint_supply as u128) / (base_reserve as u128)) +} + +pub fn calculate_fee_tier( + fee_tiers: &[PumpSwapFeeTier], + market_cap_lamports: u128, +) -> Option { + let first = fee_tiers.first()?; + if market_cap_lamports < first.market_cap_lamports_threshold { + return Some(first.fees); + } + fee_tiers + .iter() + .rev() + .find(|tier| market_cap_lamports >= tier.market_cap_lamports_threshold) + .map(|tier| tier.fees) + .or(Some(first.fees)) +} + +pub fn compute_fee_basis_points( + fee_config: Option<&PumpSwapFeeConfig>, + pool_creator: Pubkey, + base_mint: Pubkey, + base_mint_supply: Option, + base_reserve: u64, + quote_reserve: u64, +) -> PumpSwapFeeBasisPoints { + let Some(fee_config) = fee_config else { + return global_fee_basis_points(); + }; + + if !is_canonical_pump_pool(&base_mint, &pool_creator) { + return fee_config.flat_fees; + } + + let Some(base_mint_supply) = base_mint_supply else { + return global_fee_basis_points(); + }; + let Some(market_cap_lamports) = + pool_market_cap_lamports(base_mint_supply, base_reserve, quote_reserve) + else { + return global_fee_basis_points(); + }; + + calculate_fee_tier(&fee_config.fee_tiers, market_cap_lamports).unwrap_or(fee_config.flat_fees) +} + fn choose_nonzero(keys: &[Pubkey]) -> Option { let mut valid = [Pubkey::default(); 8]; let mut len = 0; @@ -656,6 +904,31 @@ mod tests { use super::*; use solana_sdk::pubkey::Pubkey; + fn fee_config_fixture() -> PumpSwapFeeConfig { + PumpSwapFeeConfig { + flat_fees: PumpSwapFeeBasisPoints::new(25, 5, 0), + fee_tiers: vec![ + PumpSwapFeeTier { + market_cap_lamports_threshold: 0, + fees: PumpSwapFeeBasisPoints::new(2, 93, 30), + }, + PumpSwapFeeTier { + market_cap_lamports_threshold: 420_000_000_000, + fees: PumpSwapFeeBasisPoints::new(20, 5, 95), + }, + PumpSwapFeeTier { + market_cap_lamports_threshold: 4_420_000_000_000, + fees: PumpSwapFeeBasisPoints::new(20, 5, 75), + }, + PumpSwapFeeTier { + market_cap_lamports_threshold: 9_820_000_000_000, + fees: PumpSwapFeeBasisPoints::new(20, 5, 70), + }, + ], + stable_fee_tiers: Vec::new(), + } + } + #[test] fn pumpswap_user_volume_accumulator_pda_deterministic() { let user = Pubkey::new_unique(); @@ -677,4 +950,40 @@ mod tests { let b = get_pool_v2_pda(&base_mint).unwrap(); assert_eq!(a, b); } + + #[test] + fn pumpswap_fee_tier_selects_issue_106_fee_bucket() { + let selected = calculate_fee_tier(&fee_config_fixture().fee_tiers, 4_500_000_000_000); + assert_eq!(selected, Some(PumpSwapFeeBasisPoints::new(20, 5, 75))); + } + + #[test] + fn pumpswap_compute_fees_uses_flat_fee_for_non_canonical_pool() { + let base_mint = Pubkey::new_unique(); + let non_canonical_creator = Pubkey::new_unique(); + let fees = compute_fee_basis_points( + Some(&fee_config_fixture()), + non_canonical_creator, + base_mint, + Some(1_000_000_000_000_000), + 1_000_000_000_000_000, + 4_500_000_000_000, + ); + assert_eq!(fees, PumpSwapFeeBasisPoints::new(25, 5, 0)); + } + + #[test] + fn pumpswap_compute_fees_uses_tier_for_canonical_pool() { + let base_mint = Pubkey::new_unique(); + let canonical_creator = get_pump_pool_authority_pda(&base_mint); + let fees = compute_fee_basis_points( + Some(&fee_config_fixture()), + canonical_creator, + base_mint, + Some(1_000_000_000_000_000), + 1_000_000_000_000_000, + 4_500_000_000_000, + ); + assert_eq!(fees, PumpSwapFeeBasisPoints::new(20, 5, 75)); + } } diff --git a/src/trading/core/params/pumpswap.rs b/src/trading/core/params/pumpswap.rs index 11d75ce..ef19ff6 100644 --- a/src/trading/core/params/pumpswap.rs +++ b/src/trading/core/params/pumpswap.rs @@ -1,8 +1,13 @@ use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id; use crate::common::SolanaRpcClient; -use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP; +use crate::instruction::utils::pumpswap::{ + accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP, PumpSwapFeeBasisPoints, +}; use solana_sdk::pubkey::Pubkey; +const SPL_MINT_SUPPLY_OFFSET: usize = 36; +const SPL_MINT_SUPPLY_LEN: usize = 8; + /// PumpSwap Protocol Specific Parameters /// /// Parameters for configuring PumpSwap trading protocol, including liquidity pool information, @@ -39,6 +44,9 @@ pub struct PumpSwapParams { pub quote_token_program: Pubkey, /// Whether the pool is in mayhem mode pub is_mayhem_mode: bool, + /// Pool creator. Canonical PumpSwap pools use the Pump program pool-authority PDA here; + /// fee tiers are selected from this value without doing RPC in the instruction builder. + pub pool_creator: Pubkey, /// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap /// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()` /// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053). @@ -50,6 +58,12 @@ pub struct PumpSwapParams { /// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket. /// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps). pub cashback_fee_basis_points: u64, + /// Base mint supply used by PumpSwap fee-tier market-cap selection. Filled by RPC + /// constructors and optional for parser/event fast paths. + pub base_mint_supply: Option, + /// Effective PumpSwap fee bps for this pool snapshot. Instruction building reads this + /// only from params, so hot-path trading never adds an RPC call for fee discovery. + pub fee_basis_points: PumpSwapFeeBasisPoints, } impl PumpSwapParams { @@ -71,6 +85,12 @@ impl PumpSwapParams { cashback_fee_basis_points: u64, ) -> Self { let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP; + let creator_fee_basis_points = if coin_creator == Pubkey::default() { + 0 + } else { + crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS + } + .saturating_add(cashback_fee_basis_points); Self { pool, base_mint, @@ -84,12 +104,46 @@ impl PumpSwapParams { base_token_program, quote_token_program, is_mayhem_mode, + pool_creator: Pubkey::default(), coin_creator, is_cashback_coin, cashback_fee_basis_points, + base_mint_supply: None, + fee_basis_points: PumpSwapFeeBasisPoints::new( + crate::instruction::utils::pumpswap::accounts::LP_FEE_BASIS_POINTS, + crate::instruction::utils::pumpswap::accounts::PROTOCOL_FEE_BASIS_POINTS, + creator_fee_basis_points, + ), } } + pub fn with_pool_creator(mut self, pool_creator: Pubkey) -> Self { + self.pool_creator = pool_creator; + self + } + + pub fn with_base_mint_supply(mut self, base_mint_supply: u64) -> Self { + self.base_mint_supply = Some(base_mint_supply); + self + } + + pub fn with_fee_basis_points( + mut self, + lp_fee_basis_points: u64, + protocol_fee_basis_points: u64, + coin_creator_fee_basis_points: u64, + ) -> Self { + let creator_fee_basis_points = + if self.coin_creator == Pubkey::default() { 0 } else { coin_creator_fee_basis_points } + .saturating_add(self.cashback_fee_basis_points); + self.fee_basis_points = PumpSwapFeeBasisPoints::new( + lp_fee_basis_points, + protocol_fee_basis_points, + creator_fee_basis_points, + ); + self + } + /// Fast-path constructor for building PumpSwap parameters directly from decoded /// trade/event data and the accompanying instruction accounts, avoiding RPC /// lookups and associated latency. Token program IDs should be sourced from @@ -135,6 +189,57 @@ impl PumpSwapParams { ) } + /// Fast-path constructor for parser/event feeds that already include fee bps. + /// + /// This avoids any fee-discovery RPC and is the preferred path when sol-parser-sdk or + /// another stream parser provides `lp_fee_basis_points`, `protocol_fee_basis_points`, and + /// `coin_creator_fee_basis_points` from PumpSwap events. + pub fn from_trade_with_fee_basis_points( + pool: Pubkey, + base_mint: Pubkey, + quote_mint: Pubkey, + pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, + pool_base_token_reserves: u64, + pool_quote_token_reserves: u64, + coin_creator_vault_ata: Pubkey, + coin_creator_vault_authority: Pubkey, + base_token_program: Pubkey, + quote_token_program: Pubkey, + fee_recipient: Pubkey, + pool_creator: Pubkey, + coin_creator: Pubkey, + is_cashback_coin: bool, + cashback_fee_basis_points: u64, + lp_fee_basis_points: u64, + protocol_fee_basis_points: u64, + coin_creator_fee_basis_points: u64, + ) -> Self { + Self::new( + pool, + base_mint, + quote_mint, + pool_base_token_account, + pool_quote_token_account, + pool_base_token_reserves, + pool_quote_token_reserves, + coin_creator_vault_ata, + coin_creator_vault_authority, + base_token_program, + quote_token_program, + fee_recipient, + coin_creator, + is_cashback_coin, + cashback_fee_basis_points, + ) + .with_pool_creator(pool_creator) + .with_fee_basis_points( + lp_fee_basis_points, + protocol_fee_basis_points, + coin_creator_fee_basis_points, + ) + } + pub async fn from_mint_by_rpc( rpc: &SolanaRpcClient, mint: &Pubkey, @@ -172,6 +277,21 @@ impl PumpSwapParams { ) -> Result { let (pool_base_token_reserves, pool_quote_token_reserves) = crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?; + let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok(); + let fee_config = crate::instruction::utils::pumpswap::fetch_fee_config(rpc).await; + let raw_fee_basis_points = crate::instruction::utils::pumpswap::compute_fee_basis_points( + fee_config.as_ref(), + pool_data.creator, + pool_data.base_mint, + base_mint_supply, + pool_base_token_reserves, + pool_quote_token_reserves, + ); + let creator_fee_basis_points = if pool_data.coin_creator == Pubkey::default() { + 0 + } else { + raw_fee_basis_points.coin_creator_fee_basis_points + }; let creator = pool_data.coin_creator; let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata( creator, @@ -213,8 +333,25 @@ impl PumpSwapParams { crate::constants::TOKEN_PROGRAM_2022 }, is_mayhem_mode: pool_data.is_mayhem_mode, + pool_creator: pool_data.creator, coin_creator: pool_data.coin_creator, cashback_fee_basis_points: 0, + base_mint_supply, + fee_basis_points: PumpSwapFeeBasisPoints::new( + raw_fee_basis_points.lp_fee_basis_points, + raw_fee_basis_points.protocol_fee_basis_points, + creator_fee_basis_points, + ), }) } } + +fn decode_mint_supply(data: &[u8]) -> Option { + let bytes = data.get(SPL_MINT_SUPPLY_OFFSET..SPL_MINT_SUPPLY_OFFSET + SPL_MINT_SUPPLY_LEN)?; + Some(u64::from_le_bytes(bytes.try_into().ok()?)) +} + +async fn fetch_mint_supply(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result { + let account = rpc.get_account(mint).await?; + decode_mint_supply(&account.data).ok_or_else(|| anyhow::anyhow!("Failed to decode mint supply")) +} diff --git a/src/utils/calc/pumpswap.rs b/src/utils/calc/pumpswap.rs index c3fef79..82ded0b 100644 --- a/src/utils/calc/pumpswap.rs +++ b/src/utils/calc/pumpswap.rs @@ -4,6 +4,7 @@ use super::common::{ use crate::instruction::utils::pumpswap::accounts::{ COIN_CREATOR_FEE_BASIS_POINTS, LP_FEE_BASIS_POINTS, PROTOCOL_FEE_BASIS_POINTS, }; +use crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints; use solana_sdk::pubkey::Pubkey; /// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional @@ -81,6 +82,26 @@ pub fn buy_base_input_internal( quote_reserve: u64, coin_creator: &Pubkey, cashback_fee_basis_points: u64, +) -> Result { + buy_base_input_internal_with_fees( + base, + slippage_basis_points, + base_reserve, + quote_reserve, + &PumpSwapFeeBasisPoints::new( + LP_FEE_BASIS_POINTS, + PROTOCOL_FEE_BASIS_POINTS, + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points), + ), + ) +} + +pub fn buy_base_input_internal_with_fees( + base: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + fee_basis_points: &PumpSwapFeeBasisPoints, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -100,12 +121,15 @@ pub fn buy_base_input_internal( 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 lp_fee = + compute_fee(quote_amount_in as u128, fee_basis_points.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 creator_bps = - creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128; - let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64; + compute_fee(quote_amount_in as u128, fee_basis_points.protocol_fee_basis_points as u128) + as u64; + let coin_creator_fee = compute_fee( + quote_amount_in as u128, + fee_basis_points.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 @@ -137,23 +161,54 @@ pub fn buy_quote_input_internal( quote_reserve: u64, coin_creator: &Pubkey, cashback_fee_basis_points: u64, +) -> Result { + buy_quote_input_internal_with_fees( + quote, + slippage_basis_points, + base_reserve, + quote_reserve, + &PumpSwapFeeBasisPoints::new( + LP_FEE_BASIS_POINTS, + PROTOCOL_FEE_BASIS_POINTS, + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points), + ), + ) +} + +pub fn buy_quote_input_internal_with_fees( + quote: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + fee_basis_points: &PumpSwapFeeBasisPoints, ) -> 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 - + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points); + let total_fee_bps = fee_basis_points + .lp_fee_basis_points + .saturating_add(fee_basis_points.protocol_fee_basis_points) + .saturating_add(fee_basis_points.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; + let mut effective_quote = (quote as u128 * 10_000) / denominator as u128; + let lp_fee = compute_fee(effective_quote, fee_basis_points.lp_fee_basis_points as u128); + let protocol_fee = + compute_fee(effective_quote, fee_basis_points.protocol_fee_basis_points as u128); + let coin_creator_fee = + compute_fee(effective_quote, fee_basis_points.coin_creator_fee_basis_points as u128); + let total_with_fees = effective_quote + lp_fee + protocol_fee + coin_creator_fee; + if total_with_fees > quote as u128 { + effective_quote = effective_quote.saturating_sub(total_with_fees - quote as u128); + } + let input_amount = effective_quote.saturating_sub(1); // 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; + let numerator = (base_reserve as u128) * input_amount; + let denominator_effective = (quote_reserve as u128) + input_amount; if denominator_effective == 0 { return Err("Pool would be depleted; denominator is zero.".to_string()); @@ -190,6 +245,26 @@ pub fn sell_base_input_internal( quote_reserve: u64, coin_creator: &Pubkey, cashback_fee_basis_points: u64, +) -> Result { + sell_base_input_internal_with_fees( + base, + slippage_basis_points, + base_reserve, + quote_reserve, + &PumpSwapFeeBasisPoints::new( + LP_FEE_BASIS_POINTS, + PROTOCOL_FEE_BASIS_POINTS, + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points), + ), + ) +} + +pub fn sell_base_input_internal_with_fees( + base: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + fee_basis_points: &PumpSwapFeeBasisPoints, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -200,12 +275,15 @@ pub fn sell_base_input_internal( / ((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 lp_fee = + compute_fee(quote_amount_out as u128, fee_basis_points.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 creator_bps = - creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128; - let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64; + compute_fee(quote_amount_out as u128, fee_basis_points.protocol_fee_basis_points as u128) + as u64; + let coin_creator_fee = compute_fee( + quote_amount_out as u128, + fee_basis_points.coin_creator_fee_basis_points as u128, + ) as u64; // Calculate final quote after fees let total_fees = lp_fee + protocol_fee + coin_creator_fee; @@ -259,6 +337,26 @@ pub fn sell_quote_input_internal( quote_reserve: u64, coin_creator: &Pubkey, cashback_fee_basis_points: u64, +) -> Result { + sell_quote_input_internal_with_fees( + quote, + slippage_basis_points, + base_reserve, + quote_reserve, + &PumpSwapFeeBasisPoints::new( + LP_FEE_BASIS_POINTS, + PROTOCOL_FEE_BASIS_POINTS, + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points), + ), + ) +} + +pub fn sell_quote_input_internal_with_fees( + quote: u64, + slippage_basis_points: u64, + base_reserve: u64, + quote_reserve: u64, + fee_basis_points: &PumpSwapFeeBasisPoints, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -270,9 +368,9 @@ pub fn sell_quote_input_internal( // Calculate raw quote amount including fees let raw_quote = calculate_quote_amount_out( quote, - LP_FEE_BASIS_POINTS, - PROTOCOL_FEE_BASIS_POINTS, - creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points), + fee_basis_points.lp_fee_basis_points, + fee_basis_points.protocol_fee_basis_points, + fee_basis_points.coin_creator_fee_basis_points, ); // Calculate base amount needed using inverse constant product formula