fix: align PumpSwap dynamic fee parameters

This commit is contained in:
0xfnzero
2026-06-30 21:40:44 +08:00
parent 9af86c8397
commit 47cef59d15
6 changed files with 620 additions and 55 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.21"
version = "4.0.22"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+14 -9
View File
@@ -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<SolanaTrade> {
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
+39 -23
View File
@@ -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(&params).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);
}
}
+311 -2
View File
@@ -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<PumpSwapFeeTier>,
pub stable_fee_tiers: Vec<PumpSwapFeeTier>,
}
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<RwLock<Option<CachedGlobalConfig>>> =
Lazy::new(|| RwLock::new(None));
static GLOBAL_CONFIG_REFRESH_IN_FLIGHT: AtomicBool = AtomicBool::new(false);
static FEE_CONFIG_CACHE: Lazy<RwLock<Option<CachedFeeConfig>>> = Lazy::new(|| RwLock::new(None));
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
@@ -223,15 +285,34 @@ fn read_pubkey_array<const N: usize>(data: &[u8], offset: usize) -> Option<[Pubk
Some(keys)
}
fn read_u64(data: &[u8], offset: usize) -> Option<u64> {
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<u128> {
let bytes = data.get(offset..offset + 16)?;
Some(u128::from_le_bytes(bytes.try_into().ok()?))
}
fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
let bytes = data.get(offset..offset + 4)?;
Some(u32::from_le_bytes(bytes.try_into().ok()?))
}
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
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<GlobalConfig> {
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<GlobalConfig> {
})
}
fn decode_fees(data: &[u8], offset: usize) -> Option<PumpSwapFeeBasisPoints> {
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<Vec<PumpSwapFeeTier>> {
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<PumpSwapFeeConfig> {
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<GlobalConfig> {
let account = match tokio::time::timeout(
PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT,
@@ -289,6 +413,42 @@ async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfi
Some(config)
}
async fn refresh_fee_config_once(rpc: &SolanaRpcClient) -> Option<PumpSwapFeeConfig> {
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<SolanaRpcClient>>) {
let Some(rpc) = rpc else {
return;
@@ -302,6 +462,7 @@ pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
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<GlobalConfig> {
(cached.fetched_at.elapsed() <= PUMPSWAP_GLOBAL_CONFIG_TTL).then(|| cached.config.clone())
}
fn cached_fee_config() -> Option<PumpSwapFeeConfig> {
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<PumpSwapFeeConfig> {
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<u128> {
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<PumpSwapFeeBasisPoints> {
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<u64>,
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<Pubkey> {
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));
}
}
+138 -1
View File
@@ -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<u64>,
/// 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<Self, anyhow::Error> {
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<u64> {
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<u64, anyhow::Error> {
let account = rpc.get_account(mint).await?;
decode_mint_supply(&account.data).ok_or_else(|| anyhow::anyhow!("Failed to decode mint supply"))
}
+117 -19
View File
@@ -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<BuyBaseInputResult, String> {
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<BuyBaseInputResult, String> {
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<BuyQuoteInputResult, String> {
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<BuyQuoteInputResult, String> {
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<SellBaseInputResult, String> {
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<SellBaseInputResult, String> {
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<SellQuoteInputResult, String> {
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<SellQuoteInputResult, String> {
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