Compare commits

..

5 Commits

Author SHA1 Message Date
0xfnzero 5449d51591 Release sol-trade-sdk 4.0.14 2026-05-25 22:01:15 +08:00
0xfnzero b973061f92 Update DEX protocol support for 4.0.13 2026-05-25 01:47:48 +08:00
0xfnzero 57e4c5d221 Fix PumpFun V2 buyback fee mutability 2026-05-22 08:56:20 +08:00
0xfnzero 8ed2aef931 Fix trade execution correctness and release 4.0.11 2026-05-21 17:50:06 +08:00
0xfnzero 0cf0064e80 docs: clarify PumpFun V2 configuration 2026-05-21 14:48:43 +08:00
31 changed files with 982 additions and 365 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.10"
version = "4.0.14"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+36 -26
View File
@@ -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.14" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.9"
sol-trade-sdk = "4.0.14"
```
## 🛠️ 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(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
.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. CreateEvents `is_cashback_enabled` or BondingCurves `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. CreateEvents `is_cashback_enabled` or BondingCurves `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,38 +365,49 @@ 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()` 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.
**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 when PumpFun officially deploys V2 on mainnet):
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.
```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() or Solscan SOL from parser data
// - SOL V2 pool: WSOL_TOKEN_ACCOUNT
// - 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 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
// SOL-paired coin 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);
```
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.
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.
+38 -20
View File
@@ -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.14" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.9"
sol-trade-sdk = "4.0.14"
```
## 🛠️ 使用示例
@@ -333,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(...)` 领取累计的返现。
@@ -360,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 v2buy_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)— **无可选账户**
@@ -372,30 +374,46 @@ Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过
**使用方式:**
设置 `PumpFunParams``quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade``quote_mint` 不是 PDA,它就是 quote SPL mint`Pubkey::default()` 和 Solscan SOL`So11111111111111111111111111111111111111111`)表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2USDC 表示 USDC V2
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
// legacy SOL 池:log 事件里可能是 Pubkey::default()parser 数据里是 Solscan SOL sentinel
// SOL V2 池:WSOL_TOKEN_ACCOUNT
// USDC 池:就是 USDC mint
let quote_mint = e.quote_mint;
// 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?;
```
| 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 |
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。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 配对,统一布局 |
| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2 |
## 🛡️ MEV 保护服务
+1 -1
View File
@@ -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"] }
+13 -2
View File
@@ -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,
+1 -1
View File
@@ -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"
+13 -2
View File
@@ -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,
+1 -1
View File
@@ -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"] }
+13 -4
View File
@@ -128,6 +128,13 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
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,
+1 -1
View File
@@ -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"] }
+10 -2
View File
@@ -118,6 +118,13 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
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,
+1 -9
View File
@@ -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<Option<Arc<TradingClient>>> = 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;
+108 -5
View File
@@ -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(&quote_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(&quote_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,
}
}
+64 -25
View File
@@ -4,7 +4,7 @@ use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
use crate::common::{
spl_associated_token_account::get_associated_token_address_with_program_id,
@@ -21,6 +21,22 @@ const MAX_PDA_CACHE_SIZE: usize = 100_000;
const MAX_ATA_CACHE_SIZE: usize = 100_000;
const MAX_INSTRUCTION_CACHE_SIZE: usize = 100_000;
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
// --------------------- Instruction Cache ---------------------
/// Instruction cache key for uniquely identifying instruction types and parameters
@@ -66,7 +82,10 @@ where
};
// Lock-free cache lookup with entry API
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
let instructions =
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone();
prune_cache(&INSTRUCTION_CACHE, MAX_INSTRUCTION_CACHE_SIZE);
instructions
}
// --------------------- Associated Token Account ---------------------
@@ -106,8 +125,26 @@ pub fn _create_associated_token_account_idempotent_fast(
use_seed,
};
// Only use seed if the mint address is not wSOL or SOL
// 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全)
let build_standard_create = || {
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
};
// Only use seed if the mint address is not wSOL or SOL.
// Seed accounts are deterministic token accounts, not ATAs; callers must ensure they are not
// recreating an existing seeded account. Fall back to idempotent ATA if seed assembly fails.
let arc_instructions = if use_seed
&& !mint.eq(&crate::constants::WSOL_TOKEN_ACCOUNT)
&& !mint.eq(&crate::constants::SOL_TOKEN_ACCOUNT)
@@ -117,29 +154,18 @@ pub fn _create_associated_token_account_idempotent_fast(
// Use cache to get instruction
get_cached_instructions(cache_key, || {
super::seed::create_associated_token_account_use_seed(payer, owner, mint, token_program)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account setup failed for mint {}: {}; fallback to idempotent ATA",
mint,
err
);
build_standard_create()
})
})
} else {
// Use cache to get instruction
get_cached_instructions(cache_key, || {
// Get Associated Token Address using cache
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
// Create Associated Token Account instruction
// Reference implementation of spl_associated_token_account::instruction::create_associated_token_account
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
})
get_cached_instructions(cache_key, build_standard_create)
};
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
@@ -183,6 +209,7 @@ where
if let Some(pda) = pda_result {
PDA_CACHE.insert(cache_key, pda);
prune_cache(&PDA_CACHE, MAX_PDA_CACHE_SIZE);
}
pda_result
@@ -265,7 +292,18 @@ fn _get_associated_token_address_with_program_id_fast(
token_mint_address,
token_program_id,
)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account address failed for mint {}: {}; fallback to ATA",
token_mint_address,
err
);
get_associated_token_address_with_program_id(
wallet_address,
token_mint_address,
token_program_id,
)
})
} else {
get_associated_token_address_with_program_id(
wallet_address,
@@ -276,6 +314,7 @@ fn _get_associated_token_address_with_program_id_fast(
// Store computation result in cache (lock-free)
ATA_CACHE.insert(cache_key, ata);
prune_cache(&ATA_CACHE, MAX_ATA_CACHE_SIZE);
ata
}
+31 -34
View File
@@ -1,5 +1,4 @@
use crate::common::SolanaRpcClient;
use anyhow::anyhow;
use fnv::FnvHasher;
use once_cell::sync::Lazy;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
@@ -50,6 +49,26 @@ async fn fetch_rent_for_token_account(
Ok(client.get_minimum_balance_for_rent_exemption(165).await?)
}
#[inline]
fn derive_seed_from_mint(mint: &Pubkey) -> String {
// Keep the legacy 8-hex seed stable. Changing this derivation changes the token account
// address and can strand balances created by earlier buys.
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
let mut seed = String::with_capacity(8);
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
let byte = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
seed.push(byte as char);
}
seed
}
pub fn create_associated_token_account_use_seed(
payer: &Pubkey,
owner: &Pubkey,
@@ -63,33 +82,23 @@ pub fn create_associated_token_account_use_seed(
let rent = if is_2022_token {
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
} else {
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
};
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(mint);
// 🔧 修复:使用传入的 token_program 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program,地址自然一致
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
let ata_like = Pubkey::create_with_seed(payer, &seed, token_program)?;
let len = 165;
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
@@ -98,7 +107,7 @@ pub fn create_associated_token_account_use_seed(
payer,
&ata_like,
payer,
seed,
&seed,
rent,
len,
token_program,
@@ -118,21 +127,9 @@ pub fn get_associated_token_address_with_program_id_use_seed(
token_mint_address: &Pubkey,
token_program_id: &Pubkey,
) -> Result<Pubkey, anyhow::Error> {
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(token_mint_address.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(token_mint_address);
// 🔧 修复:使用传入的 token_program_id 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program_id,地址自然一致
let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program_id)?;
let ata_like = Pubkey::create_with_seed(wallet_address, &seed, token_program_id)?;
Ok(ata_like)
}
-14
View File
@@ -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,
}
}
}
-1
View File
@@ -389,7 +389,6 @@ mod tests {
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
}
}
-1
View File
@@ -371,7 +371,6 @@ mod tests {
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
}
}
+379 -71
View File
@@ -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()` 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.
use crate::{
common::bonding_curve::BondingCurveAccount,
common::spl_token::close_account,
constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022},
trading::core::{
@@ -30,56 +32,121 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use solana_sdk::instruction::AccountMeta;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signer::Signer,
};
#[inline]
fn is_pump_suffix_mint(mint: &Pubkey) -> bool {
mint.to_string().ends_with("pump")
}
#[inline]
fn effective_pump_mint_token_program(mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
let tp = protocol_params.token_program;
if tp == Pubkey::default() || tp == TOKEN_PROGRAM_2022 {
return TOKEN_PROGRAM_2022;
if tp == Pubkey::default() {
TOKEN_PROGRAM_2022
} else {
tp
}
if is_pump_suffix_mint(mint) {
return TOKEN_PROGRAM_2022;
}
tp
}
/// Resolve quote mint and its token program from PumpFunParams.
/// `Pubkey::default()` means legacy SOL-paired use WSOL mint for V2 instructions.
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` means legacy SOL-paired; use WSOL mint when a
/// downstream V2 helper needs a concrete SPL quote mint.
#[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<Vec<Instruction>> {
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<Vec<Instruction>> {
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<bool> {
let protocol_params = params
.protocol_params
.as_any()
.downcast_ref::<PumpFunParams>()
.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(&quote_mint))
}
#[inline]
fn build_buy(params: &SwapParams) -> Result<Vec<Instruction>> {
if should_use_v2_layout(params)? {
build_buy_unified(params)
} else {
build_buy_legacy(params)
}
}
#[inline]
fn build_sell(params: &SwapParams) -> Result<Vec<Instruction>> {
if should_use_v2_layout(params)? {
build_sell_unified(params)
} else {
build_sell_legacy(params)
}
}
@@ -87,9 +154,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
// V1 (default) — 18 metas, no quote_mint ATA create
// ---------------------------------------------------------------------------
fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
fn build_buy_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
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,
@@ -123,7 +190,7 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(&params.output_mint, protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -164,8 +231,9 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
// ── 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,
@@ -176,10 +244,10 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
);
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)
}
};
@@ -210,13 +278,13 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
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<Vec<Instruction>> {
use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_ix_data;
fn build_sell_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
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,
@@ -277,7 +345,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(&params.input_mint, protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -303,7 +371,11 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
.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);
@@ -335,7 +407,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
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
{
@@ -352,12 +424,12 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
}
// ---------------------------------------------------------------------------
// 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<Vec<Instruction>> {
fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
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,
@@ -391,8 +463,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program =
effective_pump_mint_token_program(&params.output_mint, protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -400,6 +471,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
};
let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params);
validate_v2_buy_quote_mint(&params.input_mint, &quote_mint)?;
let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META;
let associated_base_bonding_curve =
@@ -482,7 +554,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
}
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,
@@ -493,10 +565,20 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
);
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,
)
}
};
@@ -520,7 +602,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
fee_recipient_meta,
AccountMeta::new(associated_quote_fee_recipient, false),
AccountMeta::new_readonly(buyback_fee_recipient, false),
AccountMeta::new(buyback_fee_recipient, false),
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
AccountMeta::new(bonding_curve_addr, false),
AccountMeta::new(associated_base_bonding_curve, false),
@@ -541,7 +623,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
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, &params.payer.pubkey(), &quote_mint);
@@ -550,8 +632,8 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
Ok(instructions)
}
fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
use crate::instruction::pumpfun_ix_data::encode_pumpfun_sell_v2_ix_data;
fn build_sell_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
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,
@@ -612,7 +694,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program = effective_pump_mint_token_program(&params.input_mint, protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -620,6 +702,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
};
let (quote_mint, quote_token_program) = effective_quote_mint_and_token_program(protocol_params);
validate_v2_sell_quote_mint(&params.output_mint, &quote_mint)?;
let quote_token_program_meta = crate::constants::TOKEN_PROGRAM_META;
let associated_base_bonding_curve =
@@ -699,7 +782,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
);
}
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<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
@@ -710,7 +793,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
fee_recipient_meta,
AccountMeta::new(associated_quote_fee_recipient, false),
AccountMeta::new_readonly(buyback_fee_recipient, false),
AccountMeta::new(buyback_fee_recipient, false),
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
AccountMeta::new(bonding_curve_addr, false),
AccountMeta::new(associated_base_bonding_curve, false),
@@ -730,7 +813,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
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
{
@@ -773,7 +856,7 @@ mod tests {
use super::*;
use crate::{
common::{bonding_curve::BondingCurveAccount, GasFeeStrategy},
constants::{TOKEN_PROGRAM, TOKEN_PROGRAM_2022},
constants::TOKEN_PROGRAM,
trading::core::params::{DexParamEnum, PumpFunParams, SwapParams},
};
use solana_sdk::signature::Keypair;
@@ -807,7 +890,6 @@ mod tests {
close_token_account_when_sell: None,
fee_recipient: global_constants::FEE_RECIPIENT,
quote_mint: Pubkey::default(),
use_v2_ix: false,
};
SwapParams {
@@ -844,7 +926,6 @@ mod tests {
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: Some(true),
use_pumpfun_v2: false,
}
}
@@ -857,14 +938,14 @@ mod tests {
}
#[test]
fn pump_suffix_buy_forces_token_2022_even_when_params_legacy() {
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(&params).unwrap();
let instructions = build_buy(&params).unwrap();
assert_eq!(instructions.len(), 3);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM_2022);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM);
assert_eq!(instructions[2].accounts.len(), 18);
assert_eq!(instructions[2].data.len(), 25);
}
@@ -873,7 +954,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(&params).unwrap();
let instructions = build_buy(&params).unwrap();
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
}
@@ -885,7 +966,7 @@ mod tests {
params.fixed_output_amount = Some(42);
params.use_exact_sol_amount = Some(true);
let instructions = build_buy_v1(&params).unwrap();
let instructions = build_buy(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
@@ -904,8 +985,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(&params).unwrap();
let instructions = build_buy(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
@@ -922,8 +1007,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(&params).unwrap();
let instructions = build_buy(&params).unwrap();
let transfer_ix = &instructions[1];
let system_ix = bincode::deserialize::<
solana_system_interface::instruction::SystemInstruction,
@@ -942,6 +1031,221 @@ 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_solscan_sol_quote_mint_keeps_legacy_layout() {
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, Pubkey::default());
assert_eq!(params.bonding_curve.quote_mint, crate::constants::WSOL_TOKEN_ACCOUNT);
}
#[test]
fn pumpfun_with_solscan_sol_quote_mint_selects_v1() {
let mut params = swap_params_for_buy(pump_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::SOL_TOKEN_ACCOUNT);
assert_eq!(protocol_params.quote_mint, Pubkey::default());
assert_eq!(
protocol_params.bonding_curve.quote_mint,
crate::constants::WSOL_TOKEN_ACCOUNT
);
}
let ix = build_buy(&params).unwrap().pop().unwrap();
assert_eq!(
&ix.data[..8],
crate::instruction::utils::pumpfun::BUY_EXACT_SOL_IN_DISCRIMINATOR
);
assert_eq!(ix.accounts.len(), 18);
}
#[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(&params).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(&params).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) = &params.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(&params).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(&params).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(&params).unwrap().pop().unwrap();
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
assert!(sell_ix.accounts[8].is_writable);
}
#[test]
fn pumpfun_v1_sell_fixed_output_uses_min_sol_directly() {
let mint = pump_mint();
@@ -952,7 +1256,7 @@ mod tests {
params.create_output_mint_ata = false;
params.fixed_output_amount = Some(42);
let instructions = build_sell_v1(&params).unwrap();
let instructions = build_sell(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR);
@@ -973,8 +1277,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(&params).unwrap();
let instructions = build_sell(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR);
+77 -57
View File
@@ -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` 是单字段
//! structTypeScript 传 `[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)
}
-1
View File
@@ -596,7 +596,6 @@ mod tests {
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: Some(true),
use_pumpfun_v2: false,
}
}
-1
View File
@@ -389,7 +389,6 @@ mod tests {
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
}
}
-1
View File
@@ -401,7 +401,6 @@ mod tests {
check_min_tip: false,
grpc_recv_us: None,
use_exact_sol_amount: None,
use_pumpfun_v2: false,
}
}
+1
View File
@@ -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;
+21 -1
View File
@@ -3,7 +3,9 @@ use once_cell::sync::Lazy;
use smallvec::SmallVec;
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction;
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
const MAX_COMPUTE_BUDGET_CACHE_SIZE: usize = 4_096;
/// Cache key containing all parameters for compute budget instructions
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -17,6 +19,22 @@ struct ComputeBudgetCacheKey {
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, Arc<SmallVec<[Instruction; 2]>>>> =
Lazy::new(|| DashMap::new());
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
/// Extend `instructions` with compute budget instructions; on cache hit extends from cached Arc (no SmallVec clone).
#[inline(always)]
pub fn extend_compute_budget_instructions(
@@ -41,6 +59,7 @@ pub fn extend_compute_budget_instructions(
let arc = Arc::new(insts);
instructions.extend(arc.iter().cloned());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
}
/// Returns compute budget instructions (allocates on cache hit; prefer `extend_compute_budget_instructions` on hot path).
@@ -59,5 +78,6 @@ pub fn compute_budget_instructions(unit_price: u64, unit_limit: u32) -> SmallVec
}
let arc = Arc::new(insts.clone());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
insts
}
+6 -5
View File
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{
@@ -93,19 +94,19 @@ fn build_versioned_transaction(
// 使用预分配的交易构建器以降低延迟
let mut builder = acquire_builder();
let versioned_msg = builder.build_zero_alloc(
let build_result = builder.build_zero_alloc(
&payer.pubkey(),
&full_instructions,
address_lookup_table_account,
blockhash,
);
release_builder(builder);
let versioned_msg = build_result?;
let msg_bytes = versioned_msg.serialize();
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
let signature =
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
// 归还构建器到池
release_builder(builder);
Ok(tx)
}
+26 -7
View File
@@ -46,6 +46,7 @@ use crate::{
const SWQOS_POOL_WORKERS: usize = 18;
const SWQOS_QUEUE_CAP: usize = 128;
const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
const FAST_SUBMIT_RESULT_TIMEOUT: Duration = Duration::from_secs(5);
/// Shared across all jobs in one batch; built once, cloned as single Arc per job (minimal hot-path clone).
struct SwqosSharedContext {
@@ -471,8 +472,31 @@ impl ResultCollector {
}
}
/// Fast submit mode for callers that do not wait for on-chain confirmation.
/// Return as soon as one route accepts, a landed failure consumes the nonce, all routes finish,
/// or the submit result window expires. Slow HTTP routes continue in worker tasks but no longer
/// block post-buy monitoring / sell scheduling.
async fn wait_for_first_submitted(
&self,
timeout: Duration,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let poll_interval = Duration::from_millis(1);
loop {
if self.success_flag.load(Ordering::Acquire)
|| self.landed_failed_flag.load(Ordering::Acquire)
|| self.completed_count.load(Ordering::Acquire) >= self.total_tasks
|| start.elapsed() >= timeout
{
return self.get_first();
}
tokio::time::sleep(poll_interval).await;
}
}
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
#[allow(dead_code)]
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
@@ -627,9 +651,6 @@ pub async fn execute_parallel(
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
let channel_count = selected_task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
let submit_timeout_secs: u64 =
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
@@ -714,12 +735,10 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
let ret = collector.wait_for_first_submitted(FAST_SUBMIT_RESULT_TIMEOUT).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
));
let (success, signatures, last_error, submit_timings) = ret;
+1 -4
View File
@@ -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<i64>,
/// 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<bool>,
/// 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 {
+122 -33
View File
@@ -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, 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.
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
@@ -32,22 +32,30 @@ pub struct PumpFunParams {
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
/// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache).
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致)。
/// 显式传入 Legacy 或 Token-2022 id 时严格按该值组装,不再用 mint 字符串后缀猜测。
pub token_program: Pubkey,
/// Whether to close token account when selling, only effective during sell operations
pub close_token_account_when_sell: Option<bool>,
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值
pub fee_recipient: Pubkey,
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
/// Quote mint layout selector. Default and `SOL_TOKEN_ACCOUNT` use legacy SOL layout.
/// `WSOL_TOKEN_ACCOUNT` selects SOL v2; USDC selects USDC v2.
/// 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 {
#[inline]
fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
Pubkey::default()
} else {
BondingCurveAccount::normalize_quote_mint(quote_mint)
}
}
pub fn immediate_sell(
creator_vault: Pubkey,
token_program: Pubkey,
@@ -63,12 +71,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(
@@ -85,15 +93,51 @@ impl PumpFunParams {
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> 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<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
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(
@@ -117,13 +161,14 @@ 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: Self::quote_mint_for_layout(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()` / `SOL_TOKEN_ACCOUNT` for legacy SOL layout,
/// `WSOL_TOKEN_ACCOUNT` 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`。
@@ -132,12 +177,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<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
@@ -145,16 +191,18 @@ impl PumpFunParams {
mayhem_mode: Option<bool>,
) -> 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(
@@ -176,11 +224,48 @@ 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: Self::quote_mint_for_layout(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<bool>,
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
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,
@@ -201,6 +286,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,
@@ -223,6 +309,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,
@@ -232,8 +319,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: Self::quote_mint_for_layout(quote_mint),
})
}
@@ -272,12 +358,15 @@ impl PumpFunParams {
Ok(self)
}
/// Sets `quote_mint` and enables v2 instructions. Required for USDC-paired coins.
/// For SOL-paired coins, pass `WSOL_TOKEN_ACCOUNT` or leave default.
/// 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.
#[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 = Self::quote_mint_for_layout(quote_mint);
let curve = Arc::make_mut(&mut self.bonding_curve);
*curve = curve.clone().with_quote_mint(effective_quote_mint);
self
}
+5 -5
View File
@@ -17,6 +17,7 @@ const PARALLEL_SENDER_COUNT: usize = 18;
/// 启动时预填充数量,必须 >= PARALLEL_SENDER_COUNT,否则 18 路并发 build 会触发分配或争抢
const TX_BUILDER_POOL_PREFILL: usize = 64;
use anyhow::Result;
use crossbeam_queue::ArrayQueue;
use once_cell::sync::Lazy;
use solana_message::AddressLookupTableAccount;
@@ -82,7 +83,7 @@ impl PreallocatedTxBuilder {
instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Hash,
) -> VersionedMessage {
) -> Result<VersionedMessage> {
self.reset();
self.instructions.extend_from_slice(instructions);
@@ -92,14 +93,13 @@ impl PreallocatedTxBuilder {
&self.instructions,
std::slice::from_ref(alt),
recent_blockhash,
)
.expect("v0 message compile failed");
VersionedMessage::V0(message)
)?;
Ok(VersionedMessage::V0(message))
} else {
// ✅ 没有查找表,使用 Legacy 消息(兼容所有 RPC
let message =
Message::new_with_blockhash(&self.instructions, Some(payer), &recent_blockhash);
VersionedMessage::Legacy(message)
Ok(VersionedMessage::Legacy(message))
}
}
}
+12 -29
View File
@@ -1,22 +1,10 @@
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
use solana_sdk::pubkey::Pubkey;
fn sol_to_lamports(sol: f64) -> u64 {
(sol * 1_000_000_000.0) as u64
}
use crate::{
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
utils::calc::common::compute_fee,
};
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
#[inline]
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
}
/// Calculates the amount of tokens that can be purchased with a given SOL amount
/// using the bonding curve formula.
///
@@ -54,26 +42,21 @@ pub fn get_buy_token_amount_from_sol_amount(
let input_amount = amount_128
.checked_mul(10_000)
.unwrap()
.checked_div(total_fee_basis_points_128 + 10_000)
.unwrap();
.and_then(|v| v.checked_div(total_fee_basis_points_128 + 10_000))
.unwrap_or(0);
let denominator = virtual_sol_reserves + input_amount;
let mut tokens_received =
input_amount.checked_mul(virtual_token_reserves).unwrap().checked_div(denominator).unwrap();
tokens_received = tokens_received.min(real_token_reserves);
if tokens_received <= 100 * 1_000_000_u128 {
tokens_received = if amount > sol_str_to_lamports("0.01").unwrap_or(0) {
25547619 * 1_000_000_u128
} else {
255476 * 1_000_000_u128
};
let Some(denominator) = virtual_sol_reserves.checked_add(input_amount) else { return 0 };
if denominator == 0 {
return 0;
}
tokens_received as u64
let tokens_received = input_amount
.checked_mul(virtual_token_reserves)
.and_then(|v| v.checked_div(denominator))
.unwrap_or(0)
.min(real_token_reserves);
tokens_received.min(u64::MAX as u128) as u64
}
/// Calculates the amount of SOL that will be received when selling a given token amount