diff --git a/README.md b/README.md index 1730b19..2d8ef3f 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ - [🔍 Address Lookup Tables](#-address-lookup-tables) - [🔍 Nonce Cache](#-nonce-cache) - [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap) +- [🔄 PumpFun V1 vs V2 Instructions](#-pumpfun-v1-vs-v2-instructions) - [🛡️ MEV Protection Services](#️-mev-protection-services) - [📁 Project Structure](#-project-structure) - [📄 License](#-license) @@ -145,6 +146,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) // .check_min_tip(false) // default: false - filter SWQOS below min tip // .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores // .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor) + // .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2 .build(); // Create TradingClient @@ -347,49 +349,58 @@ Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vau The SDK does not fetch creator_vault from RPC on every sell (to avoid latency); pass the up-to-date vault from gRPC/events when available. -#### PumpSwap: coin_creator_vault from events (no RPC) +#### PumpFun V1 vs V2 Instructions -For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_authority` are required in buy/sell instructions. Both are available from parsed events without RPC: +PumpFun has two instruction sets for bonding-curve trading: -- **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`. -- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event’s `coin_creator_vault_ata` and `coin_creator_vault_authority`. +| | V1 (default) | V2 (opt-in) | +|---|---|---| +| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` | +| Account metas | 18 | 27 | +| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) | +| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) | -### Pump.fun Bonding Curve v2 (buy_v2 / sell_v2 / buy_exact_quote_in_v2) - -Pump.fun has upgraded the Bonding Curve contract with **unified v2 instructions** that support both SOL-paired and USDC-paired coins through a single fixed account layout. The legacy `buy`/`sell`/`buy_exact_sol_in` instructions continue to work for SOL-paired coins and remain the default. +**Default: V1** (`use_pumpfun_v2 = false`). The SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. **Key changes in v2 instructions:** -- `quote_mint` parameter — pass wrapped SOL (`So11111111111111111111111111111111111111112`) for SOL-paired, or USDC mint for USDC-paired +- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired - 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts** - `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory - Same pricing and cost as legacy instructions for SOL-paired coins -**Using v2 instructions:** +**How to enable V2:** -Set `quote_mint` on your `PumpFunParams`. The SDK automatically switches to `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` discriminators with the 27/26-account layout: +**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet): + +```rust +let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) + .use_pumpfun_v2(true) // Switch all PumpFun trades to V2 instructions (27 accounts) + .build(); +``` + +**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios): ```rust use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT; use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT; -// SOL-paired coin — use wrapped SOL mint +// SOL-paired coin with v2 layout let params = PumpFunParams::from_trade(/* ... */) .with_quote_mint(WSOL_TOKEN_ACCOUNT); -// USDC-paired coin (coming soon — requires v2) +// USDC-paired coin (requires v2) let params = PumpFunParams::from_trade(/* ... */) .with_quote_mint(USDC_TOKEN_ACCOUNT); - -// Then trade as usual -client.buy(buy_params).await?; -client.sell(sell_params).await?; ``` -| Quote Mint | `use_v2_ix` | Instruction Used | Notes | -|-----------|-------------|-----------------|-------| -| Not set (default) | `false` | Legacy `buy`/`sell`/`buy_exact_sol_in` | Backward compatible, SOL-only | -| `WSOL_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL-paired, unified layout | -| `USDC_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC-paired (requires v2) | +> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2. + +#### PumpSwap: coin_creator_vault from events (no RPC) + +For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_authority` are required in buy/sell instructions. Both are available from parsed events without RPC: + +- **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`. +- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event's `coin_creator_vault_ata` and `coin_creator_vault_authority`. ## 🛡️ MEV Protection Services diff --git a/examples/address_lookup/src/main.rs b/examples/address_lookup/src/main.rs index f119fc5..4081715 100644 --- a/examples/address_lookup/src/main.rs +++ b/examples/address_lookup/src/main.rs @@ -176,6 +176,7 @@ async fn pumpfun_copy_trade_with_grpc( gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/bonk_copy_trading/src/main.rs b/examples/bonk_copy_trading/src/main.rs index ec78b81..85a60c7 100644 --- a/examples/bonk_copy_trading/src/main.rs +++ b/examples/bonk_copy_trading/src/main.rs @@ -179,6 +179,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()> gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/bonk_sniper_trading/src/main.rs b/examples/bonk_sniper_trading/src/main.rs index 84763c4..88173c4 100644 --- a/examples/bonk_sniper_trading/src/main.rs +++ b/examples/bonk_sniper_trading/src/main.rs @@ -147,6 +147,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult< gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/cli_trading/src/main.rs b/examples/cli_trading/src/main.rs index 9b1ee18..917fc18 100644 --- a/examples/cli_trading/src/main.rs +++ b/examples/cli_trading/src/main.rs @@ -636,6 +636,7 @@ async fn handle_buy_pumpfun( gas_fee_strategy: gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; match client.buy(buy_params).await { @@ -692,6 +693,7 @@ async fn handle_buy_pumpswap( gas_fee_strategy: gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; match client.buy(buy_params).await { @@ -748,6 +750,7 @@ async fn handle_buy_bonk( gas_fee_strategy: gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; match client.buy(buy_params).await { @@ -808,6 +811,7 @@ async fn handle_buy_raydium_v4( gas_fee_strategy: gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; match client.buy(buy_params).await { @@ -869,6 +873,7 @@ async fn handle_buy_raydium_cpmm( gas_fee_strategy: gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; match client.buy(buy_params).await { diff --git a/examples/meteora_damm_v2_direct_trading/src/main.rs b/examples/meteora_damm_v2_direct_trading/src/main.rs index 1beb6c7..e8ec36f 100644 --- a/examples/meteora_damm_v2_direct_trading/src/main.rs +++ b/examples/meteora_damm_v2_direct_trading/src/main.rs @@ -51,6 +51,7 @@ async fn main() -> Result<(), Box> { gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/middleware_system/src/main.rs b/examples/middleware_system/src/main.rs index c987463..af9d3af 100644 --- a/examples/middleware_system/src/main.rs +++ b/examples/middleware_system/src/main.rs @@ -112,6 +112,7 @@ async fn test_middleware() -> AnyResult<()> { gas_fee_strategy: gas_fee_strategy, simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/nonce_cache/src/main.rs b/examples/nonce_cache/src/main.rs index afe5008..d8808ee 100644 --- a/examples/nonce_cache/src/main.rs +++ b/examples/nonce_cache/src/main.rs @@ -172,6 +172,7 @@ async fn pumpfun_copy_trade_with_grpc( gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/pumpfun_copy_trading/src/main.rs b/examples/pumpfun_copy_trading/src/main.rs index ed43f57..6e6b13f 100644 --- a/examples/pumpfun_copy_trading/src/main.rs +++ b/examples/pumpfun_copy_trading/src/main.rs @@ -169,6 +169,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/pumpfun_sniper_trading/src/main.rs b/examples/pumpfun_sniper_trading/src/main.rs index 0d90fc6..d0a04d3 100644 --- a/examples/pumpfun_sniper_trading/src/main.rs +++ b/examples/pumpfun_sniper_trading/src/main.rs @@ -159,6 +159,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/pumpswap_direct_trading/src/main.rs b/examples/pumpswap_direct_trading/src/main.rs index 1202ae8..ba50207 100644 --- a/examples/pumpswap_direct_trading/src/main.rs +++ b/examples/pumpswap_direct_trading/src/main.rs @@ -50,6 +50,7 @@ async fn main() -> Result<(), Box> { gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/pumpswap_trading/src/main.rs b/examples/pumpswap_trading/src/main.rs index cfbad3e..9d22f87 100644 --- a/examples/pumpswap_trading/src/main.rs +++ b/examples/pumpswap_trading/src/main.rs @@ -244,6 +244,7 @@ async fn pumpswap_trade_with_grpc( gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/raydium_amm_v4_trading/src/main.rs b/examples/raydium_amm_v4_trading/src/main.rs index b98f737..ff88f9e 100644 --- a/examples/raydium_amm_v4_trading/src/main.rs +++ b/examples/raydium_amm_v4_trading/src/main.rs @@ -181,6 +181,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent) gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/raydium_cpmm_trading/src/main.rs b/examples/raydium_cpmm_trading/src/main.rs index c9f647a..c8720eb 100644 --- a/examples/raydium_cpmm_trading/src/main.rs +++ b/examples/raydium_cpmm_trading/src/main.rs @@ -169,6 +169,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) -> gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/examples/seed_trading/src/main.rs b/examples/seed_trading/src/main.rs index 5223662..c4a18ac 100644 --- a/examples/seed_trading/src/main.rs +++ b/examples/seed_trading/src/main.rs @@ -50,6 +50,7 @@ async fn main() -> Result<(), Box> { gas_fee_strategy: gas_fee_strategy.clone(), simulate: false, use_exact_sol_amount: None, + use_pumpfun_v2: false, grpc_recv_us: None, }; client.buy(buy_params).await?; diff --git a/src/client/mod.rs b/src/client/mod.rs index d87d6df..37ea3b6 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -303,6 +303,8 @@ pub struct TradingClient { pub log_enabled: bool, /// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency. pub check_min_tip: bool, + /// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27-account metas). Default false. + pub use_pumpfun_v2: bool, } static INSTANCE: Mutex>> = Mutex::new(None); @@ -323,6 +325,7 @@ impl Clone for TradingClient { effective_core_ids: self.effective_core_ids.clone(), log_enabled: self.log_enabled, check_min_tip: self.check_min_tip, + use_pumpfun_v2: self.use_pumpfun_v2, } } } @@ -457,6 +460,7 @@ impl TradingClient { effective_core_ids, log_enabled: true, check_min_tip: false, + use_pumpfun_v2: false, } } @@ -503,6 +507,7 @@ impl TradingClient { effective_core_ids, log_enabled: true, check_min_tip: false, + use_pumpfun_v2: false, } } @@ -682,6 +687,7 @@ impl TradingClient { effective_core_ids: infrastructure.effective_core_ids.clone(), log_enabled: trade_config.log_enabled, check_min_tip: trade_config.check_min_tip, + use_pumpfun_v2: trade_config.use_pumpfun_v2, }; let mut current = INSTANCE.lock(); @@ -860,6 +866,7 @@ impl TradingClient { check_min_tip: self.check_min_tip, grpc_recv_us: params.grpc_recv_us, use_exact_sol_amount: params.use_exact_sol_amount, + use_pumpfun_v2: self.use_pumpfun_v2, }; let swap_result = executor.swap(buy_params).await; @@ -967,6 +974,7 @@ impl TradingClient { check_min_tip: self.check_min_tip, grpc_recv_us: params.grpc_recv_us, use_exact_sol_amount: None, + use_pumpfun_v2: self.use_pumpfun_v2, }; let swap_result = executor.swap(sell_params).await; diff --git a/src/common/types.rs b/src/common/types.rs index 38ce257..b7b636a 100755 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -95,6 +95,10 @@ pub struct TradeConfig { /// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation) /// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency). pub mev_protection: bool, + /// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27-account metas, quote_mint support). + /// Default: `false` — uses V1 instructions (18-account metas, legacy SOL-paired, smaller transaction). + /// Set to `true` when PumpFun officially deploys V2 on mainnet. + pub use_pumpfun_v2: bool, } impl TradeConfig { @@ -149,6 +153,7 @@ pub struct TradeConfigBuilder { check_min_tip: bool, swqos_cores_from_end: bool, mev_protection: bool, + use_pumpfun_v2: bool, } impl TradeConfigBuilder { @@ -163,6 +168,7 @@ impl TradeConfigBuilder { check_min_tip: false, swqos_cores_from_end: false, mev_protection: false, + use_pumpfun_v2: false, } } @@ -208,6 +214,14 @@ impl TradeConfigBuilder { self } + /// Use PumpFun V2 instructions (`buy_v2` / `sell_v2`, 27-account metas, `quote_mint` support). + /// Default: `false` (V1 — 18-account metas, legacy SOL-paired, smaller transaction). + /// Set to `true` when PumpFun officially deploys V2 on mainnet. + pub fn use_pumpfun_v2(mut self, v: bool) -> Self { + self.use_pumpfun_v2 = v; + self + } + /// Consume the builder and produce a [`TradeConfig`]. pub fn build(self) -> TradeConfig { TradeConfig { @@ -220,6 +234,7 @@ impl TradeConfigBuilder { check_min_tip: self.check_min_tip, swqos_cores_from_end: self.swqos_cores_from_end, mev_protection: self.mev_protection, + use_pumpfun_v2: self.use_pumpfun_v2, } } } diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index ca834ea..09dfebd 100644 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -1,4 +1,9 @@ //! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)). +//! +//! Runtime flag: set `SwapParams.use_pumpfun_v2 = true` to use `buy_v2` / `sell_v2` / +//! `buy_exact_quote_in_v2` (27-account metas, quote_mint support). +//! Default (`false`) uses V1 instructions (18-account metas, legacy SOL-paired). +//! PumpFun V2 is NOT live on mainnet yet — keep `false` until officially deployed. use crate::{ common::spl_token::close_account, @@ -9,16 +14,11 @@ use crate::{ }, }; use crate::{ - instruction::pumpfun_ix_data::{ - encode_pumpfun_buy_exact_quote_in_v2_ix_data, encode_pumpfun_buy_exact_sol_in_ix_data, - encode_pumpfun_buy_ix_data, encode_pumpfun_buy_v2_ix_data, - encode_pumpfun_sell_ix_data, encode_pumpfun_sell_v2_ix_data, - }, instruction::utils::pumpfun::{ - accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, - get_buyback_fee_recipient_random, get_protocol_extra_fee_recipient_random, - get_quote_token_program, get_user_volume_accumulator_pda, - pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix_with_fee_sharing, + accounts, get_bonding_curve_pda, + get_user_volume_accumulator_pda, + pump_fun_fee_recipient_meta, + resolve_creator_vault_for_ix_with_fee_sharing, global_constants::{self}, }, utils::calc::{ @@ -40,474 +40,731 @@ fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey } } +/// Resolve quote mint and its token program from PumpFunParams. +/// `Pubkey::default()` means legacy SOL-paired → use WSOL mint for V2 instructions. +#[inline] +fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) { + let quote_mint = if protocol_params.quote_mint == Pubkey::default() { + crate::constants::WSOL_TOKEN_ACCOUNT + } else { + protocol_params.quote_mint + }; + (quote_mint, crate::constants::TOKEN_PROGRAM) +} + pub struct PumpFunInstructionBuilder; #[async_trait::async_trait] impl InstructionBuilder for PumpFunInstructionBuilder { async fn build_buy_instructions(&self, params: &SwapParams) -> Result> { - let protocol_params = params - .protocol_params - .as_any() - .downcast_ref::() - .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; - - let lamports_in = params.input_amount.unwrap_or(0); - if lamports_in == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - - let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); - - let bonding_curve = &protocol_params.bonding_curve; - let creator = protocol_params.effective_creator_for_trade(); - let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing( - &creator, - protocol_params.creator_vault, - ¶ms.output_mint, - protocol_params.fee_sharing_creator_vault_if_active, - ) - .ok_or_else(|| { - anyhow!( - "creator_vault PDA derivation failed (creator={})", - creator - ) - })?; - - let buy_token_amount = match params.fixed_output_amount { - Some(amount) => amount, - None => 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, - creator, - lamports_in, - ), - }; - - let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp); - - let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| { - anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint) - })?; - - let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let token_program = effective_pump_mint_token_program(protocol_params); - let token_program_meta = if token_program == TOKEN_PROGRAM_2022 { - crate::constants::TOKEN_PROGRAM_2022_META + if params.use_pumpfun_v2 { + build_buy_v2(params) } else { - crate::constants::TOKEN_PROGRAM_META - }; - - let associated_bonding_curve = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &bonding_curve_addr, - ¶ms.output_mint, - &token_program, - ); - - let user_token_account = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( - ¶ms.payer.pubkey(), - ¶ms.output_mint, - &token_program, - params.open_seed_optimize, - ); - - let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) - .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; - - let mut instructions = Vec::with_capacity(2); - - if params.create_output_mint_ata { - instructions.extend( - crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( - ¶ms.payer.pubkey(), - ¶ms.payer.pubkey(), - ¶ms.output_mint, - &token_program, - params.open_seed_optimize, - ), - ); + build_buy_v1(params) } - - let fee_recipient_meta = - pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); - - // ── V2 instruction path (buy_v2 / buy_exact_quote_in_v2, 27 fixed accounts) ── - if protocol_params.use_v2_ix { - let quote_mint = if protocol_params.quote_mint != Pubkey::default() { - protocol_params.quote_mint - } else { - crate::constants::WSOL_TOKEN_ACCOUNT - }; - let quote_token_program = get_quote_token_program("e_mint); - let quote_token_program_meta = if quote_token_program == crate::constants::TOKEN_PROGRAM { - crate::constants::TOKEN_PROGRAM_META - } else { - crate::constants::TOKEN_PROGRAM_2022_META - }; - - let associated_quote_bonding_curve = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &bonding_curve_addr, "e_mint, "e_token_program, - ); - let associated_quote_user = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( - ¶ms.payer.pubkey(), "e_mint, "e_token_program, - params.open_seed_optimize, - ); - let associated_quote_fee_recipient = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &fee_recipient_meta.pubkey, "e_mint, "e_token_program, - ); - let buyback_fee_recipient = - get_buyback_fee_recipient_random(); - let associated_quote_buyback_fee_recipient = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &buyback_fee_recipient, "e_mint, "e_token_program, - ); - let associated_creator_vault = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &creator_vault_account, "e_mint, "e_token_program, - ); - let associated_user_volume_accumulator = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &user_volume_accumulator, "e_mint, "e_token_program, - ); - let sharing_config = - crate::instruction::utils::pumpfun::get_fee_sharing_config_pda(¶ms.output_mint) - .ok_or_else(|| anyhow!("sharing_config PDA derivation failed"))?; - - let v2_data = if params.use_exact_sol_amount.unwrap_or(true) { - let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp); - encode_pumpfun_buy_exact_quote_in_v2_ix_data(lamports_in, min_tokens_out) - } else { - encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost) - }; - - let v2_metas: Vec = vec![ - global_constants::GLOBAL_ACCOUNT_META, - AccountMeta::new_readonly(params.output_mint, false), - AccountMeta::new_readonly(quote_mint, false), - token_program_meta, - quote_token_program_meta, - crate::constants::ASSOCIATED_TOKEN_PROGRAM_META, - fee_recipient_meta, - AccountMeta::new(associated_quote_fee_recipient, false), - AccountMeta::new_readonly(buyback_fee_recipient, false), - AccountMeta::new(associated_quote_buyback_fee_recipient, false), - AccountMeta::new(bonding_curve_addr, false), - AccountMeta::new(associated_bonding_curve, false), - AccountMeta::new(associated_quote_bonding_curve, false), - AccountMeta::new(params.payer.pubkey(), true), - AccountMeta::new(user_token_account, false), - AccountMeta::new(associated_quote_user, false), - AccountMeta::new(creator_vault_account, false), - AccountMeta::new(associated_creator_vault, false), - AccountMeta::new_readonly(sharing_config, false), - accounts::GLOBAL_VOLUME_ACCUMULATOR_META, - AccountMeta::new(user_volume_accumulator, false), - AccountMeta::new(associated_user_volume_accumulator, false), - accounts::FEE_CONFIG_META, - accounts::FEE_PROGRAM_META, - crate::constants::SYSTEM_PROGRAM_META, - accounts::EVENT_AUTHORITY_META, - accounts::PUMPFUN_META, - ]; - - if quote_mint != crate::constants::WSOL_TOKEN_ACCOUNT { - let create_quote_ata = crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( - ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), - "e_mint, "e_token_program, params.open_seed_optimize, - ); - if !create_quote_ata.is_empty() { - instructions.extend(create_quote_ata); - } - } - - instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &v2_data, v2_metas)); - return Ok(instructions); - } - - // ── Legacy buy path ── - let track_volume_val = if bonding_curve.is_cashback_coin { 1u8 } else { 0u8 }; - let buy_data = if params.use_exact_sol_amount.unwrap_or(true) { - let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp); - encode_pumpfun_buy_exact_sol_in_ix_data( - lamports_in, - min_tokens_out, - track_volume_val, - ) - } else { - encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val) - }; - - let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).ok_or_else(|| { - anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint) - })?; - let mut metas: Vec = vec![ - global_constants::GLOBAL_ACCOUNT_META, - fee_recipient_meta, - AccountMeta::new_readonly(params.output_mint, false), - AccountMeta::new(bonding_curve_addr, false), - AccountMeta::new(associated_bonding_curve, false), - AccountMeta::new(user_token_account, false), - AccountMeta::new(params.payer.pubkey(), true), - crate::constants::SYSTEM_PROGRAM_META, - token_program_meta, - AccountMeta::new(creator_vault_account, false), - accounts::EVENT_AUTHORITY_META, - accounts::PUMPFUN_META, - accounts::GLOBAL_VOLUME_ACCUMULATOR_META, - AccountMeta::new(user_volume_accumulator, false), - accounts::FEE_CONFIG_META, - accounts::FEE_PROGRAM_META, - ]; - metas.push(AccountMeta::new_readonly(bonding_curve_v2, false)); - metas.push(AccountMeta::new( - get_protocol_extra_fee_recipient_random(), - false, - )); - - instructions.push(Instruction::new_with_bytes( - accounts::PUMPFUN, - &buy_data, - metas, - )); - - Ok(instructions) } async fn build_sell_instructions(&self, params: &SwapParams) -> Result> { - let protocol_params = params - .protocol_params - .as_any() - .downcast_ref::() - .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; - - let token_amount = if let Some(amount) = params.input_amount { - if amount == 0 { - return Err(anyhow!("Amount cannot be zero")); - } - amount + if params.use_pumpfun_v2 { + build_sell_v2(params) } else { - return Err(anyhow!("Amount token is required")); - }; - - let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); - - let bonding_curve = &protocol_params.bonding_curve; - let creator = protocol_params.effective_creator_for_trade(); - let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing( - &creator, - protocol_params.creator_vault, - ¶ms.input_mint, - protocol_params.fee_sharing_creator_vault_if_active, - ) - .ok_or_else(|| { - anyhow!( - "creator_vault PDA derivation failed (creator={})", - creator - ) - })?; - - let sol_amount = get_sell_sol_amount_from_token_amount( - bonding_curve.virtual_token_reserves as u128, - bonding_curve.virtual_sol_reserves as u128, - creator, - token_amount, - ); - - let min_sol_output = match params.fixed_output_amount { - Some(fixed) => fixed, - None => calculate_with_slippage_sell(sol_amount, slippage_bp), - }; - - let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| { - anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) - })?; - - let is_mayhem_mode = bonding_curve.is_mayhem_mode; - let token_program = effective_pump_mint_token_program(protocol_params); - let token_program_meta = if token_program == TOKEN_PROGRAM_2022 { - crate::constants::TOKEN_PROGRAM_2022_META - } else { - crate::constants::TOKEN_PROGRAM_META - }; - - let associated_bonding_curve = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &bonding_curve_addr, - ¶ms.input_mint, - &token_program, - ); - - let user_token_account = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( - ¶ms.payer.pubkey(), - ¶ms.input_mint, - &token_program, - params.open_seed_optimize, - ); - - let mut instructions = Vec::with_capacity(2); - - // ── V2 sell path (sell_v2, 26 fixed accounts) ── - if protocol_params.use_v2_ix { - let quote_mint = if protocol_params.quote_mint != Pubkey::default() { - protocol_params.quote_mint - } else { - crate::constants::WSOL_TOKEN_ACCOUNT - }; - let quote_token_program = get_quote_token_program("e_mint); - let quote_token_program_meta = if quote_token_program == crate::constants::TOKEN_PROGRAM { - crate::constants::TOKEN_PROGRAM_META - } else { - crate::constants::TOKEN_PROGRAM_2022_META - }; - - let associated_quote_bonding_curve = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &bonding_curve_addr, "e_mint, "e_token_program, - ); - let associated_quote_user = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( - ¶ms.payer.pubkey(), "e_mint, "e_token_program, - params.open_seed_optimize, - ); - - let sell_fee_recipient_meta = - pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); - let associated_quote_fee_recipient = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &sell_fee_recipient_meta.pubkey, "e_mint, "e_token_program, - ); - let buyback_fee_recipient = - get_buyback_fee_recipient_random(); - let associated_quote_buyback_fee_recipient = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &buyback_fee_recipient, "e_mint, "e_token_program, - ); - let associated_creator_vault = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &creator_vault_account, "e_mint, "e_token_program, - ); - let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) - .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; - let associated_user_volume_accumulator = - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &user_volume_accumulator, "e_mint, "e_token_program, - ); - let sharing_config = - crate::instruction::utils::pumpfun::get_fee_sharing_config_pda(¶ms.input_mint) - .ok_or_else(|| anyhow!("sharing_config PDA derivation failed"))?; - - let v2_sell_data = encode_pumpfun_sell_v2_ix_data(token_amount, min_sol_output); - - let v2_sell_metas: Vec = vec![ - global_constants::GLOBAL_ACCOUNT_META, - AccountMeta::new_readonly(params.input_mint, false), - AccountMeta::new_readonly(quote_mint, false), - token_program_meta, - quote_token_program_meta, - crate::constants::ASSOCIATED_TOKEN_PROGRAM_META, - sell_fee_recipient_meta, - AccountMeta::new(associated_quote_fee_recipient, false), - AccountMeta::new_readonly(buyback_fee_recipient, false), - AccountMeta::new(associated_quote_buyback_fee_recipient, false), - AccountMeta::new(bonding_curve_addr, false), - AccountMeta::new(associated_bonding_curve, false), - AccountMeta::new(associated_quote_bonding_curve, false), - AccountMeta::new(params.payer.pubkey(), true), - AccountMeta::new(user_token_account, false), - AccountMeta::new(associated_quote_user, false), - AccountMeta::new(creator_vault_account, false), - AccountMeta::new(associated_creator_vault, false), - AccountMeta::new_readonly(sharing_config, false), - AccountMeta::new(user_volume_accumulator, false), - AccountMeta::new(associated_user_volume_accumulator, false), - accounts::FEE_CONFIG_META, - accounts::FEE_PROGRAM_META, - crate::constants::SYSTEM_PROGRAM_META, - accounts::EVENT_AUTHORITY_META, - accounts::PUMPFUN_META, - ]; - - instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &v2_sell_data, v2_sell_metas)); - - if protocol_params.close_token_account_when_sell.unwrap_or(false) - || params.close_input_mint_ata - { - instructions.push(close_account( - &token_program, &user_token_account, - ¶ms.payer.pubkey(), ¶ms.payer.pubkey(), - &[¶ms.payer.pubkey()], - )?); - } - - return Ok(instructions); + build_sell_v1(params) } - - // ── Legacy sell path ── - let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output); - let fee_recipient_meta = - pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); - - let mut metas: Vec = vec![ - global_constants::GLOBAL_ACCOUNT_META, - fee_recipient_meta, - AccountMeta::new_readonly(params.input_mint, false), - AccountMeta::new(bonding_curve_addr, false), - AccountMeta::new(associated_bonding_curve, false), - AccountMeta::new(user_token_account, false), - AccountMeta::new(params.payer.pubkey(), true), - crate::constants::SYSTEM_PROGRAM_META, - AccountMeta::new(creator_vault_account, false), - token_program_meta, - accounts::EVENT_AUTHORITY_META, - accounts::PUMPFUN_META, - accounts::FEE_CONFIG_META, - accounts::FEE_PROGRAM_META, - ]; - - if bonding_curve.is_cashback_coin { - let user_volume_accumulator = - get_user_volume_accumulator_pda(¶ms.payer.pubkey()) - .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; - metas.push(AccountMeta::new(user_volume_accumulator, false)); - } - - let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.input_mint).ok_or_else(|| { - anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint) - })?; - metas.push(AccountMeta::new_readonly(bonding_curve_v2, false)); - metas.push(AccountMeta::new( - get_protocol_extra_fee_recipient_random(), - false, - )); - - instructions.push(Instruction::new_with_bytes( - accounts::PUMPFUN, - &sell_data, - metas, - )); - - if protocol_params.close_token_account_when_sell.unwrap_or(false) - || params.close_input_mint_ata - { - instructions.push(close_account( - &token_program, - &user_token_account, - ¶ms.payer.pubkey(), - ¶ms.payer.pubkey(), - &[¶ms.payer.pubkey()], - )?); - } - - Ok(instructions) } } +// --------------------------------------------------------------------------- +// V1 (default) — 18 metas, no quote_mint ATA create +// --------------------------------------------------------------------------- + +fn build_buy_v1(params: &SwapParams) -> Result> { + use crate::instruction::pumpfun_ix_data::{ + encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data, + }; + use crate::instruction::utils::pumpfun::{ + get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random, + }; + + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + + let lamports_in = params.input_amount.unwrap_or(0); + if lamports_in == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); + + let bonding_curve = &protocol_params.bonding_curve; + let creator = protocol_params.effective_creator_for_trade(); + let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing( + &creator, + protocol_params.creator_vault, + ¶ms.output_mint, + protocol_params.fee_sharing_creator_vault_if_active, + ) + .ok_or_else(|| { + anyhow!( + "creator_vault PDA derivation failed (creator={})", + creator + ) + })?; + + let buy_token_amount = match params.fixed_output_amount { + Some(amount) => amount, + None => 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, + creator, + lamports_in, + ), + }; + + let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp); + + let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| { + anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint) + })?; + + let is_mayhem_mode = bonding_curve.is_mayhem_mode; + let token_program = effective_pump_mint_token_program(protocol_params); + let token_program_meta = if token_program == TOKEN_PROGRAM_2022 { + crate::constants::TOKEN_PROGRAM_2022_META + } else { + crate::constants::TOKEN_PROGRAM_META + }; + + let associated_bonding_curve = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + ¶ms.output_mint, + &token_program, + ); + + let user_token_account = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.output_mint, + &token_program, + params.open_seed_optimize, + ); + + let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) + .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; + + let mut instructions = Vec::with_capacity(2); + + if params.create_output_mint_ata { + instructions.extend( + crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + ¶ms.output_mint, + &token_program, + params.open_seed_optimize, + ), + ); + } + + // ── Legacy buy path ── + let track_volume_val = if bonding_curve.is_cashback_coin { 1u8 } else { 0u8 }; + let buy_data = if params.use_exact_sol_amount.unwrap_or(true) { + let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp); + encode_pumpfun_buy_exact_sol_in_ix_data( + lamports_in, + min_tokens_out, + track_volume_val, + ) + } else { + encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val) + }; + + let fee_recipient_meta = + pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); + + let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).ok_or_else(|| { + anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint) + })?; + let mut metas: Vec = vec![ + global_constants::GLOBAL_ACCOUNT_META, + fee_recipient_meta, + AccountMeta::new_readonly(params.output_mint, false), + AccountMeta::new(bonding_curve_addr, false), + AccountMeta::new(associated_bonding_curve, false), + AccountMeta::new(user_token_account, false), + AccountMeta::new(params.payer.pubkey(), true), + crate::constants::SYSTEM_PROGRAM_META, + token_program_meta, + AccountMeta::new(creator_vault_account, false), + accounts::EVENT_AUTHORITY_META, + accounts::PUMPFUN_META, + accounts::GLOBAL_VOLUME_ACCUMULATOR_META, + AccountMeta::new(user_volume_accumulator, false), + accounts::FEE_CONFIG_META, + accounts::FEE_PROGRAM_META, + ]; + metas.push(AccountMeta::new_readonly(bonding_curve_v2, false)); + metas.push(AccountMeta::new( + get_protocol_extra_fee_recipient_random(), + false, + )); + + instructions.push(Instruction::new_with_bytes( + accounts::PUMPFUN, + &buy_data, + metas, + )); + + Ok(instructions) +} + +fn build_sell_v1(params: &SwapParams) -> Result> { + use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_ix_data; + use crate::instruction::utils::pumpfun::{ + get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random, + }; + + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + + let token_amount = if let Some(amount) = params.input_amount { + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + amount + } else { + return Err(anyhow!("Amount token is required")); + }; + + let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); + + let bonding_curve = &protocol_params.bonding_curve; + let creator = protocol_params.effective_creator_for_trade(); + let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing( + &creator, + protocol_params.creator_vault, + ¶ms.input_mint, + protocol_params.fee_sharing_creator_vault_if_active, + ) + .ok_or_else(|| { + anyhow!( + "creator_vault PDA derivation failed (creator={})", + creator + ) + })?; + + let sol_amount = get_sell_sol_amount_from_token_amount( + bonding_curve.virtual_token_reserves as u128, + bonding_curve.virtual_sol_reserves as u128, + creator, + token_amount, + ); + + let min_sol_output = match params.fixed_output_amount { + Some(fixed) => fixed, + None => calculate_with_slippage_sell(sol_amount, slippage_bp), + }; + + let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| { + anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) + })?; + + let is_mayhem_mode = bonding_curve.is_mayhem_mode; + let token_program = effective_pump_mint_token_program(protocol_params); + let token_program_meta = if token_program == TOKEN_PROGRAM_2022 { + crate::constants::TOKEN_PROGRAM_2022_META + } else { + crate::constants::TOKEN_PROGRAM_META + }; + + let associated_bonding_curve = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + ¶ms.input_mint, + &token_program, + ); + + let user_token_account = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.input_mint, + &token_program, + params.open_seed_optimize, + ); + + let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) + .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; + + let mut instructions = Vec::with_capacity(2); + let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output); + + let fee_recipient_meta = + pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); + + let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.input_mint).ok_or_else(|| { + anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint) + })?; + let mut metas: Vec = vec![ + global_constants::GLOBAL_ACCOUNT_META, + fee_recipient_meta, + AccountMeta::new_readonly(params.input_mint, false), + AccountMeta::new(bonding_curve_addr, false), + AccountMeta::new(associated_bonding_curve, false), + AccountMeta::new(user_token_account, false), + AccountMeta::new(params.payer.pubkey(), true), + crate::constants::SYSTEM_PROGRAM_META, + AccountMeta::new(creator_vault_account, false), + token_program_meta, + accounts::EVENT_AUTHORITY_META, + accounts::PUMPFUN_META, + accounts::FEE_CONFIG_META, + accounts::FEE_PROGRAM_META, + ]; + + if bonding_curve.is_mayhem_mode { + metas.push(AccountMeta::new(user_volume_accumulator, false)); + } + + metas.push(AccountMeta::new_readonly(bonding_curve_v2, false)); + metas.push(AccountMeta::new( + get_protocol_extra_fee_recipient_random(), + false, + )); + + instructions.push(Instruction::new_with_bytes( + accounts::PUMPFUN, + &sell_data, + metas, + )); + + if protocol_params.close_token_account_when_sell.unwrap_or(false) + || params.close_input_mint_ata + { + instructions.push(close_account( + &token_program, + &user_token_account, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[¶ms.payer.pubkey()], + )?); + } + + Ok(instructions) +} + +// --------------------------------------------------------------------------- +// V2 (opt-in via `use_pumpfun_v2 = true`) — 27 metas, quote_mint ATA create +// --------------------------------------------------------------------------- + +fn build_buy_v2(params: &SwapParams) -> Result> { + use crate::instruction::pumpfun_ix_data::{ + encode_pumpfun_buy_exact_quote_in_v2_ix_data, encode_pumpfun_buy_v2_ix_data, + }; + use crate::instruction::utils::pumpfun::{ + get_buyback_fee_recipient_random, get_fee_sharing_config_pda, + }; + + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + + let lamports_in = params.input_amount.unwrap_or(0); + if lamports_in == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + + let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); + + let bonding_curve = &protocol_params.bonding_curve; + let creator = protocol_params.effective_creator_for_trade(); + let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing( + &creator, + protocol_params.creator_vault, + ¶ms.output_mint, + protocol_params.fee_sharing_creator_vault_if_active, + ) + .ok_or_else(|| { + anyhow!( + "creator_vault PDA derivation failed (creator={})", + creator + ) + })?; + + let buy_token_amount = match params.fixed_output_amount { + Some(amount) => amount, + None => 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, + creator, + lamports_in, + ), + }; + + let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp); + + let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| { + anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint) + })?; + + let is_mayhem_mode = bonding_curve.is_mayhem_mode; + let base_token_program = effective_pump_mint_token_program(protocol_params); + let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 { + crate::constants::TOKEN_PROGRAM_2022_META + } else { + crate::constants::TOKEN_PROGRAM_META + }; + + let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params); + let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META; + + let associated_base_bonding_curve = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + ¶ms.output_mint, + &base_token_program, + ); + + let associated_base_user = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.output_mint, + &base_token_program, + params.open_seed_optimize, + ); + + let fee_recipient_meta = + pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); + let fee_recipient_pk = fee_recipient_meta.pubkey; + let buyback_fee_recipient = get_buyback_fee_recipient_random(); + + let associated_quote_fee_recipient = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &fee_recipient_pk, + "e_mint, + "e_token_program, + ); + let associated_quote_buyback_fee_recipient = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &buyback_fee_recipient, + "e_mint, + "e_token_program, + ); + let associated_quote_bonding_curve = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + "e_mint, + "e_token_program, + ); + let associated_quote_user = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( + ¶ms.payer.pubkey(), + "e_mint, + "e_token_program, + params.open_seed_optimize, + ); + let associated_creator_vault = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &creator_vault_account, + "e_mint, + "e_token_program, + ); + + let sharing_config = + get_fee_sharing_config_pda(¶ms.output_mint).ok_or_else(|| { + anyhow!("sharing_config PDA derivation failed for mint {}", params.output_mint) + })?; + + let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) + .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; + let associated_user_volume_accumulator = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &user_volume_accumulator, + "e_mint, + "e_token_program, + ); + + let mut instructions = Vec::with_capacity(4); + + if params.create_output_mint_ata { + instructions.extend( + crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + ¶ms.output_mint, + &base_token_program, + params.open_seed_optimize, + ), + ); + } + + if params.create_input_mint_ata { + instructions.extend( + crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + "e_mint, + "e_token_program, + params.open_seed_optimize, + ), + ); + } + + let buy_data = if params.use_exact_sol_amount.unwrap_or(true) { + let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp); + encode_pumpfun_buy_exact_quote_in_v2_ix_data(lamports_in, min_tokens_out) + } else { + encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost) + }; + + let metas: Vec = vec![ + global_constants::GLOBAL_ACCOUNT_META, + AccountMeta::new_readonly(params.output_mint, false), + AccountMeta::new_readonly(quote_mint, false), + base_token_program_meta, + quote_token_program_meta, + AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), + fee_recipient_meta, + AccountMeta::new(associated_quote_fee_recipient, false), + AccountMeta::new_readonly(buyback_fee_recipient, false), + AccountMeta::new(associated_quote_buyback_fee_recipient, false), + AccountMeta::new(bonding_curve_addr, false), + AccountMeta::new(associated_base_bonding_curve, false), + AccountMeta::new(associated_quote_bonding_curve, false), + AccountMeta::new(params.payer.pubkey(), true), + AccountMeta::new(associated_base_user, false), + AccountMeta::new(associated_quote_user, false), + AccountMeta::new(creator_vault_account, false), + AccountMeta::new(associated_creator_vault, false), + AccountMeta::new_readonly(sharing_config, false), + accounts::GLOBAL_VOLUME_ACCUMULATOR_META, + AccountMeta::new(user_volume_accumulator, false), + AccountMeta::new(associated_user_volume_accumulator, false), + accounts::FEE_CONFIG_META, + accounts::FEE_PROGRAM_META, + crate::constants::SYSTEM_PROGRAM_META, + accounts::EVENT_AUTHORITY_META, + accounts::PUMPFUN_META, + ]; + + instructions.push(Instruction::new_with_bytes( + accounts::PUMPFUN, + &buy_data, + metas, + )); + + Ok(instructions) +} + +fn build_sell_v2(params: &SwapParams) -> Result> { + use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_v2_ix_data; + use crate::instruction::utils::pumpfun::{ + get_buyback_fee_recipient_random, get_fee_sharing_config_pda, + }; + + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + + let token_amount = if let Some(amount) = params.input_amount { + if amount == 0 { + return Err(anyhow!("Amount cannot be zero")); + } + amount + } else { + return Err(anyhow!("Amount token is required")); + }; + + let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE); + + let bonding_curve = &protocol_params.bonding_curve; + let creator = protocol_params.effective_creator_for_trade(); + let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing( + &creator, + protocol_params.creator_vault, + ¶ms.input_mint, + protocol_params.fee_sharing_creator_vault_if_active, + ) + .ok_or_else(|| { + anyhow!( + "creator_vault PDA derivation failed (creator={})", + creator + ) + })?; + + let sol_amount = get_sell_sol_amount_from_token_amount( + bonding_curve.virtual_token_reserves as u128, + bonding_curve.virtual_sol_reserves as u128, + creator, + token_amount, + ); + + let min_sol_output = match params.fixed_output_amount { + Some(fixed) => fixed, + None => calculate_with_slippage_sell(sol_amount, slippage_bp), + }; + + let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| { + anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) + })?; + + let is_mayhem_mode = bonding_curve.is_mayhem_mode; + let base_token_program = effective_pump_mint_token_program(protocol_params); + let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 { + crate::constants::TOKEN_PROGRAM_2022_META + } else { + crate::constants::TOKEN_PROGRAM_META + }; + + let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params); + let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META; + + let associated_base_bonding_curve = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + ¶ms.input_mint, + &base_token_program, + ); + + let associated_base_user = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.input_mint, + &base_token_program, + params.open_seed_optimize, + ); + + let fee_recipient_meta = + pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); + let fee_recipient_pk = fee_recipient_meta.pubkey; + let buyback_fee_recipient = get_buyback_fee_recipient_random(); + + let associated_quote_fee_recipient = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &fee_recipient_pk, + "e_mint, + "e_token_program, + ); + let associated_quote_buyback_fee_recipient = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &buyback_fee_recipient, + "e_mint, + "e_token_program, + ); + let associated_quote_bonding_curve = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + "e_mint, + "e_token_program, + ); + let associated_quote_user = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( + ¶ms.payer.pubkey(), + "e_mint, + "e_token_program, + params.open_seed_optimize, + ); + let associated_creator_vault = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &creator_vault_account, + "e_mint, + "e_token_program, + ); + + let sharing_config = get_fee_sharing_config_pda(¶ms.input_mint).ok_or_else(|| { + anyhow!("sharing_config PDA derivation failed for mint {}", params.input_mint) + })?; + + let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey()) + .ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?; + let associated_user_volume_accumulator = + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &user_volume_accumulator, + "e_mint, + "e_token_program, + ); + + let mut instructions = Vec::with_capacity(3); + + if params.create_output_mint_ata { + instructions.extend( + crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + "e_mint, + "e_token_program, + params.open_seed_optimize, + ), + ); + } + + let sell_data = encode_pumpfun_sell_v2_ix_data(token_amount, min_sol_output); + + let metas: Vec = vec![ + global_constants::GLOBAL_ACCOUNT_META, + AccountMeta::new_readonly(params.input_mint, false), + AccountMeta::new_readonly(quote_mint, false), + base_token_program_meta, + quote_token_program_meta, + AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false), + fee_recipient_meta, + AccountMeta::new(associated_quote_fee_recipient, false), + AccountMeta::new_readonly(buyback_fee_recipient, false), + AccountMeta::new(associated_quote_buyback_fee_recipient, false), + AccountMeta::new(bonding_curve_addr, false), + AccountMeta::new(associated_base_bonding_curve, false), + AccountMeta::new(associated_quote_bonding_curve, false), + AccountMeta::new(params.payer.pubkey(), true), + AccountMeta::new(associated_base_user, false), + AccountMeta::new(associated_quote_user, false), + AccountMeta::new(creator_vault_account, false), + AccountMeta::new(associated_creator_vault, false), + AccountMeta::new_readonly(sharing_config, false), + AccountMeta::new(user_volume_accumulator, false), + AccountMeta::new(associated_user_volume_accumulator, false), + accounts::FEE_CONFIG_META, + accounts::FEE_PROGRAM_META, + crate::constants::SYSTEM_PROGRAM_META, + accounts::EVENT_AUTHORITY_META, + accounts::PUMPFUN_META, + ]; + + instructions.push(Instruction::new_with_bytes( + accounts::PUMPFUN, + &sell_data, + metas, + )); + + if protocol_params.close_token_account_when_sell.unwrap_or(false) + || params.close_input_mint_ata + { + instructions.push(close_account( + &base_token_program, + &associated_base_user, + ¶ms.payer.pubkey(), + ¶ms.payer.pubkey(), + &[¶ms.payer.pubkey()], + )?); + } + + Ok(instructions) +} + +// --------------------------------------------------------------------------- +// Shared: claim_cashback (independent of V1/V2) +// --------------------------------------------------------------------------- + /// Claim cashback (UserVolumeAccumulator → user lamports). pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option { const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197]; @@ -516,12 +773,25 @@ pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option AccountMeta::new(*payer, true), AccountMeta::new(user_volume_accumulator, false), crate::constants::SYSTEM_PROGRAM_META, - accounts::EVENT_AUTHORITY_META, - accounts::PUMPFUN_META, ]; - Some(Instruction::new_with_bytes( + let ix = Instruction::new_with_bytes( accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, ix_accounts, - )) + ); + Some(ix) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{instruction::pumpfun_ix_data, trading::core::params::PumpFunParams}; + + #[test] + fn test_claim_cashback_instruction() { + let payer = Pubkey::new_unique(); + let ix = claim_cashback_pumpfun_instruction(&payer).unwrap(); + assert_eq!(ix.accounts.len(), 3); + assert_eq!(ix.accounts[0].pubkey, payer); + } } diff --git a/src/instruction/pumpfun_ix_data.rs b/src/instruction/pumpfun_ix_data.rs index 0fe54d7..c757f6e 100644 --- a/src/instruction/pumpfun_ix_data.rs +++ b/src/instruction/pumpfun_ix_data.rs @@ -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` = 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 } diff --git a/src/instruction/utils/pumpfun.rs b/src/instruction/utils/pumpfun.rs index 0a9e787..f2da8f9 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -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::(&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] diff --git a/src/trading/core/params/dex_swap.rs b/src/trading/core/params/dex_swap.rs index f256b5b..a57d4fd 100644 --- a/src/trading/core/params/dex_swap.rs +++ b/src/trading/core/params/dex_swap.rs @@ -97,6 +97,10 @@ pub struct SwapParams { /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input. /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes. pub use_exact_sol_amount: Option, + /// Use PumpFun V2 instructions (buy_v2 / sell_v2 / buy_exact_quote_in_v2, 27-account metas, quote_mint support). + /// Default: `false` — uses V1 instructions (18-account metas, legacy SOL-paired). + /// Set to `true` when PumpFun officially deploys V2 on mainnet. Until then, keep `false` for smaller transaction size. + pub use_pumpfun_v2: bool, } impl SwapParams {