diff --git a/Cargo.toml b/Cargo.toml index c1f66d9..cbb6db2 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.19" +version = "4.0.20" edition = "2021" authors = [ "William ", diff --git a/README.md b/README.md index 784d9b1..c1d26a8 100644 --- a/README.md +++ b/README.md @@ -83,11 +83,11 @@ This SDK is available in multiple languages: **Rust crate:** `sol-trade-sdk = "4.0.17"` -This release refreshes PumpFun V2 WSOL quote-pool handling, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data. +This release refreshes PumpFun native-SOL quote handling so SOL/WSOL sentinels prefer the smaller V1 hot path, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data. ## ✨ Features -1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, selecting legacy or V2 on-chain instructions as needed (SOL/WSOL + USDC) +1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, preferring V1 for native SOL and selecting V2 for USDC/non-native quote mints or explicit WSOL settlement 2. **PumpSwap Trading**: Support for PumpSwap pool trading operations 3. **Bonk Trading**: Support for Bonk trading operations 4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations @@ -426,15 +426,15 @@ PumpFun has two instruction sets for bonding-curve trading: |---|---|---| | 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) | +| Quote mint | Native SOL (`default`, Solscan SOL sentinel, or WSOL sentinel) | Non-native quote mint, or explicit WSOL settlement | +| Transaction size | Smaller (preferred hot path) | Larger (may require LUT for nonce/tip/ATA-heavy transactions) | -The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` selects the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required. +The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` plus the requested settlement token (`pay_with` / `receive_as`) select the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required. -**Default: V1**. When `quote_mint` is `Pubkey::default()` or the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), 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. Passing `WSOL_TOKEN_ACCOUNT` selects SOL V2; passing USDC selects USDC V2. +**Default: V1**. When `quote_mint` is `Pubkey::default()`, the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), or `WSOL_TOKEN_ACCOUNT` (`So11111111111111111111111111111111111111112`), the SDK treats the curve as native SOL-paired and uses V1 instructions when `pay_with` / `receive_as` is `SOL`. This is the preferred hot path because it avoids the 27-account V2 layout. Passing USDC or another real quote mint selects V2. Passing `WSOL` as the buy input or sell output selects V2 only when you intentionally want to settle through an existing WSOL ATA. **Key changes in v2 instructions:** -- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired +- `quote_mint` parameter — native SOL-paired curves may appear as default, Solscan SOL, or WSOL; USDC/non-native quote mints select V2 - 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 @@ -442,13 +442,12 @@ The SDK-side builder is version-neutral: callers use the normal buy/sell flow, a **Pass `quote_mint` into `PumpFunParams::from_trade`**: -When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()` and Solscan SOL (`So11111111111111111111111111111111111111111`) mean the legacy SOL layout; `WSOL_TOKEN_ACCOUNT` means SOL V2; USDC means USDC V2. +When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()`, Solscan SOL (`So11111111111111111111111111111111111111111`), and `WSOL_TOKEN_ACCOUNT` all mean a native SOL-paired curve and default to V1 for normal SOL settlement. USDC means USDC V2. ```rust // quote_mint is not a PDA. It is the quote SPL mint carried by parser/gRPC events: -// - Legacy SOL pool: Pubkey::default() or Solscan SOL from parser data -// - SOL V2 pool: WSOL_TOKEN_ACCOUNT -// - USDC V2 pool: USDC mint +// - Native SOL pool: Pubkey::default(), Solscan SOL, or WSOL sentinel from parser data +// - USDC/non-native pool: actual quote SPL mint let quote_mint = e.quote_mint; let params = PumpFunParams::from_trade( e.bonding_curve, @@ -469,11 +468,11 @@ let params = PumpFunParams::from_trade( ); ``` -For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves. +For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves. For SOL-paired curves, use `SOL` for the normal fast path; use `WSOL` only if you intentionally want V2 settlement through an existing WSOL ATA. When consuming parser events, map `quoteMint`, `virtualQuoteReserves`, and `realQuoteReserves` into `PumpFunParams::from_trade(...)`; USDC pools use `4_292_000_000` as the initial virtual quote reserve. For legacy SOL events where `quote_mint` is `Pubkey::default()` or Solscan SOL, use `virtual_sol_reserves` / `real_sol_reserves` when the quote-reserve fields are absent or zero. -> **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. +> **Note**: V2 transactions with ATA creation + durable nonce/tip may exceed `PACKET_DATA_SIZE`. The SDK reports this locally and does not remove compute-budget or tip instructions because that changes priority semantics. Use V1 when the curve is native SOL-paired, pre-create ATAs, or enable an Address Lookup Table (`address_lookup_table_account`) when using V2. #### PumpSwap: coin_creator_vault from events (no RPC) diff --git a/README_CN.md b/README_CN.md index 7de525a..64020fb 100755 --- a/README_CN.md +++ b/README_CN.md @@ -83,11 +83,11 @@ **Rust crate:** `sol-trade-sdk = "4.0.17"` -本版本刷新 PumpFun V2 WSOL quote 池处理逻辑,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。 +本版本刷新 PumpFun native SOL quote 处理逻辑,SOL/WSOL sentinel 默认优先走更小的 V1 热路径,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。 ## ✨ 项目特性 -1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,内部按需选择旧版或 V2 链上指令(SOL/WSOL + USDC) +1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,native SOL 优先走 V1,USDC/非 SOL quote 或显式 WSOL 结算才走 V2 2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作 3. **Bonk 交易**: 支持 Bonk 的交易操作 4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作 @@ -428,22 +428,21 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟) Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。 -SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 自动选择正确的链上 discriminator 和账户布局。 +SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 和买/卖的结算 mint 自动选择正确的链上 discriminator 和账户布局。能用 V1 的 native SOL 池会优先用 V1。 **v2 指令关键变化:** -- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint +- 新增 `quote_mint` 参数 — native SOL 配对可能表现为默认值、Solscan SOL sentinel(`So11111111111111111111111111111111111111111`)或 WSOL sentinel(`So11111111111111111111111111111111111111112`);USDC/其他真实 quote mint 才选择 V2 - 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户** - `buyback_fee_recipient`、`sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户 - SOL 配对币的报价和成本与旧版一致,无额外开销 **使用方式:** -把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint;`Pubkey::default()` 和 Solscan SOL(`So11111111111111111111111111111111111111111`)表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2,USDC 表示 USDC V2: +把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint 或 native SOL sentinel;`Pubkey::default()`、Solscan SOL(`So11111111111111111111111111111111111111111`)和 `WSOL_TOKEN_ACCOUNT` 都表示 native SOL 配对,正常用 SOL 结算时默认走旧版 V1;USDC 表示 USDC V2: ```rust -// legacy SOL 池:log 事件里可能是 Pubkey::default(),parser 数据里是 Solscan SOL sentinel -// SOL V2 池:WSOL_TOKEN_ACCOUNT -// USDC 池:就是 USDC mint +// native SOL 池:可能是 Pubkey::default()、Solscan SOL sentinel 或 WSOL sentinel +// USDC / 非 SOL 池:就是实际 quote SPL mint let quote_mint = e.quote_mint; let params = PumpFunParams::from_trade( @@ -469,14 +468,14 @@ client.buy(buy_params).await?; client.sell(sell_params).await?; ``` -USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。 +USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SOL 配对的普通热路径请传 `SOL`,SDK 会用 V1;只有你明确传 `WSOL` 作为买入输入或卖出输出、希望通过已有 WSOL ATA 结算时,才会选择 V2。 +SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。 消费 parser 事件时,需要把 `quoteMint`、`virtualQuoteReserves`、`realQuoteReserves` 传进 `PumpFunParams::from_trade(...)`;USDC 池初始虚拟 quote reserve 是 `4_292_000_000`。 legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quote reserve 字段缺失/为 0,应回退使用 `virtual_sol_reserves` / `real_sol_reserves`。 | quote_mint | 实际使用的指令 | 说明 | |-----------|---------|------| -| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL | -| `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 | +| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) / `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | 优先旧版 `buy`/`sell`/`buy_exact_sol_in` | native SOL 配对;普通 SOL 结算走 V1,显式 WSOL 结算才走 V2 | | `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) | ## 🛡️ MEV 保护服务 diff --git a/src/client/mod.rs b/src/client/mod.rs index bcdc2f2..6bb8319 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -173,9 +173,10 @@ pub struct SimpleBuyParams { pub dex_type: DexType, /// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`. /// - /// For PumpFun V2 SOL-paired coins, use `TradeTokenType::SOL` even when the - /// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote - /// account but keeps settlement in native SOL. + /// For PumpFun SOL-paired coins, use `TradeTokenType::SOL` for the normal + /// fast path even when parser data reports WSOL as the quote sentinel. The + /// SDK will prefer V1; choose `WSOL` only when intentionally spending an + /// existing WSOL ATA through V2. pub pay_with: TradeTokenType, /// Mint address of the token being bought. pub mint: Pubkey, diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 772f1d6..8f35f36 100644 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -1,10 +1,11 @@ //! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)). //! //! The SDK selects the legacy or V2 on-chain layout from `PumpFunParams.quote_mint`. -//! `Pubkey::default()` and the Solscan SOL sentinel keep the smaller legacy SOL layout; -//! explicit WSOL or USDC uses the V2 27/26-account unified metas. -//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency. +//! Native SOL-paired coins (`Pubkey::default()`, Solscan SOL sentinel, or WSOL sentinel) +//! keep the smaller legacy SOL layout by default. Non-native quote mints such as USDC use +//! the V2 27/26-account unified metas. +use crate::swqos::TradeType; use crate::{ common::bonding_curve::BondingCurveAccount, common::spl_token::close_account, @@ -52,8 +53,8 @@ fn effective_pump_mint_token_program(protocol_params: &PumpFunParams, mint: &Pub } /// Resolve quote mint and its token program from PumpFunParams. -/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` means legacy SOL-paired; use WSOL mint when a -/// downstream V2 helper needs a concrete SPL quote mint. +/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` / `WSOL_TOKEN_ACCOUNT` are native SOL-paired. +/// V2 helpers still need a concrete SPL mint, so native SOL resolves to WSOL internally. #[inline] fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) { let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint(); @@ -73,6 +74,16 @@ fn is_sol_quote_mint(quote_mint: &Pubkey) -> bool { || *quote_mint == crate::constants::SOL_TOKEN_ACCOUNT } +#[inline] +fn is_native_sol_settlement_mint(mint: &Pubkey) -> bool { + *mint == crate::constants::SOL_TOKEN_ACCOUNT || *mint == Pubkey::default() +} + +#[inline] +fn is_explicit_wsol_settlement_mint(mint: &Pubkey) -> bool { + *mint == crate::constants::WSOL_TOKEN_ACCOUNT +} + #[inline] fn validate_v2_buy_quote_mint(input_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> { if is_sol_quote_mint(quote_mint) { @@ -130,9 +141,20 @@ fn should_use_v2_layout(params: &SwapParams) -> Result { .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; let (quote_mint, _) = effective_quote_mint_and_token_program(protocol_params); - let explicit_v2_quote = protocol_params.quote_mint != Pubkey::default() - && protocol_params.quote_mint != crate::constants::SOL_TOKEN_ACCOUNT; - Ok(explicit_v2_quote || !is_sol_quote_mint("e_mint)) + if !is_sol_quote_mint("e_mint) { + return Ok(true); + } + + // Pump docs treat WSOL quote mint as the native SOL sentinel for SOL-paired curves. + // Keep V1 for native SOL settlement; use V2 only when the caller explicitly wants to + // spend/receive an existing WSOL ATA. + Ok(match params.trade_type { + TradeType::Buy | TradeType::CreateAndBuy => { + is_explicit_wsol_settlement_mint(¶ms.input_mint) + } + TradeType::Sell => is_explicit_wsol_settlement_mint(¶ms.output_mint), + TradeType::Create => false, + }) } #[inline] @@ -171,6 +193,13 @@ fn build_buy_legacy(params: &SwapParams) -> Result> { .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + if !is_native_sol_settlement_mint(¶ms.input_mint) { + return Err(anyhow!( + "PumpFun native SOL buy expects input_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL input only when spending an existing WSOL ATA.", + params.input_mint + )); + } + let lamports_in = params.input_amount.unwrap_or(0); if lamports_in == 0 { return Err(anyhow!("Amount cannot be zero")); @@ -299,6 +328,13 @@ fn build_sell_legacy(params: &SwapParams) -> Result> { .downcast_ref::() .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + if !is_native_sol_settlement_mint(¶ms.output_mint) { + return Err(anyhow!( + "PumpFun native SOL sell expects output_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL output only when receiving into an existing WSOL ATA.", + params.output_mint + )); + } + let token_amount = if let Some(amount) = params.input_amount { if amount == 0 { return Err(anyhow!("Amount cannot be zero")); @@ -1002,14 +1038,15 @@ mod tests { } #[test] - fn pumpfun_v2_fixed_output_uses_buy_with_max_input_budget() { + fn pumpfun_v2_usdc_fixed_output_uses_buy_with_max_input_budget() { let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); params.create_output_mint_ata = false; + params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT; params.fixed_output_amount = Some(42); params.use_exact_sol_amount = Some(true); if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { *protocol_params = - protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT); } let instructions = build_buy(¶ms).unwrap(); @@ -1024,7 +1061,7 @@ mod tests { } #[test] - fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() { + fn pumpfun_wsol_quote_regular_buy_uses_v1_native_sol() { let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); params.create_output_mint_ata = false; params.create_input_mint_ata = true; @@ -1043,13 +1080,13 @@ mod tests { params.slippage_basis_points.unwrap(), ); - assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR); + assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR); assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected); - assert_eq!(buy_ix.accounts.len(), 27); + assert_eq!(buy_ix.accounts.len(), 18); } #[test] - fn pumpfun_v2_regular_buy_skips_wsol_ata_create_on_hot_path() { + fn pumpfun_wsol_quote_regular_buy_skips_wsol_ata_create_on_hot_path() { let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); params.create_output_mint_ata = false; params.create_input_mint_ata = true; @@ -1065,13 +1102,15 @@ mod tests { assert_eq!(instructions.len(), 1); assert_eq!( &instructions.last().unwrap().data[..8], - crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR + crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR ); } #[test] - fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() { + fn pumpfun_v2_explicit_wsol_input_with_output_ata_create_reports_oversized_without_dropping_priority( + ) { let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); + params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT; params.create_input_mint_ata = true; params.create_output_mint_ata = true; params.use_exact_sol_amount = Some(false); @@ -1080,6 +1119,41 @@ mod tests { protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); } + let business_instructions = build_buy(¶ms).unwrap(); + let err = crate::trading::common::transaction_builder::build_transaction( + ¶ms.payer, + 150_000, + 500_000, + &business_instructions, + None, + Some(solana_hash::Hash::new_unique()), + None, + "PumpFun", + true, + true, + &Pubkey::new_unique(), + 0.001, + None, + ) + .unwrap_err() + .to_string(); + + assert!(err.contains("transaction too large"), "{err}"); + assert!(err.contains("did not remove compute budget or relay tip"), "{err}"); + } + + #[test] + fn pumpfun_v2_explicit_wsol_input_hot_path_transaction_fits_when_output_ata_prepared() { + let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); + params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT; + params.create_input_mint_ata = true; + params.create_output_mint_ata = false; + params.use_exact_sol_amount = Some(false); + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + } + let business_instructions = build_buy(¶ms).unwrap(); let transaction = crate::trading::common::transaction_builder::build_transaction( ¶ms.payer, @@ -1107,7 +1181,7 @@ mod tests { } #[test] - fn pumpfun_from_trade_wsol_quote_regular_buy_selects_buy_v2() { + fn pumpfun_from_trade_wsol_quote_regular_buy_selects_v1() { let mint = pump_mint(); let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM); params.create_output_mint_ata = false; @@ -1135,8 +1209,8 @@ mod tests { let instructions = build_buy(¶ms).unwrap(); let buy_ix = instructions.last().unwrap(); - assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR); - assert_eq!(buy_ix.accounts.len(), 27); + assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR); + assert_eq!(buy_ix.accounts.len(), 18); } #[test] @@ -1161,6 +1235,44 @@ mod tests { assert_eq!(instructions[0].accounts.len(), 27); } + #[test] + fn pumpfun_sell_does_not_select_v2_from_base_mint_wsol() { + let mut params = swap_params_for_buy(crate::constants::WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM); + params.trade_type = crate::swqos::TradeType::Sell; + params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT; + params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT; + params.create_output_mint_ata = false; + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + } + + let instructions = build_sell(¶ms).unwrap(); + let ix = instructions.last().unwrap(); + + assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR); + } + + #[test] + fn pumpfun_sell_selects_v2_for_explicit_wsol_output_settlement() { + let mint = pump_mint(); + let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM); + params.trade_type = crate::swqos::TradeType::Sell; + params.input_mint = mint; + params.output_mint = crate::constants::WSOL_TOKEN_ACCOUNT; + params.create_output_mint_ata = false; + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + } + + let instructions = build_sell(¶ms).unwrap(); + let ix = instructions.last().unwrap(); + + assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR); + assert_eq!(ix.accounts.len(), 26); + } + #[test] fn pumpfun_usdc_dev_trade_uses_usdc_initial_quote_reserves() { let mint = pump_mint(); @@ -1372,13 +1484,14 @@ mod tests { } #[test] - fn pumpfun_v2_buyback_fee_recipient_is_writable() { + fn pumpfun_v2_usdc_buyback_fee_recipient_is_writable() { let mint = pump_mint(); let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM); params.create_output_mint_ata = false; + params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT; if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { *protocol_params = - protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT); } let buy_ix = build_buy(¶ms).unwrap().pop().unwrap(); @@ -1387,7 +1500,7 @@ mod tests { params.trade_type = crate::swqos::TradeType::Sell; params.input_mint = mint; - params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT; + params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT; let sell_ix = build_sell(¶ms).unwrap().pop().unwrap(); assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey)); assert!(sell_ix.accounts[8].is_writable); @@ -1416,17 +1529,17 @@ mod tests { } #[test] - fn pumpfun_v2_sell_fixed_output_uses_min_sol_directly() { + fn pumpfun_v2_usdc_sell_fixed_output_uses_min_quote_directly() { let mint = pump_mint(); let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM); params.trade_type = crate::swqos::TradeType::Sell; params.input_mint = mint; - params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT; + params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT; params.create_output_mint_ata = false; params.fixed_output_amount = Some(42); if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { *protocol_params = - protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT); } let instructions = build_sell(¶ms).unwrap(); diff --git a/src/trading/common/transaction_builder.rs b/src/trading/common/transaction_builder.rs index 82aecca..cf83ee6 100755 --- a/src/trading/common/transaction_builder.rs +++ b/src/trading/common/transaction_builder.rs @@ -46,7 +46,7 @@ pub fn build_transaction( tip_amount: f64, durable_nonce: Option<&DurableNonceInfo>, ) -> Result { - let transaction = build_transaction_with_compute_budget( + let transaction = build_transaction_inner( payer, unit_limit, unit_price, @@ -63,44 +63,32 @@ pub fn build_transaction( )?; let serialized_len = bincode::serialized_size(&transaction)? as usize; - if serialized_len <= PACKET_DATA_SIZE - || durable_nonce.is_some() - || !with_tip - || tip_amount <= 0.0 - || (unit_limit == 0 && unit_price == 0) - { + if crate::common::sdk_log::sdk_log_enabled() { + println!( + " [SDK][tx-size ] {} {} serialized={} bytes, business_ix={}, nonce={}, tip={}, cu_limit={}, cu_price={}, alt={}", + protocol_name, + if is_buy { "buy" } else { "sell" }, + serialized_len, + business_instructions.len(), + durable_nonce.is_some(), + with_tip && tip_amount > 0.0, + unit_limit, + unit_price, + address_lookup_table_account.is_some() + ); + } + if serialized_len <= PACKET_DATA_SIZE { return Ok(transaction); } - let compact_transaction = build_transaction_with_compute_budget( - payer, - 0, - 0, - business_instructions, - address_lookup_table_account, - recent_blockhash, - middleware_manager, - protocol_name, - is_buy, - with_tip, - tip_account, - tip_amount, - durable_nonce, - )?; - let compact_len = bincode::serialized_size(&compact_transaction)? as usize; - if compact_len <= PACKET_DATA_SIZE { - return Ok(compact_transaction); - } - Err(anyhow!( - "transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting", + "transaction too large: {} > {}; SDK did not remove compute budget or relay tip because that changes transaction priority semantics. Use an address lookup table or pre-create token ATAs before submitting", serialized_len, - PACKET_DATA_SIZE, - compact_len + PACKET_DATA_SIZE )) } -fn build_transaction_with_compute_budget( +fn build_transaction_inner( payer: &Arc, unit_limit: u32, unit_price: u64, @@ -181,3 +169,41 @@ fn build_versioned_transaction( Ok(tx) } + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::instruction::AccountMeta; + + fn oversized_instruction(account_count: usize, data_len: usize) -> Instruction { + let accounts = + (0..account_count).map(|_| AccountMeta::new(Pubkey::new_unique(), false)).collect(); + Instruction { program_id: Pubkey::new_unique(), accounts, data: vec![7; data_len] } + } + + #[test] + fn oversized_transaction_returns_error_without_dropping_priority_semantics() { + let payer = Arc::new(Keypair::new()); + let business_instructions = vec![oversized_instruction(36, 700)]; + let err = build_transaction( + &payer, + 80_000, + 100_000, + &business_instructions, + None, + Some(Hash::new_unique()), + None, + "test", + true, + true, + &Pubkey::new_unique(), + 0.001, + None, + ) + .unwrap_err() + .to_string(); + + assert!(err.contains("transaction too large"), "{err}"); + assert!(err.contains("did not remove compute budget or relay tip"), "{err}"); + } +} diff --git a/src/trading/core/params/pumpfun.rs b/src/trading/core/params/pumpfun.rs index 5b90cbb..52568f8 100644 --- a/src/trading/core/params/pumpfun.rs +++ b/src/trading/core/params/pumpfun.rs @@ -14,9 +14,10 @@ use std::sync::Arc; /// **Buy/sell**:`creator_vault` 及(若可得)**`tradeEvent` / CPI 日志中的 `creator`** 优先于陈旧的曲线快照; /// ix 组装与链下询价见 [`Self::effective_creator_for_trade`]、[`crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing`]。 /// -/// **V2 instructions**: The SDK selects the layout automatically from `quote_mint`. -/// Leave `quote_mint` default, or pass the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), for -/// legacy SOL layout. Pass `WSOL_TOKEN_ACCOUNT` for SOL V2, or USDC for USDC-paired coins. +/// **Instruction layout**: The SDK selects the smallest valid layout automatically from +/// `quote_mint`. Native SOL-paired coins use the legacy SOL layout when `quote_mint` +/// is default, the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), or the WSOL sentinel +/// (`WSOL_TOKEN_ACCOUNT`). Non-native quote mints such as USDC use V2. #[derive(Clone)] pub struct PumpFunParams { pub bonding_curve: Arc, @@ -41,8 +42,9 @@ pub struct PumpFunParams { /// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。 /// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值。 pub fee_recipient: Pubkey, - /// Quote mint layout selector. Default and `SOL_TOKEN_ACCOUNT` use legacy SOL layout. - /// `WSOL_TOKEN_ACCOUNT` selects SOL v2; USDC selects USDC v2. + /// Quote mint layout selector. Default, `SOL_TOKEN_ACCOUNT`, and `WSOL_TOKEN_ACCOUNT` + /// are native SOL-paired and use the smaller legacy SOL layout by default. + /// USDC and other non-native quote mints select V2. /// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`. pub quote_mint: Pubkey, } @@ -50,7 +52,10 @@ pub struct PumpFunParams { impl PumpFunParams { #[inline] fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey { - if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT { + if quote_mint == Pubkey::default() + || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT + || quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT + { Pubkey::default() } else { BondingCurveAccount::normalize_quote_mint(quote_mint) @@ -59,7 +64,10 @@ impl PumpFunParams { #[inline] fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey { - if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT { + if quote_mint == Pubkey::default() + || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT + || quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT + { crate::constants::SOL_TOKEN_ACCOUNT } else { BondingCurveAccount::normalize_quote_mint(quote_mint) @@ -176,8 +184,8 @@ impl PumpFunParams { } /// Build PumpFun params from event/parser data. Pass `quote_mint` from the event: - /// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` for legacy SOL layout, - /// `WSOL_TOKEN_ACCOUNT` for SOL V2, and `USDC_TOKEN_ACCOUNT` for USDC V2. + /// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` / `WSOL_TOKEN_ACCOUNT` for native SOL + /// layout, and `USDC_TOKEN_ACCOUNT` or another non-native quote mint for V2. /// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts. /// /// `mayhem_mode`: @@ -369,8 +377,9 @@ impl PumpFunParams { } /// Sets `quote_mint`. The instruction builder derives V1/V2 layout from this value. - /// For legacy SOL-paired coins, pass `SOL_TOKEN_ACCOUNT` or leave default. - /// Pass `WSOL_TOKEN_ACCOUNT` only when you intentionally want SOL v2 layout. + /// For native SOL-paired coins, pass `Pubkey::default()`, `SOL_TOKEN_ACCOUNT`, or + /// `WSOL_TOKEN_ACCOUNT`; all three select the smaller V1 layout unless buy/sell params + /// explicitly request WSOL settlement. Pass USDC or another non-native quote mint for V2. #[inline] pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self { let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint); @@ -419,6 +428,10 @@ mod tests { PumpFunParams::quote_mint_for_rpc_return(crate::constants::SOL_TOKEN_ACCOUNT), crate::constants::SOL_TOKEN_ACCOUNT ); + assert_eq!( + PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT), + crate::constants::SOL_TOKEN_ACCOUNT + ); } #[test] @@ -427,9 +440,22 @@ mod tests { PumpFunParams::quote_mint_for_rpc_return(crate::constants::USDC_TOKEN_ACCOUNT), crate::constants::USDC_TOKEN_ACCOUNT ); + } + + #[test] + fn quote_mint_for_layout_normalizes_native_sol_sentinels_to_default() { + assert_eq!(PumpFunParams::quote_mint_for_layout(Pubkey::default()), Pubkey::default()); assert_eq!( - PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT), - crate::constants::WSOL_TOKEN_ACCOUNT + PumpFunParams::quote_mint_for_layout(crate::constants::SOL_TOKEN_ACCOUNT), + Pubkey::default() + ); + assert_eq!( + PumpFunParams::quote_mint_for_layout(crate::constants::WSOL_TOKEN_ACCOUNT), + Pubkey::default() + ); + assert_eq!( + PumpFunParams::quote_mint_for_layout(crate::constants::USDC_TOKEN_ACCOUNT), + crate::constants::USDC_TOKEN_ACCOUNT ); } }