feat(pumpfun): runtime V2 flag, buyback fee pool fix, legacy ix encoding

- Replace compile-time `pumpfun-v2` feature flag with runtime
  `TradeConfig::use_pumpfun_v2(bool)` for flexible V1/V2 switching
- Fix BUYBACK_FEE_RECIPIENTS pool: replace wrong addresses (was reusing
  standard pool + FEE_CONFIG) with official buyback pool from
  FEE_RECIPIENTS.md
- Fix legacy buy/buy_exact_sol_in ix data encoding: use 2-byte
  Option<bool> (option tag + value) matching official Pump SDK, 26 bytes
  total instead of broken 25
- Add `SwapParams.use_pumpfun_v2` field and wire through TradingClient
  buy/sell paths
- V2 instructions read `quote_mint` from `PumpFunParams` (not
  BondingCurveAccount which lacks the field)
- Fix `fetch_bonding_curve_account` to use BorshDeserialize instead of
  non-existent `decode_from_chain_account_data`
- Update all examples with `use_pumpfun_v2: false` field
- Update README with unified V1/V2 section showing both enabling methods
This commit is contained in:
0xfnzero
2026-05-09 07:15:07 +08:00
parent 0311a0b876
commit 1cc051a874
21 changed files with 841 additions and 513 deletions
+736 -466
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -1,6 +1,9 @@
//! Pump.fun 曲线 `buy` / `buy_exact_sol_in` / `sell` **instruction data** 栈上编码(热路径零堆分配)。
//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`**
//! 的 instruction data 栈上编码(热路径零堆分配)。
//!
//! 与 `@pump-fun/pump-sdk` Anchor `coder.instruction.encode` 对齐:`OptionBool` 在 ix 参数中为 **1 字节**。
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 在 ix 参数中为
//! **1 字节 option tag + 1 字节值**Anchor `Option<bool>` = 2 字节),共 26 字节 ix data。
//! `*_v2` 指令无 `track_volume` 字节(见 [pump-public-docs](https://github.com/pump-fun/pump-public-docs))。
use crate::instruction::utils::pumpfun::{
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
@@ -74,10 +77,10 @@ pub fn encode_pumpfun_buy_exact_quote_in_v2_ix_data(
}
#[inline(always)]
pub fn encode_pumpfun_sell_v2_ix_data(amount: u64, min_sol_output: u64) -> [u8; 24] {
pub fn encode_pumpfun_sell_v2_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&SELL_V2_DISCRIMINATOR);
d[8..16].copy_from_slice(&amount.to_le_bytes());
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
d
}
+21 -22
View File
@@ -4,6 +4,7 @@
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
use anyhow::anyhow;
use borsh::BorshDeserialize;
use rand::seq::IndexedRandom;
use solana_sdk::{
instruction::{AccountMeta, Instruction},
@@ -108,16 +109,16 @@ pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
];
/// Buyback fee recipients (v2 account #9 in buy_v2/sell_v2).
/// Selected randomly — distinct from main fee recipients.
/// 对应官方 FEE_RECIPIENTS.md "Buyback (Applies to All)" 池,与主 fee_recipient 池互斥。
pub const BUYBACK_FEE_RECIPIENTS: [Pubkey; 8] = [
pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"),
pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"),
pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP"),
pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"),
pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"),
pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"),
pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"),
pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt"),
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
];
}
@@ -184,11 +185,12 @@ pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
/// buy_v2: unified buy with quote_mint support (SOL + USDC), 27 fixed accounts, 2 args (no track_volume)
/// `buy_v2` — unified SOL/USDC quote interface ([pump-public-docs](https://github.com/pump-fun/pump-public-docs)).
pub const BUY_V2_DISCRIMINATOR: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
/// sell_v2: unified sell with quote_mint support (SOL + USDC), 26 fixed accounts, 2 args
/// `sell_v2`
pub const SELL_V2_DISCRIMINATOR: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
/// buy_exact_quote_in_v2: spend exact quote amount for min tokens out (SOL + USDC), 27 fixed accounts, 2 args
/// `buy_exact_quote_in_v2` (native SOL spend for SOL-paired coins when `quote_mint` is WSOL)
pub const BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
@@ -297,7 +299,7 @@ pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
}
/// Random buyback fee recipient from static pool (v2 account #9 in buy_v2/sell_v2).
/// Buyback fee recipient (#9 in buy_v2/sell_v2) — dedicated pool, distinct from protocol extra fee recipients.
#[inline]
pub fn get_buyback_fee_recipient_random() -> Pubkey {
*global_constants::BUYBACK_FEE_RECIPIENTS
@@ -305,12 +307,6 @@ pub fn get_buyback_fee_recipient_random() -> Pubkey {
.unwrap_or(&global_constants::BUYBACK_FEE_RECIPIENTS[0])
}
/// Quote token program id for a given quote_mint (both WSOL and USDC use the legacy Token Program).
#[inline]
pub fn get_quote_token_program(_quote_mint: &Pubkey) -> Pubkey {
crate::constants::TOKEN_PROGRAM
}
#[inline]
pub fn pump_fun_fee_recipient_meta(observed_fee_recipient: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
let trust_observation = observed_fee_recipient != Pubkey::default()
@@ -549,9 +545,9 @@ pub async fn fetch_bonding_curve_account(
return Err(anyhow!("Bonding curve not found"));
}
let bonding_curve =
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
let mut bonding_curve = BondingCurveAccount::try_from_slice(&account.data[8..])
.map_err(|e| anyhow::anyhow!("Failed to decode bonding curve account: {}", e))?;
bonding_curve.account = bonding_curve_pda;
Ok((Arc::new(bonding_curve), bonding_curve_pda))
}
@@ -573,6 +569,9 @@ mod tests {
assert_eq!(BUY_DISCRIMINATOR.len(), 8);
assert_eq!(BUY_EXACT_SOL_IN_DISCRIMINATOR.len(), 8);
assert_eq!(SELL_DISCRIMINATOR.len(), 8);
assert_eq!(BUY_V2_DISCRIMINATOR.len(), 8);
assert_eq!(SELL_V2_DISCRIMINATOR.len(), 8);
assert_eq!(BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR.len(), 8);
}
#[test]