From b973061f922771ae7c721898fee368e31835f294 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Mon, 25 May 2026 01:47:48 +0800 Subject: [PATCH] Update DEX protocol support for 4.0.13 --- Cargo.toml | 2 +- README.md | 65 ++-- README_CN.md | 65 ++-- examples/address_lookup/Cargo.toml | 2 +- examples/address_lookup/src/main.rs | 15 +- examples/nonce_cache/Cargo.toml | 2 +- examples/nonce_cache/src/main.rs | 15 +- examples/pumpfun_copy_trading/Cargo.toml | 2 +- examples/pumpfun_copy_trading/src/main.rs | 17 +- examples/pumpfun_sniper_trading/Cargo.toml | 2 +- examples/pumpfun_sniper_trading/src/main.rs | 12 +- src/client/mod.rs | 10 +- src/common/bonding_curve.rs | 113 +++++- src/common/types.rs | 14 - src/instruction/bonk.rs | 1 - src/instruction/meteora_damm_v2.rs | 1 - src/instruction/pumpfun.rs | 364 +++++++++++++++++--- src/instruction/pumpfun_ix_data.rs | 134 ++++--- src/instruction/pumpswap.rs | 1 - src/instruction/raydium_amm_v4.rs | 1 - src/instruction/raydium_cpmm.rs | 1 - src/instruction/utils/pumpfun.rs | 1 + src/trading/core/params/dex_swap.rs | 5 +- src/trading/core/params/pumpfun.rs | 145 ++++++-- 24 files changed, 745 insertions(+), 245 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5fffc22..f945b35 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "4.0.12" +version = "4.0.13" edition = "2021" authors = [ "William ", diff --git a/README.md b/README.md index b774840..528097d 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ This SDK is available in multiple languages: ## ✨ Features -1. **PumpFun Trading**: Support for `buy`, `sell`, `buy_exact_sol_in`, and the new unified `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` instructions (SOL + USDC) +1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, selecting legacy or V2 on-chain instructions as needed (SOL + USDC) 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 @@ -108,14 +108,14 @@ Add the dependency to your `Cargo.toml`: ```toml # Add to your Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.13" } ``` ### Use crates.io ```toml # Add to your Cargo.toml -sol-trade-sdk = "4.0.9" +sol-trade-sdk = "4.0.13" ``` ## 🛠️ Usage Examples @@ -152,7 +152,6 @@ 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(true) // PumpFun V2 (27 accounts, quote_mint); required for USDC-paired PumpFun coins .build(); // Create TradingClient @@ -335,7 +334,7 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi - **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step. - **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK: - - **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `is_cashback_coin`). + - **PumpFun**: `PumpFunParams::from_trade(..., mint, quote_mint, creator, ..., is_cashback_coin, mayhem_mode)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `is_cashback_coin`). - **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data. - The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params. - **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback. @@ -355,7 +354,7 @@ 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. -#### PumpFun V1 vs V2 Instructions +#### PumpFun Unified Buy/Sell With V1/V2 Instructions PumpFun has two instruction sets for bonding-curve trading: @@ -366,42 +365,48 @@ PumpFun has two instruction sets for bonding-curve trading: | 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) | -**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. +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. + +**Default: V1**. When `quote_mint` is `Pubkey::default()`, 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 an explicit quote mint selects V2: WSOL/native SOL for SOL-paired V2, or USDC for USDC-paired pools. **Key changes in v2 instructions:** - `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 +- USDC-paired coins must be bought with USDC and sell back to USDC. The SDK rejects SOL input for USDC quote pools before transaction submission. -**How to enable V2:** +**Pass `quote_mint` into `PumpFunParams::from_trade`**: -**Method 1 — Global runtime flag** (recommended for `TradingClient`; required for USDC-paired coins): +When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()` means the legacy SOL layout; explicit native SOL or WSOL means the SOL V2 layout; USDC means the USDC V2 layout. ```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(); +// quote_mint is not a PDA. It is the quote SPL mint carried by parser/gRPC events: +// - Legacy SOL pool: Pubkey::default() from log events +// - SOL V2 pool: native SOL or WSOL from parser data +// - USDC V2 pool: USDC mint +let quote_mint = e.quote_mint; +let params = PumpFunParams::from_trade( + e.bonding_curve, + e.associated_bonding_curve, + e.mint, + quote_mint, + e.creator, + e.creator_vault, + e.virtual_token_reserves, + e.virtual_quote_reserves, + e.real_token_reserves, + e.real_quote_reserves, + close_token_account_when_sell, + e.fee_recipient, + e.token_program, + e.is_cashback_coin, + Some(e.mayhem_mode), +); ``` -**Method 2 — Set the quote mint on `PumpFunParams`**: - -When using the high-level `TradingClient`, keep `.use_pumpfun_v2(true)` enabled in `TradeConfig` and set `quote_mint` on the PumpFun params to select the pair: - -```rust -use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT; -use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT; - -// SOL-paired coin with v2 layout -let params = PumpFunParams::from_trade(/* ... */) - .with_quote_mint(WSOL_TOKEN_ACCOUNT); - -// USDC-paired coin (requires v2) -let params = PumpFunParams::from_trade(/* ... */) - .with_quote_mint(USDC_TOKEN_ACCOUNT); -``` - -`with_quote_mint(...)` also marks the params as V2-capable for lower-level instruction builders, but the high-level `TradingClient` uses the client-level `use_pumpfun_v2` runtime flag when choosing V1 vs V2. +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. +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. > **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. diff --git a/README_CN.md b/README_CN.md index b9333a7..c68cfc3 100755 --- a/README_CN.md +++ b/README_CN.md @@ -81,7 +81,7 @@ ## ✨ 项目特性 -1. **PumpFun 交易**: 支持 `buy`、`sell`、`buy_exact_sol_in` 以及全新的统一化 `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` 指令(SOL + USDC) +1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,内部按需选择旧版或 V2 链上指令(SOL + USDC) 2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作 3. **Bonk 交易**: 支持 Bonk 的交易操作 4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作 @@ -108,14 +108,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.9" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.13" } ``` ### 使用 crates.io ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = "4.0.9" +sol-trade-sdk = "4.0.13" ``` ## 🛠️ 使用示例 @@ -152,7 +152,6 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) // .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS // .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心 // .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor) - // .use_pumpfun_v2(true) // PumpFun V2(27 个账户,quote_mint);USDC 配对 PumpFun 币必须开启 .build(); // 创建 TradingClient @@ -334,7 +333,7 @@ PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还 - **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc` 或 `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。 - **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK: - - **PumpFun**:`PumpFunParams::from_trade(..., is_cashback_coin)` 与 `PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。 + - **PumpFun**:`PumpFunParams::from_trade(..., mint, quote_mint, creator, ..., is_cashback_coin, mayhem_mode)` 与 `PumpFunParams::from_dev_trade(..., is_cashback_coin)` 都需要传入 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。 - **PumpSwap**:`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。 - **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。 - **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。 @@ -361,10 +360,12 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟) - **sol-parser-sdk**:指令解析从账户 17、18 写入;若事件来自日志,账户填充器也会从指令补全。用 `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` 即可。 - **solana-streamer**:指令解析从 `accounts.get(17)`、`accounts.get(18)` 写入。同样用事件的 `coin_creator_vault_ata`、`coin_creator_vault_authority` 调用 `from_trade`。 -### Pump.fun Bonding Curve v2(buy_v2 / sell_v2 / buy_exact_quote_in_v2) +### Pump.fun Bonding Curve 统一买卖入口与 v2 指令 Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。 +SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 自动选择正确的链上 discriminator 和账户布局。 + **v2 指令关键变化:** - 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint - 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户** @@ -373,40 +374,44 @@ Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过 **使用方式:** -高层 `TradingClient` 需要先在 `TradeConfig` 开启 PumpFun V2;USDC 配对币必须开启: +把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint;`Pubkey::default()` 表示旧版 SOL 布局,显式 native SOL/WSOL 表示 SOL V2,USDC 表示 USDC V2: ```rust -let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) - .use_pumpfun_v2(true) - .build(); -``` +// SOL 池:log 事件里通常是 Pubkey::default(),parser 数据里可能是 native SOL/WSOL +// USDC 池:就是 USDC mint +let quote_mint = e.quote_mint; -然后在 `PumpFunParams` 设置 `quote_mint` 来选择 SOL/USDC 配对: - -```rust -use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT; -use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT; - -// SOL 配对币 — 传包装 SOL mint -let params = PumpFunParams::from_trade(/* ... */) - .with_quote_mint(WSOL_TOKEN_ACCOUNT); - -// USDC 配对币(即将开放 — 必须使用 v2) -let params = PumpFunParams::from_trade(/* ... */) - .with_quote_mint(USDC_TOKEN_ACCOUNT); +let params = PumpFunParams::from_trade( + e.bonding_curve, + e.associated_bonding_curve, + e.mint, + quote_mint, + e.creator, + e.creator_vault, + e.virtual_token_reserves, + e.virtual_quote_reserves, + e.real_token_reserves, + e.real_quote_reserves, + close_token_account_when_sell, + e.fee_recipient, + e.token_program, + e.is_cashback_coin, + Some(e.mayhem_mode), +); // 之后正常交易 client.buy(buy_params).await?; client.sell(sell_params).await?; ``` -`with_quote_mint(...)` 会把参数标记为可使用 V2,底层 instruction builder 可据此走 V2;但高层 `TradingClient` 选择 V1/V2 时使用的是 client 级别的 `use_pumpfun_v2` 运行时开关。 +USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。 +消费 parser 事件时,需要把 `quoteMint`、`virtualQuoteReserves`、`realQuoteReserves` 传进 `PumpFunParams::from_trade(...)`;USDC 池初始虚拟 quote reserve 是 `4_292_000_000`。 -| quote_mint | use_v2_ix | 实际使用的指令 | 说明 | -|-----------|-------------|---------|------| -| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL | -| `WSOL_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 | -| `USDC_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) | +| quote_mint | 实际使用的指令 | 说明 | +|-----------|---------|------| +| 未设置(默认) | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL | +| `WSOL_TOKEN_ACCOUNT` / native SOL | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 | +| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) | ## 🛡️ MEV 保护服务 diff --git a/examples/address_lookup/Cargo.toml b/examples/address_lookup/Cargo.toml index b062cbe..602b512 100644 --- a/examples/address_lookup/Cargo.toml +++ b/examples/address_lookup/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sol-trade-sdk = { path = "../.." } -sol-parser-sdk = "0.2.2" +sol-parser-sdk = "0.4.14" solana-sdk = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } tokio = { version = "1", features = ["full"] } diff --git a/examples/address_lookup/src/main.rs b/examples/address_lookup/src/main.rs index 87060a4..a31e2c2 100644 --- a/examples/address_lookup/src/main.rs +++ b/examples/address_lookup/src/main.rs @@ -130,6 +130,16 @@ async fn pumpfun_copy_trade_with_grpc( let client = create_solana_trade_client().await?; let mint_pubkey = trade_info.mint; + let virtual_quote_reserves = if trade_info.virtual_quote_reserves != 0 { + trade_info.virtual_quote_reserves + } else { + trade_info.virtual_sol_reserves + }; + let real_quote_reserves = if trade_info.virtual_quote_reserves != 0 { + trade_info.real_quote_reserves + } else { + trade_info.real_sol_reserves + }; let slippage_basis_points = Some(100); let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; @@ -154,12 +164,13 @@ async fn pumpfun_copy_trade_with_grpc( trade_info.bonding_curve, trade_info.associated_bonding_curve, trade_info.mint, + trade_info.quote_mint, trade_info.creator, trade_info.creator_vault, trade_info.virtual_token_reserves, - trade_info.virtual_sol_reserves, + virtual_quote_reserves, trade_info.real_token_reserves, - trade_info.real_sol_reserves, + real_quote_reserves, None, trade_info.fee_recipient, trade_info.token_program, diff --git a/examples/nonce_cache/Cargo.toml b/examples/nonce_cache/Cargo.toml index 3a47599..00b4730 100644 --- a/examples/nonce_cache/Cargo.toml +++ b/examples/nonce_cache/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sol-trade-sdk = { path = "../.." } -sol-parser-sdk = "0.2.2" +sol-parser-sdk = "0.4.14" solana-sdk = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } spl-associated-token-account = "7.0.0" diff --git a/examples/nonce_cache/src/main.rs b/examples/nonce_cache/src/main.rs index e6dc33f..8ddd81a 100644 --- a/examples/nonce_cache/src/main.rs +++ b/examples/nonce_cache/src/main.rs @@ -129,6 +129,16 @@ async fn pumpfun_copy_trade_with_grpc( let client = create_solana_trade_client().await?; let mint_pubkey = trade_info.mint; + let virtual_quote_reserves = if trade_info.virtual_quote_reserves != 0 { + trade_info.virtual_quote_reserves + } else { + trade_info.virtual_sol_reserves + }; + let real_quote_reserves = if trade_info.virtual_quote_reserves != 0 { + trade_info.real_quote_reserves + } else { + trade_info.real_sol_reserves + }; let slippage_basis_points = Some(100); let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; @@ -150,12 +160,13 @@ async fn pumpfun_copy_trade_with_grpc( trade_info.bonding_curve, trade_info.associated_bonding_curve, trade_info.mint, + trade_info.quote_mint, trade_info.creator, trade_info.creator_vault, trade_info.virtual_token_reserves, - trade_info.virtual_sol_reserves, + virtual_quote_reserves, trade_info.real_token_reserves, - trade_info.real_sol_reserves, + real_quote_reserves, None, trade_info.fee_recipient, trade_info.token_program, diff --git a/examples/pumpfun_copy_trading/Cargo.toml b/examples/pumpfun_copy_trading/Cargo.toml index a6b24e2..49bbaec 100644 --- a/examples/pumpfun_copy_trading/Cargo.toml +++ b/examples/pumpfun_copy_trading/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sol-trade-sdk = { path = "../.." } -sol-parser-sdk = "0.2.2" +sol-parser-sdk = "0.4.14" solana-sdk = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } tokio = { version = "1", features = ["full"] } diff --git a/examples/pumpfun_copy_trading/src/main.rs b/examples/pumpfun_copy_trading/src/main.rs index e28ccd8..811f857 100644 --- a/examples/pumpfun_copy_trading/src/main.rs +++ b/examples/pumpfun_copy_trading/src/main.rs @@ -128,6 +128,13 @@ async fn create_solana_trade_client() -> AnyResult { async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> { let client = create_solana_trade_client().await?; let mint_pubkey = e.mint; + let virtual_quote_reserves = if e.virtual_quote_reserves != 0 { + e.virtual_quote_reserves + } else { + e.virtual_sol_reserves + }; + let real_quote_reserves = + if e.virtual_quote_reserves != 0 { e.real_quote_reserves } else { e.real_sol_reserves }; let slippage_basis_points = Some(100u64); let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; @@ -147,12 +154,13 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) e.bonding_curve, e.associated_bonding_curve, e.mint, + e.quote_mint, e.creator, e.creator_vault, e.virtual_token_reserves, - e.virtual_sol_reserves, + virtual_quote_reserves, e.real_token_reserves, - e.real_sol_reserves, + real_quote_reserves, None, e.fee_recipient, e.token_program, @@ -197,12 +205,13 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) e.bonding_curve, e.associated_bonding_curve, e.mint, + e.quote_mint, e.creator, e.creator_vault, e.virtual_token_reserves, - e.virtual_sol_reserves, + virtual_quote_reserves, e.real_token_reserves, - e.real_sol_reserves, + real_quote_reserves, Some(true), e.fee_recipient, e.token_program, diff --git a/examples/pumpfun_sniper_trading/Cargo.toml b/examples/pumpfun_sniper_trading/Cargo.toml index eec88d7..0b08518 100644 --- a/examples/pumpfun_sniper_trading/Cargo.toml +++ b/examples/pumpfun_sniper_trading/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" [dependencies] sol-trade-sdk = { path = "../.." } -sol-parser-sdk = "0.2.2" +sol-parser-sdk = "0.4.14" solana-sdk = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } tokio = { version = "1", features = ["full"] } diff --git a/examples/pumpfun_sniper_trading/src/main.rs b/examples/pumpfun_sniper_trading/src/main.rs index 6c0d5e8..ec7bba8 100644 --- a/examples/pumpfun_sniper_trading/src/main.rs +++ b/examples/pumpfun_sniper_trading/src/main.rs @@ -118,6 +118,13 @@ async fn create_solana_trade_client() -> AnyResult { async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> { let client = create_solana_trade_client().await?; let mint_pubkey = e.mint; + let virtual_quote_reserves = if e.virtual_quote_reserves != 0 { + e.virtual_quote_reserves + } else { + e.virtual_sol_reserves + }; + let real_quote_reserves = + if e.virtual_quote_reserves != 0 { e.real_quote_reserves } else { e.real_sol_reserves }; let slippage_basis_points = Some(300u64); let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; @@ -181,12 +188,13 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent e.bonding_curve, e.associated_bonding_curve, e.mint, + e.quote_mint, e.creator, e.creator_vault, e.virtual_token_reserves, - e.virtual_sol_reserves, + virtual_quote_reserves, e.real_token_reserves, - e.real_sol_reserves, + real_quote_reserves, Some(true), e.fee_recipient, e.token_program, diff --git a/src/client/mod.rs b/src/client/mod.rs index 2d54d0b..3f298eb 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -312,8 +312,6 @@ 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); @@ -334,7 +332,6 @@ 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, } } } @@ -380,7 +377,7 @@ pub struct TradeBuyParams { pub gas_fee_strategy: GasFeeStrategy, /// Whether to simulate the transaction instead of executing it pub simulate: bool, - /// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap). + /// Use exact quote-input buy instructions (legacy PumpFun uses SOL quote; V2/PumpSwap use generic quote). /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens. /// 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. @@ -471,7 +468,6 @@ impl TradingClient { effective_core_ids, log_enabled: true, check_min_tip: false, - use_pumpfun_v2: false, } } @@ -518,7 +514,6 @@ impl TradingClient { effective_core_ids, log_enabled: true, check_min_tip: false, - use_pumpfun_v2: false, } } @@ -698,7 +693,6 @@ 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(); @@ -881,7 +875,6 @@ 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; @@ -997,7 +990,6 @@ 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/bonding_curve.rs b/src/common/bonding_curve.rs index b6785d2..334fc5a 100755 --- a/src/common/bonding_curve.rs +++ b/src/common/bonding_curve.rs @@ -31,7 +31,7 @@ use solana_sdk::pubkey::Pubkey; use crate::instruction::utils::pumpfun::global_constants::{ INITIAL_REAL_TOKEN_RESERVES, INITIAL_VIRTUAL_SOL_RESERVES, INITIAL_VIRTUAL_TOKEN_RESERVES, - TOKEN_TOTAL_SUPPLY, + INITIAL_VIRTUAL_USDC_RESERVES, TOKEN_TOTAL_SUPPLY, }; use crate::instruction::utils::pumpfun::{get_bonding_curve_pda, get_creator_vault_pda}; @@ -62,9 +62,57 @@ pub struct BondingCurveAccount { pub is_mayhem_mode: bool, /// Whether this coin has cashback enabled (creator fee redirected to users) pub is_cashback_coin: bool, + /// Quote mint for V2 curves. Defaults to WSOL for legacy/event payloads that do not expose it. + pub quote_mint: Pubkey, } impl BondingCurveAccount { + #[inline] + pub fn normalize_quote_mint(quote_mint: Pubkey) -> Pubkey { + if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT { + crate::constants::WSOL_TOKEN_ACCOUNT + } else { + quote_mint + } + } + + #[inline] + pub fn effective_quote_mint(&self) -> Pubkey { + Self::normalize_quote_mint(self.quote_mint) + } + + #[inline] + pub fn initial_virtual_quote_reserves_for_quote_mint(quote_mint: &Pubkey) -> u64 { + if *quote_mint == crate::constants::USDC_TOKEN_ACCOUNT { + INITIAL_VIRTUAL_USDC_RESERVES + } else { + INITIAL_VIRTUAL_SOL_RESERVES + } + } + + #[inline] + pub fn virtual_quote_reserves(&self) -> u64 { + self.virtual_sol_reserves + } + + #[inline] + pub fn real_quote_reserves(&self) -> u64 { + self.real_sol_reserves + } + + #[inline] + pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self { + let quote_mint = Self::normalize_quote_mint(quote_mint); + let old_initial = + Self::initial_virtual_quote_reserves_for_quote_mint(&self.effective_quote_mint()); + let new_initial = Self::initial_virtual_quote_reserves_for_quote_mint("e_mint); + if self.virtual_sol_reserves == old_initial.saturating_add(self.real_sol_reserves) { + self.virtual_sol_reserves = new_initial.saturating_add(self.real_sol_reserves); + } + self.quote_mint = quote_mint; + self + } + /// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag /// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead. pub fn from_dev_trade( @@ -75,24 +123,50 @@ impl BondingCurveAccount { creator: Pubkey, is_mayhem_mode: bool, is_cashback_coin: bool, + ) -> Self { + Self::from_dev_trade_with_quote_mint( + bonding_curve, + mint, + dev_token_amount, + dev_sol_amount, + creator, + is_mayhem_mode, + is_cashback_coin, + crate::constants::WSOL_TOKEN_ACCOUNT, + ) + } + + /// Same as [`Self::from_dev_trade`], but quote-aware for V2 pools such as USDC. + pub fn from_dev_trade_with_quote_mint( + bonding_curve: Pubkey, + mint: &Pubkey, + dev_token_amount: u64, + dev_quote_amount: u64, + creator: Pubkey, + is_mayhem_mode: bool, + is_cashback_coin: bool, + quote_mint: Pubkey, ) -> Self { let account = if bonding_curve != Pubkey::default() { bonding_curve } else { get_bonding_curve_pda(&mint).unwrap() }; + let quote_mint = Self::normalize_quote_mint(quote_mint); Self { discriminator: 0, account: account, virtual_token_reserves: INITIAL_VIRTUAL_TOKEN_RESERVES - dev_token_amount, - virtual_sol_reserves: INITIAL_VIRTUAL_SOL_RESERVES + dev_sol_amount, + virtual_sol_reserves: Self::initial_virtual_quote_reserves_for_quote_mint("e_mint) + + dev_quote_amount, real_token_reserves: INITIAL_REAL_TOKEN_RESERVES - dev_token_amount, - real_sol_reserves: dev_sol_amount, + real_sol_reserves: dev_quote_amount, token_total_supply: TOKEN_TOTAL_SUPPLY, complete: false, creator: creator, is_mayhem_mode: is_mayhem_mode, is_cashback_coin, + quote_mint, } } @@ -108,24 +182,53 @@ impl BondingCurveAccount { real_sol_reserves: u64, is_mayhem_mode: bool, is_cashback_coin: bool, + ) -> Self { + Self::from_trade_with_quote_mint( + bonding_curve, + mint, + creator, + virtual_token_reserves, + virtual_sol_reserves, + real_token_reserves, + real_sol_reserves, + is_mayhem_mode, + is_cashback_coin, + crate::constants::WSOL_TOKEN_ACCOUNT, + ) + } + + /// Same as [`Self::from_trade`], but carries the V2 quote mint alongside quote reserves. + pub fn from_trade_with_quote_mint( + bonding_curve: Pubkey, + mint: Pubkey, + creator: Pubkey, + virtual_token_reserves: u64, + virtual_quote_reserves: u64, + real_token_reserves: u64, + real_quote_reserves: u64, + is_mayhem_mode: bool, + is_cashback_coin: bool, + quote_mint: Pubkey, ) -> Self { let account = if bonding_curve != Pubkey::default() { bonding_curve } else { get_bonding_curve_pda(&mint).unwrap() }; + let quote_mint = Self::normalize_quote_mint(quote_mint); Self { discriminator: 0, account: account, virtual_token_reserves: virtual_token_reserves, - virtual_sol_reserves: virtual_sol_reserves, + virtual_sol_reserves: virtual_quote_reserves, real_token_reserves: real_token_reserves, - real_sol_reserves: real_sol_reserves, + real_sol_reserves: real_quote_reserves, token_total_supply: TOKEN_TOTAL_SUPPLY, complete: false, creator: creator, is_mayhem_mode: is_mayhem_mode, is_cashback_coin, + quote_mint, } } diff --git a/src/common/types.rs b/src/common/types.rs index 759d1cb..6282096 100755 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -103,9 +103,6 @@ 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/26-account metas, quote_mint support). - /// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface. - pub use_pumpfun_v2: bool, } impl TradeConfig { @@ -160,7 +157,6 @@ pub struct TradeConfigBuilder { check_min_tip: bool, swqos_cores_from_end: bool, mev_protection: bool, - use_pumpfun_v2: bool, } impl TradeConfigBuilder { @@ -175,7 +171,6 @@ impl TradeConfigBuilder { check_min_tip: false, swqos_cores_from_end: false, mev_protection: false, - use_pumpfun_v2: false, } } @@ -221,14 +216,6 @@ 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 { @@ -241,7 +228,6 @@ 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/bonk.rs b/src/instruction/bonk.rs index 75b6445..087bb16 100755 --- a/src/instruction/bonk.rs +++ b/src/instruction/bonk.rs @@ -389,7 +389,6 @@ mod tests { check_min_tip: false, grpc_recv_us: None, use_exact_sol_amount: None, - use_pumpfun_v2: false, } } diff --git a/src/instruction/meteora_damm_v2.rs b/src/instruction/meteora_damm_v2.rs index 429bb83..5c102c9 100644 --- a/src/instruction/meteora_damm_v2.rs +++ b/src/instruction/meteora_damm_v2.rs @@ -371,7 +371,6 @@ mod tests { check_min_tip: false, grpc_recv_us: None, use_exact_sol_amount: None, - use_pumpfun_v2: false, } } diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 49e4b47..90a4611 100644 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -1,10 +1,12 @@ //! 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/26-account unified metas, quote_mint support). +//! The SDK selects the legacy or V2 on-chain layout from `PumpFunParams.quote_mint`. +//! `Pubkey::default()` keeps the smaller legacy SOL layout; explicit WSOL/native SOL +//! or USDC uses the V2 27/26-account unified metas. //! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency. use crate::{ + common::bonding_curve::BondingCurveAccount, common::spl_token::close_account, constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022}, trading::core::{ @@ -50,32 +52,98 @@ fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey /// `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 + let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint(); + let quote_mint = if protocol_params.quote_mint != Pubkey::default() { + BondingCurveAccount::normalize_quote_mint(protocol_params.quote_mint) + } else if curve_quote_mint != Pubkey::default() { + curve_quote_mint } else { - protocol_params.quote_mint + crate::constants::WSOL_TOKEN_ACCOUNT }; (quote_mint, crate::constants::TOKEN_PROGRAM) } +#[inline] +fn is_sol_quote_mint(quote_mint: &Pubkey) -> bool { + *quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT + || *quote_mint == crate::constants::SOL_TOKEN_ACCOUNT +} + +#[inline] +fn validate_v2_buy_quote_mint(input_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> { + if is_sol_quote_mint(quote_mint) { + if *input_mint == crate::constants::SOL_TOKEN_ACCOUNT + || *input_mint == crate::constants::WSOL_TOKEN_ACCOUNT + { + return Ok(()); + } + } else if input_mint == quote_mint { + return Ok(()); + } + Err(anyhow!( + "PumpFun V2 buy input_mint {} does not match quote_mint {}; USDC quote pools must be bought with USDC, not SOL", + input_mint, + quote_mint + )) +} + +#[inline] +fn validate_v2_sell_quote_mint(output_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> { + if is_sol_quote_mint(quote_mint) { + if *output_mint == crate::constants::SOL_TOKEN_ACCOUNT + || *output_mint == crate::constants::WSOL_TOKEN_ACCOUNT + { + return Ok(()); + } + } else if output_mint == quote_mint { + return Ok(()); + } + Err(anyhow!( + "PumpFun V2 sell output_mint {} does not match quote_mint {}; USDC quote pools settle to USDC, not SOL", + output_mint, + quote_mint + )) +} + pub struct PumpFunInstructionBuilder; #[async_trait::async_trait] impl InstructionBuilder for PumpFunInstructionBuilder { async fn build_buy_instructions(&self, params: &SwapParams) -> Result> { - if params.use_pumpfun_v2 { - build_buy_v2(params) - } else { - build_buy_v1(params) - } + build_buy(params) } async fn build_sell_instructions(&self, params: &SwapParams) -> Result> { - if params.use_pumpfun_v2 { - build_sell_v2(params) - } else { - build_sell_v1(params) - } + build_sell(params) + } +} + +#[inline] +fn should_use_v2_layout(params: &SwapParams) -> Result { + let protocol_params = params + .protocol_params + .as_any() + .downcast_ref::() + .ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?; + let (quote_mint, _) = effective_quote_mint_and_token_program(protocol_params); + Ok(protocol_params.quote_mint != Pubkey::default() || !is_sol_quote_mint("e_mint)) +} + +#[inline] +fn build_buy(params: &SwapParams) -> Result> { + if should_use_v2_layout(params)? { + build_buy_unified(params) + } else { + build_buy_legacy(params) + } +} + +#[inline] +fn build_sell(params: &SwapParams) -> Result> { + if should_use_v2_layout(params)? { + build_sell_unified(params) + } else { + build_sell_legacy(params) } } @@ -83,9 +151,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder { // V1 (default) — 18 metas, no quote_mint ATA create // --------------------------------------------------------------------------- -fn build_buy_v1(params: &SwapParams) -> Result> { +fn build_buy_legacy(params: &SwapParams) -> Result> { use crate::instruction::pumpfun_ix_data::{ - encode_pumpfun_buy_exact_sol_in_ix_data, encode_pumpfun_buy_ix_data, + encode_pumpfun_buy_exact_quote_in_ix_data, encode_pumpfun_buy_ix_data, PumpFunIxVersion, }; use crate::instruction::utils::pumpfun::{ get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random, @@ -160,8 +228,9 @@ fn build_buy_v1(params: &SwapParams) -> Result> { // ── Legacy buy path ── let track_volume_val = if bonding_curve.is_cashback_coin { 1u8 } else { 0u8 }; + let ix_version = PumpFunIxVersion::Legacy { track_volume: track_volume_val }; let buy_data = if let Some(token_amount) = params.fixed_output_amount { - encode_pumpfun_buy_ix_data(token_amount, lamports_in, track_volume_val) + encode_pumpfun_buy_ix_data(token_amount, lamports_in, ix_version) } else { let buy_token_amount = get_buy_token_amount_from_sol_amount( bonding_curve.virtual_token_reserves as u128, @@ -172,10 +241,10 @@ fn build_buy_v1(params: &SwapParams) -> Result> { ); 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) + encode_pumpfun_buy_exact_quote_in_ix_data(lamports_in, min_tokens_out, ix_version) } else { let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp); - encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, track_volume_val) + encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, ix_version) } }; @@ -206,13 +275,13 @@ fn build_buy_v1(params: &SwapParams) -> Result> { 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)); + instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas)); Ok(instructions) } -fn build_sell_v1(params: &SwapParams) -> Result> { - use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_ix_data; +fn build_sell_legacy(params: &SwapParams) -> Result> { + use crate::instruction::pumpfun_ix_data::{encode_pumpfun_sell_ix_data, PumpFunIxVersion}; use crate::instruction::utils::pumpfun::{ get_bonding_curve_v2_pda, get_protocol_extra_fee_recipient_random, is_phantom_default_creator_vault, @@ -299,7 +368,11 @@ fn build_sell_v1(params: &SwapParams) -> Result> { .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 sell_data = encode_pumpfun_sell_ix_data( + token_amount, + min_sol_output, + PumpFunIxVersion::Legacy { track_volume: 0 }, + ); let fee_recipient_meta = pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); @@ -331,7 +404,7 @@ fn build_sell_v1(params: &SwapParams) -> Result> { 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)); + instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, sell_data.as_slice(), metas)); if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata { @@ -348,12 +421,12 @@ fn build_sell_v1(params: &SwapParams) -> Result> { } // --------------------------------------------------------------------------- -// V2 (opt-in via `use_pumpfun_v2 = true`) — 27 metas, quote_mint ATA create +// V2 — 27 metas, quote_mint ATA create // --------------------------------------------------------------------------- -fn build_buy_v2(params: &SwapParams) -> Result> { +fn build_buy_unified(params: &SwapParams) -> Result> { use crate::instruction::pumpfun_ix_data::{ - encode_pumpfun_buy_exact_quote_in_v2_ix_data, encode_pumpfun_buy_v2_ix_data, + encode_pumpfun_buy_exact_quote_in_ix_data, encode_pumpfun_buy_ix_data, PumpFunIxVersion, }; use crate::instruction::utils::pumpfun::{ get_buyback_fee_recipient_random, get_fee_sharing_config_pda, @@ -395,6 +468,7 @@ fn build_buy_v2(params: &SwapParams) -> Result> { }; let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params); + validate_v2_buy_quote_mint(¶ms.input_mint, "e_mint)?; let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META; let associated_base_bonding_curve = @@ -477,7 +551,7 @@ fn build_buy_v2(params: &SwapParams) -> Result> { } let (buy_data, quote_amount_to_fund) = if let Some(token_amount) = params.fixed_output_amount { - (encode_pumpfun_buy_v2_ix_data(token_amount, lamports_in), lamports_in) + (encode_pumpfun_buy_ix_data(token_amount, lamports_in, PumpFunIxVersion::V2), lamports_in) } else { let buy_token_amount = get_buy_token_amount_from_sol_amount( bonding_curve.virtual_token_reserves as u128, @@ -488,10 +562,20 @@ fn build_buy_v2(params: &SwapParams) -> Result> { ); 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), lamports_in) + ( + encode_pumpfun_buy_exact_quote_in_ix_data( + lamports_in, + min_tokens_out, + PumpFunIxVersion::V2, + ), + lamports_in, + ) } else { let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp); - (encode_pumpfun_buy_v2_ix_data(buy_token_amount, max_sol_cost), max_sol_cost) + ( + encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, PumpFunIxVersion::V2), + max_sol_cost, + ) } }; @@ -536,7 +620,7 @@ fn build_buy_v2(params: &SwapParams) -> Result> { accounts::PUMPFUN_META, ]; - instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, metas)); + instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas)); if params.close_input_mint_ata { push_close_wsol_if_needed(&mut instructions, ¶ms.payer.pubkey(), "e_mint); @@ -545,8 +629,8 @@ fn build_buy_v2(params: &SwapParams) -> Result> { Ok(instructions) } -fn build_sell_v2(params: &SwapParams) -> Result> { - use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_v2_ix_data; +fn build_sell_unified(params: &SwapParams) -> Result> { + use crate::instruction::pumpfun_ix_data::{encode_pumpfun_sell_ix_data, PumpFunIxVersion}; use crate::instruction::utils::pumpfun::{ get_buyback_fee_recipient_random, get_fee_sharing_config_pda, is_phantom_default_creator_vault, @@ -615,6 +699,7 @@ fn build_sell_v2(params: &SwapParams) -> Result> { }; let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params); + validate_v2_sell_quote_mint(¶ms.output_mint, "e_mint)?; let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META; let associated_base_bonding_curve = @@ -694,7 +779,7 @@ fn build_sell_v2(params: &SwapParams) -> Result> { ); } - let sell_data = encode_pumpfun_sell_v2_ix_data(token_amount, min_sol_output); + let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output, PumpFunIxVersion::V2); let metas: Vec = vec![ global_constants::GLOBAL_ACCOUNT_META, @@ -725,7 +810,7 @@ fn build_sell_v2(params: &SwapParams) -> Result> { accounts::PUMPFUN_META, ]; - instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, metas)); + instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, sell_data.as_slice(), metas)); if protocol_params.close_token_account_when_sell.unwrap_or(false) || params.close_input_mint_ata { @@ -802,7 +887,6 @@ mod tests { close_token_account_when_sell: None, fee_recipient: global_constants::FEE_RECIPIENT, quote_mint: Pubkey::default(), - use_v2_ix: false, }; SwapParams { @@ -839,7 +923,6 @@ mod tests { check_min_tip: false, grpc_recv_us: None, use_exact_sol_amount: Some(true), - use_pumpfun_v2: false, } } @@ -855,7 +938,7 @@ mod tests { fn pump_suffix_buy_respects_explicit_legacy_token_program() { crate::common::seed::set_default_rents(); let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); - let instructions = build_buy_v1(¶ms).unwrap(); + let instructions = build_buy(¶ms).unwrap(); assert_eq!(instructions.len(), 3); assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM); @@ -868,7 +951,7 @@ mod tests { fn non_pump_buy_respects_explicit_legacy_token_program() { crate::common::seed::set_default_rents(); let params = swap_params_for_buy(Pubkey::new_unique(), TOKEN_PROGRAM); - let instructions = build_buy_v1(¶ms).unwrap(); + let instructions = build_buy(¶ms).unwrap(); assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM); } @@ -880,7 +963,7 @@ mod tests { params.fixed_output_amount = Some(42); params.use_exact_sol_amount = Some(true); - let instructions = build_buy_v1(¶ms).unwrap(); + let instructions = build_buy(¶ms).unwrap(); let ix = instructions.last().unwrap(); assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR); @@ -899,8 +982,12 @@ mod tests { params.create_output_mint_ata = false; 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); + } - let instructions = build_buy_v2(¶ms).unwrap(); + let instructions = build_buy(¶ms).unwrap(); let ix = instructions.last().unwrap(); assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR); @@ -917,8 +1004,12 @@ mod tests { params.create_output_mint_ata = false; params.create_input_mint_ata = true; 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 instructions = build_buy_v2(¶ms).unwrap(); + let instructions = build_buy(¶ms).unwrap(); let transfer_ix = &instructions[1]; let system_ix = bincode::deserialize::< solana_system_interface::instruction::SystemInstruction, @@ -937,20 +1028,195 @@ mod tests { } } + #[test] + fn pumpfun_usdc_dev_trade_uses_usdc_initial_quote_reserves() { + let mint = pump_mint(); + let dev_quote_amount = 707_080; + let params = PumpFunParams::from_dev_trade_with_quote_mint( + mint, + 55_855_975_892_641, + dev_quote_amount, + Pubkey::new_unique(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + None, + global_constants::FEE_RECIPIENT, + TOKEN_PROGRAM, + false, + Some(false), + crate::constants::USDC_TOKEN_ACCOUNT, + ); + + assert_eq!(params.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT); + assert_eq!(params.bonding_curve.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT); + assert_eq!( + params.bonding_curve.virtual_sol_reserves, + global_constants::INITIAL_VIRTUAL_USDC_RESERVES + dev_quote_amount + ); + } + + #[test] + fn pumpfun_with_quote_mint_adjusts_dev_trade_quote_reserves() { + let mint = pump_mint(); + let dev_quote_amount = 707_080; + let params = PumpFunParams::from_dev_trade( + mint, + 55_855_975_892_641, + dev_quote_amount, + Pubkey::new_unique(), + Pubkey::default(), + Pubkey::default(), + Pubkey::default(), + None, + global_constants::FEE_RECIPIENT, + TOKEN_PROGRAM, + false, + Some(false), + ) + .with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT); + + assert_eq!( + params.bonding_curve.virtual_sol_reserves, + global_constants::INITIAL_VIRTUAL_USDC_RESERVES + dev_quote_amount + ); + } + + #[test] + fn pumpfun_usdc_trade_event_preserves_virtual_quote_reserves() { + let mint = pump_mint(); + let virtual_quote_reserves = 4_527_693_121; + let real_quote_reserves = 235_693_121; + let params = PumpFunParams::from_trade( + Pubkey::default(), + Pubkey::default(), + mint, + crate::constants::USDC_TOKEN_ACCOUNT, + Pubkey::new_unique(), + Pubkey::default(), + 944_144_024_107_359, + virtual_quote_reserves, + 737_244_024_107_359, + real_quote_reserves, + None, + global_constants::FEE_RECIPIENT, + TOKEN_PROGRAM, + false, + Some(false), + ); + + assert_eq!(params.quote_mint, crate::constants::USDC_TOKEN_ACCOUNT); + assert_eq!(params.bonding_curve.virtual_quote_reserves(), virtual_quote_reserves); + assert_eq!(params.bonding_curve.real_quote_reserves(), real_quote_reserves); + } + + #[test] + fn pumpfun_native_sol_quote_mint_normalizes_to_wsol() { + let mint = pump_mint(); + let params = PumpFunParams::from_trade( + Pubkey::default(), + Pubkey::default(), + mint, + crate::constants::SOL_TOKEN_ACCOUNT, + Pubkey::new_unique(), + Pubkey::default(), + 944_144_024_107_359, + 30_235_693_121, + 737_244_024_107_359, + 235_693_121, + None, + global_constants::FEE_RECIPIENT, + TOKEN_PROGRAM, + false, + Some(false), + ); + + assert_eq!(params.quote_mint, crate::constants::WSOL_TOKEN_ACCOUNT); + assert_eq!(params.bonding_curve.quote_mint, crate::constants::WSOL_TOKEN_ACCOUNT); + } + + #[test] + fn pumpfun_v2_usdc_buy_rejects_sol_input() { + let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM); + params.create_output_mint_ata = false; + params.input_mint = crate::constants::SOL_TOKEN_ACCOUNT; + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT); + } + + let err = build_buy(¶ms).unwrap_err().to_string(); + assert!(err.contains("USDC quote pools must be bought with USDC")); + } + + #[test] + fn pumpfun_usdc_quote_mint_selects_v2_without_global_flag() { + 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; + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT); + } + + let ix = build_buy(¶ms).unwrap().pop().unwrap(); + assert_eq!( + &ix.data[..8], + crate::instruction::utils::pumpfun::BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR + ); + assert_eq!(ix.accounts.len(), 27); + } + + #[test] + fn pumpfun_v2_usdc_buy_uses_idl_accounts_17_18_19() { + 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::USDC_TOKEN_ACCOUNT); + } + + let associated_creator_vault = + if let DexParamEnum::PumpFun(protocol_params) = ¶ms.protocol_params { + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &protocol_params.creator_vault, + &crate::constants::USDC_TOKEN_ACCOUNT, + &crate::constants::TOKEN_PROGRAM, + ) + } else { + Pubkey::default() + }; + let ix = build_buy(¶ms).unwrap().pop().unwrap(); + assert_eq!(ix.accounts.len(), 27); + assert_eq!(ix.accounts[17].pubkey, associated_creator_vault); + assert_eq!( + ix.accounts[18].pubkey, + crate::instruction::utils::pumpfun::get_fee_sharing_config_pda(&mint).unwrap() + ); + assert_eq!(ix.accounts[19].pubkey, accounts::GLOBAL_VOLUME_ACCUMULATOR); + assert_eq!(ix.accounts[22].pubkey, accounts::FEE_CONFIG); + } + #[test] fn pumpfun_v2_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; + if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params { + *protocol_params = + protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT); + } - let buy_ix = build_buy_v2(¶ms).unwrap().pop().unwrap(); + let buy_ix = build_buy(¶ms).unwrap().pop().unwrap(); assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&buy_ix.accounts[8].pubkey)); assert!(buy_ix.accounts[8].is_writable); params.trade_type = crate::swqos::TradeType::Sell; params.input_mint = mint; params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT; - let sell_ix = build_sell_v2(¶ms).unwrap().pop().unwrap(); + 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); } @@ -965,7 +1231,7 @@ mod tests { params.create_output_mint_ata = false; params.fixed_output_amount = Some(42); - let instructions = build_sell_v1(¶ms).unwrap(); + let instructions = build_sell(¶ms).unwrap(); let ix = instructions.last().unwrap(); assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR); @@ -986,8 +1252,12 @@ mod tests { params.output_mint = crate::constants::SOL_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); + } - let instructions = build_sell_v2(¶ms).unwrap(); + let instructions = build_sell(¶ms).unwrap(); let ix = instructions.last().unwrap(); assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR); diff --git a/src/instruction/pumpfun_ix_data.rs b/src/instruction/pumpfun_ix_data.rs index 31d6fba..c4cfa25 100644 --- a/src/instruction/pumpfun_ix_data.rs +++ b/src/instruction/pumpfun_ix_data.rs @@ -1,5 +1,6 @@ -//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` 与 **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`** -//! 的 instruction data 栈上编码(热路径零堆分配)。 +//! Pump.fun bonding-curve `buy` / `buy_exact_quote_in` / `sell` +//! instruction data stack encoding. The public helper names are version-neutral; +//! [`PumpFunIxVersion`] selects the legacy or V2 on-chain discriminator. //! //! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 是单字段 //! struct(TypeScript 传 `[true]`),在 ix 参数中为 1 字节 bool,共 25 字节 ix data。 @@ -10,71 +11,90 @@ use crate::instruction::utils::pumpfun::{ BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR, }; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PumpFunIxVersion { + Legacy { track_volume: u8 }, + V2, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum PumpFunIxData { + Bytes25([u8; 25]), + Bytes24([u8; 24]), +} + +impl PumpFunIxData { + #[inline(always)] + pub(crate) fn as_slice(&self) -> &[u8] { + match self { + Self::Bytes25(data) => data, + Self::Bytes24(data) => data, + } + } +} + #[inline(always)] -pub fn encode_pumpfun_buy_ix_data( +pub(crate) fn encode_pumpfun_buy_ix_data( token_amount: u64, - max_sol_cost: u64, - track_volume_val: u8, -) -> [u8; 25] { - let mut d = [0u8; 25]; - d[..8].copy_from_slice(&BUY_DISCRIMINATOR); - d[8..16].copy_from_slice(&token_amount.to_le_bytes()); - d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes()); - d[24] = track_volume_val; - d + max_quote_cost: u64, + version: PumpFunIxVersion, +) -> PumpFunIxData { + match version { + PumpFunIxVersion::Legacy { track_volume } => { + let mut d = [0u8; 25]; + d[..8].copy_from_slice(&BUY_DISCRIMINATOR); + d[8..16].copy_from_slice(&token_amount.to_le_bytes()); + d[16..24].copy_from_slice(&max_quote_cost.to_le_bytes()); + d[24] = track_volume; + PumpFunIxData::Bytes25(d) + } + PumpFunIxVersion::V2 => { + let mut d = [0u8; 24]; + d[..8].copy_from_slice(&BUY_V2_DISCRIMINATOR); + d[8..16].copy_from_slice(&token_amount.to_le_bytes()); + d[16..24].copy_from_slice(&max_quote_cost.to_le_bytes()); + PumpFunIxData::Bytes24(d) + } + } } #[inline(always)] -pub fn encode_pumpfun_buy_exact_sol_in_ix_data( - spendable_sol_in: u64, - min_tokens_out: u64, - track_volume_val: u8, -) -> [u8; 25] { - let mut d = [0u8; 25]; - d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR); - d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes()); - d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes()); - d[24] = track_volume_val; - d -} - -#[inline(always)] -pub fn encode_pumpfun_sell_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] { - let mut d = [0u8; 24]; - d[..8].copy_from_slice(&SELL_DISCRIMINATOR); - d[8..16].copy_from_slice(&token_amount.to_le_bytes()); - d[16..24].copy_from_slice(&min_sol_output.to_le_bytes()); - d -} - -// --- v2 instruction data encoders (no track_volume arg — 2 args each, 24 bytes total) --- - -#[inline(always)] -pub fn encode_pumpfun_buy_v2_ix_data(amount: u64, max_sol_cost: u64) -> [u8; 24] { - let mut d = [0u8; 24]; - d[..8].copy_from_slice(&BUY_V2_DISCRIMINATOR); - d[8..16].copy_from_slice(&amount.to_le_bytes()); - d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes()); - d -} - -#[inline(always)] -pub fn encode_pumpfun_buy_exact_quote_in_v2_ix_data( +pub(crate) fn encode_pumpfun_buy_exact_quote_in_ix_data( spendable_quote_in: u64, min_tokens_out: u64, -) -> [u8; 24] { - let mut d = [0u8; 24]; - d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR); - d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes()); - d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes()); - d + version: PumpFunIxVersion, +) -> PumpFunIxData { + match version { + PumpFunIxVersion::Legacy { track_volume } => { + let mut d = [0u8; 25]; + d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR); + d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes()); + d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes()); + d[24] = track_volume; + PumpFunIxData::Bytes25(d) + } + PumpFunIxVersion::V2 => { + let mut d = [0u8; 24]; + d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR); + d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes()); + d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes()); + PumpFunIxData::Bytes24(d) + } + } } #[inline(always)] -pub fn encode_pumpfun_sell_v2_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] { +pub(crate) fn encode_pumpfun_sell_ix_data( + token_amount: u64, + min_quote_output: u64, + version: PumpFunIxVersion, +) -> PumpFunIxData { let mut d = [0u8; 24]; - d[..8].copy_from_slice(&SELL_V2_DISCRIMINATOR); + d[..8].copy_from_slice(match version { + PumpFunIxVersion::Legacy { .. } => &SELL_DISCRIMINATOR, + PumpFunIxVersion::V2 => &SELL_V2_DISCRIMINATOR, + }); d[8..16].copy_from_slice(&token_amount.to_le_bytes()); - d[16..24].copy_from_slice(&min_sol_output.to_le_bytes()); - d + d[16..24].copy_from_slice(&min_quote_output.to_le_bytes()); + PumpFunIxData::Bytes24(d) } diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index a935f82..074405e 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -596,7 +596,6 @@ mod tests { check_min_tip: false, grpc_recv_us: None, use_exact_sol_amount: Some(true), - use_pumpfun_v2: false, } } diff --git a/src/instruction/raydium_amm_v4.rs b/src/instruction/raydium_amm_v4.rs index 205d5e4..4381851 100755 --- a/src/instruction/raydium_amm_v4.rs +++ b/src/instruction/raydium_amm_v4.rs @@ -389,7 +389,6 @@ mod tests { check_min_tip: false, grpc_recv_us: None, use_exact_sol_amount: None, - use_pumpfun_v2: false, } } diff --git a/src/instruction/raydium_cpmm.rs b/src/instruction/raydium_cpmm.rs index 5454a8a..3999093 100755 --- a/src/instruction/raydium_cpmm.rs +++ b/src/instruction/raydium_cpmm.rs @@ -401,7 +401,6 @@ mod tests { check_min_tip: false, grpc_recv_us: None, use_exact_sol_amount: None, - use_pumpfun_v2: false, } } diff --git a/src/instruction/utils/pumpfun.rs b/src/instruction/utils/pumpfun.rs index 3d753e5..c2b86cb 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -39,6 +39,7 @@ pub mod global_constants { pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000; pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000; + pub const INITIAL_VIRTUAL_USDC_RESERVES: u64 = 4_292_000_000; pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000; pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000; pub const FEE_BASIS_POINTS: u64 = 95; diff --git a/src/trading/core/params/dex_swap.rs b/src/trading/core/params/dex_swap.rs index 9713569..933027d 100644 --- a/src/trading/core/params/dex_swap.rs +++ b/src/trading/core/params/dex_swap.rs @@ -94,14 +94,11 @@ pub struct SwapParams { pub check_min_tip: bool, /// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled. pub grpc_recv_us: Option, - /// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap). + /// Use exact quote-input buy instructions (legacy PumpFun uses SOL quote; V2/PumpSwap use generic quote). /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens. /// 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/26-account metas, quote_mint support). - /// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface. - pub use_pumpfun_v2: bool, } impl SwapParams { diff --git a/src/trading/core/params/pumpfun.rs b/src/trading/core/params/pumpfun.rs index a5ee490..b9412da 100644 --- a/src/trading/core/params/pumpfun.rs +++ b/src/trading/core/params/pumpfun.rs @@ -14,9 +14,9 @@ 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**: Set `use_v2_ix = true` to use `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` -/// with unified 27/26-account layout. Required for USDC-paired coins (`quote_mint != WSOL`). -/// For SOL-paired coins, legacy instructions still work and are the default. +/// **V2 instructions**: The SDK selects the layout automatically from `quote_mint`. +/// Leave `quote_mint` default for legacy SOL layout, pass WSOL/native SOL for SOL V2, +/// or pass USDC for USDC-paired coins. #[derive(Clone)] pub struct PumpFunParams { pub bonding_curve: Arc, @@ -43,9 +43,6 @@ pub struct PumpFunParams { /// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired). /// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`. pub quote_mint: Pubkey, - /// Whether to use v2 instructions (`buy_v2`/`sell_v2`/`buy_exact_quote_in_v2`). - /// Default `false` for backward compatibility. Must be `true` for USDC-paired coins. - pub use_v2_ix: bool, } impl PumpFunParams { @@ -64,12 +61,12 @@ impl PumpFunParams { close_token_account_when_sell: Some(close_token_account_when_sell), fee_recipient: Pubkey::default(), quote_mint: Pubkey::default(), - use_v2_ix: false, } } - /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event - /// so that sell instructions include the correct remaining accounts for cashback. + /// Build PumpFun params from a SOL-paired dev trade event. For USDC-paired dev trades, + /// use [`Self::from_dev_trade_with_quote_mint`] so reserve reconstruction uses the right quote mint. + /// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts. /// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`). /// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts). pub fn from_dev_trade( @@ -86,15 +83,51 @@ impl PumpFunParams { is_cashback_coin: bool, mayhem_mode: Option, ) -> Self { - let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient); - let bonding_curve_account = BondingCurveAccount::from_dev_trade( - bonding_curve, - &mint, + Self::from_dev_trade_with_quote_mint( + mint, token_amount, max_sol_cost, creator, + bonding_curve, + associated_bonding_curve, + creator_vault, + close_token_account_when_sell, + fee_recipient, + token_program, + is_cashback_coin, + mayhem_mode, + Pubkey::default(), + ) + } + + /// Quote-aware constructor for PumpFun V2 pools. Use this for USDC-paired coins so + /// dev-trade reserve reconstruction uses the quote mint's initial virtual reserves. + pub fn from_dev_trade_with_quote_mint( + mint: Pubkey, + token_amount: u64, + max_quote_cost: u64, + creator: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + creator_vault: Pubkey, + close_token_account_when_sell: Option, + fee_recipient: Pubkey, + token_program: Pubkey, + is_cashback_coin: bool, + mayhem_mode: Option, + quote_mint: Pubkey, + ) -> Self { + let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient); + let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint); + let bonding_curve_account = BondingCurveAccount::from_dev_trade_with_quote_mint( + bonding_curve, + &mint, + token_amount, + max_quote_cost, + creator, is_mayhem_mode, is_cashback_coin, + effective_quote_mint, ); let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( @@ -118,13 +151,18 @@ impl PumpFunParams { close_token_account_when_sell: close_token_account_when_sell, token_program: token_program, fee_recipient, - quote_mint: Pubkey::default(), - use_v2_ix: false, + quote_mint: if quote_mint == Pubkey::default() { + Pubkey::default() + } else { + effective_quote_mint + }, } } - /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event - /// so that sell instructions include the correct remaining accounts for cashback. + /// Build PumpFun params from event/parser data. Pass `quote_mint` from the event: + /// `Pubkey::default()` for legacy SOL layout, `WSOL_TOKEN_ACCOUNT`/native SOL for SOL V2, + /// and `USDC_TOKEN_ACCOUNT` for USDC V2. + /// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts. /// /// `mayhem_mode`: /// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**(Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。 @@ -133,12 +171,13 @@ impl PumpFunParams { bonding_curve: Pubkey, associated_bonding_curve: Pubkey, mint: Pubkey, + quote_mint: Pubkey, creator: Pubkey, creator_vault: Pubkey, virtual_token_reserves: u64, - virtual_sol_reserves: u64, + virtual_quote_reserves: u64, real_token_reserves: u64, - real_sol_reserves: u64, + real_quote_reserves: u64, close_token_account_when_sell: Option, fee_recipient: Pubkey, token_program: Pubkey, @@ -146,16 +185,18 @@ impl PumpFunParams { mayhem_mode: Option, ) -> Self { let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient); - let bonding_curve = BondingCurveAccount::from_trade( + let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint); + let bonding_curve = BondingCurveAccount::from_trade_with_quote_mint( bonding_curve, mint, creator, virtual_token_reserves, - virtual_sol_reserves, + virtual_quote_reserves, real_token_reserves, - real_sol_reserves, + real_quote_reserves, is_mayhem_mode, is_cashback_coin, + effective_quote_mint, ); let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( @@ -177,11 +218,52 @@ impl PumpFunParams { close_token_account_when_sell: close_token_account_when_sell, token_program: token_program, fee_recipient, - quote_mint: Pubkey::default(), - use_v2_ix: false, + quote_mint: if quote_mint == Pubkey::default() { + Pubkey::default() + } else { + effective_quote_mint + }, } } + /// Deprecated compatibility alias. Prefer [`Self::from_trade`] and pass `quote_mint` there. + #[deprecated(note = "use PumpFunParams::from_trade(..., quote_mint)")] + pub fn from_trade_with_quote_mint( + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + mint: Pubkey, + creator: Pubkey, + creator_vault: Pubkey, + virtual_token_reserves: u64, + virtual_quote_reserves: u64, + real_token_reserves: u64, + real_quote_reserves: u64, + close_token_account_when_sell: Option, + fee_recipient: Pubkey, + token_program: Pubkey, + is_cashback_coin: bool, + mayhem_mode: Option, + quote_mint: Pubkey, + ) -> Self { + Self::from_trade( + bonding_curve, + associated_bonding_curve, + mint, + quote_mint, + creator, + creator_vault, + virtual_token_reserves, + virtual_quote_reserves, + real_token_reserves, + real_quote_reserves, + close_token_account_when_sell, + fee_recipient, + token_program, + is_cashback_coin, + mayhem_mode, + ) + } + /// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。 pub async fn from_mint_by_rpc( rpc: &SolanaRpcClient, @@ -202,6 +284,7 @@ impl PumpFunParams { creator: account.0.creator, is_mayhem_mode: account.0.is_mayhem_mode, is_cashback_coin: account.0.is_cashback_coin, + quote_mint: account.0.quote_mint, }; let associated_bonding_curve = get_associated_token_address_with_program_id( &bonding_curve.account, @@ -224,6 +307,7 @@ impl PumpFunParams { crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator) }) .unwrap_or_default(); + let quote_mint = bonding_curve.quote_mint; Ok(Self { bonding_curve: Arc::new(bonding_curve), associated_bonding_curve: associated_bonding_curve, @@ -233,8 +317,7 @@ impl PumpFunParams { close_token_account_when_sell: None, token_program: mint_account.owner, fee_recipient: Pubkey::default(), - quote_mint: Pubkey::default(), - use_v2_ix: false, + quote_mint, }) } @@ -273,12 +356,16 @@ impl PumpFunParams { Ok(self) } - /// Sets `quote_mint` and enables v2 instructions. Required for USDC-paired coins. + /// Sets `quote_mint`. The instruction builder derives V1/V2 layout from this value. /// For SOL-paired coins, pass `WSOL_TOKEN_ACCOUNT` or leave default. #[inline] pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self { - self.quote_mint = quote_mint; - self.use_v2_ix = quote_mint != Pubkey::default(); + let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint); + self.quote_mint = + if quote_mint == Pubkey::default() { Pubkey::default() } else { effective_quote_mint }; + if let Some(curve) = Arc::get_mut(&mut self.bonding_curve) { + *curve = curve.clone().with_quote_mint(effective_quote_mint); + } self }