Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7326b55f4 | |||
| 7471bcba09 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.18"
|
||||
version = "4.0.20"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
|
||||
@@ -83,11 +83,11 @@ This SDK is available in multiple languages:
|
||||
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.17"`
|
||||
|
||||
This release refreshes PumpFun V2 WSOL quote-pool handling, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
|
||||
This release refreshes PumpFun native-SOL quote handling so SOL/WSOL sentinels prefer the smaller V1 hot path, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, selecting legacy or V2 on-chain instructions as needed (SOL/WSOL + USDC)
|
||||
1. **PumpFun Trading**: Unified SDK-side `buy`, `sell`, and `buy_exact_quote_in` flow, preferring V1 for native SOL and selecting V2 for USDC/non-native quote mints or explicit WSOL settlement
|
||||
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
|
||||
3. **Bonk Trading**: Support for Bonk trading operations
|
||||
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
|
||||
@@ -259,7 +259,8 @@ Optional builder methods:
|
||||
| `.wait_for_all_submits(true)` | In fast-submit mode, wait for all SWQoS lane responses and return all signatures. |
|
||||
| `.simulate(true)` | Build and simulate the transaction instead of sending it. |
|
||||
| `.grpc_recv_us(ts)` | Attach upstream receive timestamp for latency tracing. |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Use durable nonce instead of `recent_blockhash`. |
|
||||
| `.durable_nonce(nonce_info)` | Use durable nonce and clear `recent_blockhash`. Recommended when you start from `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)`. |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Construct params directly with durable nonce instead of `recent_blockhash`. |
|
||||
| `SimpleSellParams::with_tip(false)` | Disable relay tips for sells. Buys use the gas fee strategy/tip settings. |
|
||||
|
||||
`TradeBuyParams` and `TradeSellParams` remain available as advanced low-level APIs. See the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
|
||||
@@ -323,7 +324,29 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- If no custom URL is provided (`None`), the system will use the default endpoint for the specified `SwqosRegion`
|
||||
- This allows for maximum flexibility while maintaining backward compatibility
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. Fetch the latest nonce value and attach it to the high-level buy/sell params:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
recent_blockhash, // will be cleared by `.durable_nonce(...)`
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(nonce_info)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||
|
||||
@@ -403,15 +426,15 @@ PumpFun has two instruction sets for bonding-curve trading:
|
||||
|---|---|---|
|
||||
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
|
||||
| Account metas | 18 | 27 |
|
||||
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
|
||||
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
|
||||
| Quote mint | Native SOL (`default`, Solscan SOL sentinel, or WSOL sentinel) | Non-native quote mint, or explicit WSOL settlement |
|
||||
| Transaction size | Smaller (preferred hot path) | Larger (may require LUT for nonce/tip/ATA-heavy transactions) |
|
||||
|
||||
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` selects the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
|
||||
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` plus the requested settlement token (`pay_with` / `receive_as`) select the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
|
||||
|
||||
**Default: V1**. When `quote_mint` is `Pubkey::default()` or the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), the SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. Passing `WSOL_TOKEN_ACCOUNT` selects SOL V2; passing USDC selects USDC V2.
|
||||
**Default: V1**. When `quote_mint` is `Pubkey::default()`, the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), or `WSOL_TOKEN_ACCOUNT` (`So11111111111111111111111111111111111111112`), the SDK treats the curve as native SOL-paired and uses V1 instructions when `pay_with` / `receive_as` is `SOL`. This is the preferred hot path because it avoids the 27-account V2 layout. Passing USDC or another real quote mint selects V2. Passing `WSOL` as the buy input or sell output selects V2 only when you intentionally want to settle through an existing WSOL ATA.
|
||||
|
||||
**Key changes in v2 instructions:**
|
||||
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
|
||||
- `quote_mint` parameter — native SOL-paired curves may appear as default, Solscan SOL, or WSOL; USDC/non-native quote mints select V2
|
||||
- 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts**
|
||||
- `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory
|
||||
- Same pricing and cost as legacy instructions for SOL-paired coins
|
||||
@@ -419,13 +442,12 @@ The SDK-side builder is version-neutral: callers use the normal buy/sell flow, a
|
||||
|
||||
**Pass `quote_mint` into `PumpFunParams::from_trade`**:
|
||||
|
||||
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()` and Solscan SOL (`So11111111111111111111111111111111111111111`) mean the legacy SOL layout; `WSOL_TOKEN_ACCOUNT` means SOL V2; USDC means USDC V2.
|
||||
When using event/parser data, pass the event's `quote_mint` right after `mint`. `Pubkey::default()`, Solscan SOL (`So11111111111111111111111111111111111111111`), and `WSOL_TOKEN_ACCOUNT` all mean a native SOL-paired curve and default to V1 for normal SOL settlement. USDC means USDC V2.
|
||||
|
||||
```rust
|
||||
// quote_mint is not a PDA. It is the quote SPL mint carried by parser/gRPC events:
|
||||
// - Legacy SOL pool: Pubkey::default() or Solscan SOL from parser data
|
||||
// - SOL V2 pool: WSOL_TOKEN_ACCOUNT
|
||||
// - USDC V2 pool: USDC mint
|
||||
// - Native SOL pool: Pubkey::default(), Solscan SOL, or WSOL sentinel from parser data
|
||||
// - USDC/non-native pool: actual quote SPL mint
|
||||
let quote_mint = e.quote_mint;
|
||||
let params = PumpFunParams::from_trade(
|
||||
e.bonding_curve,
|
||||
@@ -446,11 +468,11 @@ let params = PumpFunParams::from_trade(
|
||||
);
|
||||
```
|
||||
|
||||
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves.
|
||||
For USDC-paired coins, pass `USDC_TOKEN_ACCOUNT` as the buy `input_mint` and sell `output_mint`; SOL/WSOL is only valid for SOL-paired PumpFun curves. For SOL-paired curves, use `SOL` for the normal fast path; use `WSOL` only if you intentionally want V2 settlement through an existing WSOL ATA.
|
||||
When consuming parser events, map `quoteMint`, `virtualQuoteReserves`, and `realQuoteReserves` into `PumpFunParams::from_trade(...)`; USDC pools use `4_292_000_000` as the initial virtual quote reserve.
|
||||
For legacy SOL events where `quote_mint` is `Pubkey::default()` or Solscan SOL, use `virtual_sol_reserves` / `real_sol_reserves` when the quote-reserve fields are absent or zero.
|
||||
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce/tip may exceed `PACKET_DATA_SIZE`. The SDK reports this locally and does not remove compute-budget or tip instructions because that changes priority semantics. Use V1 when the curve is native SOL-paired, pre-create ATAs, or enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
|
||||
#### PumpSwap: coin_creator_vault from events (no RPC)
|
||||
|
||||
|
||||
+35
-13
@@ -83,11 +83,11 @@
|
||||
|
||||
**Rust crate:** `sol-trade-sdk = "4.0.17"`
|
||||
|
||||
本版本刷新 PumpFun V2 WSOL quote 池处理逻辑,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
|
||||
本版本刷新 PumpFun native SOL quote 处理逻辑,SOL/WSOL sentinel 默认优先走更小的 V1 热路径,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,内部按需选择旧版或 V2 链上指令(SOL/WSOL + USDC)
|
||||
1. **PumpFun 交易**: SDK 侧统一为 `buy`、`sell`、`buy_exact_quote_in` 流程,native SOL 优先走 V1,USDC/非 SOL quote 或显式 WSOL 结算才走 V2
|
||||
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
|
||||
3. **Bonk 交易**: 支持 Bonk 的交易操作
|
||||
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
|
||||
@@ -258,7 +258,8 @@ client.buy_simple(buy_params).await?;
|
||||
| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 |
|
||||
| `.simulate(true)` | 只构建并模拟交易,不真正发送。 |
|
||||
| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 使用 durable nonce,不使用 `recent_blockhash`。 |
|
||||
| `.durable_nonce(nonce_info)` | 使用 durable nonce,并清空 `recent_blockhash`。如果你从 `SimpleBuyParams::new(...)` / `SimpleSellParams::new(...)` 开始构造,推荐用这个。 |
|
||||
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | 直接用 durable nonce 构造参数,不使用 `recent_blockhash`。 |
|
||||
| `SimpleSellParams::with_tip(false)` | 关闭卖出交易 relay tip。买入的 tip 使用 gas fee strategy 控制。 |
|
||||
|
||||
`TradeBuyParams` 和 `TradeSellParams` 仍保留为高级低层接口。详细说明见 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
|
||||
@@ -322,7 +323,29 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- 如果没有提供自定义 URL(`None`),系统将使用指定 `SwqosRegion` 的默认端点
|
||||
- 这提供了最大的灵活性,同时保持向后兼容性
|
||||
|
||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||
当使用多个 MEV 服务时,需要使用 `Durable Nonce`。先获取最新 nonce,再挂到新的 buy/sell 参数上:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, AccountPolicy, BuyAmount, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint_pubkey,
|
||||
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
|
||||
DexParamEnum::PumpFun(pumpfun_params),
|
||||
recent_blockhash, // 会被 `.durable_nonce(...)` 清空
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(nonce_info)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
client.buy_simple(buy_params).await?;
|
||||
```
|
||||
|
||||
#### Astralane(Binary / Plain / QUIC)
|
||||
|
||||
@@ -405,22 +428,21 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
|
||||
|
||||
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
|
||||
|
||||
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 自动选择正确的链上 discriminator 和账户布局。
|
||||
SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK 会根据 `quote_mint` 和买/卖的结算 mint 自动选择正确的链上 discriminator 和账户布局。能用 V1 的 native SOL 池会优先用 V1。
|
||||
|
||||
**v2 指令关键变化:**
|
||||
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint
|
||||
- 新增 `quote_mint` 参数 — native SOL 配对可能表现为默认值、Solscan SOL sentinel(`So11111111111111111111111111111111111111111`)或 WSOL sentinel(`So11111111111111111111111111111111111111112`);USDC/其他真实 quote mint 才选择 V2
|
||||
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
|
||||
- `buyback_fee_recipient`、`sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
|
||||
- SOL 配对币的报价和成本与旧版一致,无额外开销
|
||||
|
||||
**使用方式:**
|
||||
|
||||
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint;`Pubkey::default()` 和 Solscan SOL(`So11111111111111111111111111111111111111111`)表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2,USDC 表示 USDC V2:
|
||||
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint 或 native SOL sentinel;`Pubkey::default()`、Solscan SOL(`So11111111111111111111111111111111111111111`)和 `WSOL_TOKEN_ACCOUNT` 都表示 native SOL 配对,正常用 SOL 结算时默认走旧版 V1;USDC 表示 USDC V2:
|
||||
|
||||
```rust
|
||||
// legacy SOL 池:log 事件里可能是 Pubkey::default(),parser 数据里是 Solscan SOL sentinel
|
||||
// SOL V2 池:WSOL_TOKEN_ACCOUNT
|
||||
// USDC 池:就是 USDC mint
|
||||
// native SOL 池:可能是 Pubkey::default()、Solscan SOL sentinel 或 WSOL sentinel
|
||||
// USDC / 非 SOL 池:就是实际 quote SPL mint
|
||||
let quote_mint = e.quote_mint;
|
||||
|
||||
let params = PumpFunParams::from_trade(
|
||||
@@ -446,14 +468,14 @@ client.buy(buy_params).await?;
|
||||
client.sell(sell_params).await?;
|
||||
```
|
||||
|
||||
USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。
|
||||
USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SOL 配对的普通热路径请传 `SOL`,SDK 会用 V1;只有你明确传 `WSOL` 作为买入输入或卖出输出、希望通过已有 WSOL ATA 结算时,才会选择 V2。
|
||||
SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。
|
||||
消费 parser 事件时,需要把 `quoteMint`、`virtualQuoteReserves`、`realQuoteReserves` 传进 `PumpFunParams::from_trade(...)`;USDC 池初始虚拟 quote reserve 是 `4_292_000_000`。
|
||||
legacy SOL 事件里如果 `quote_mint` 是默认值或 Solscan SOL,并且 quote reserve 字段缺失/为 0,应回退使用 `virtual_sol_reserves` / `real_sol_reserves`。
|
||||
|
||||
| quote_mint | 实际使用的指令 | 说明 |
|
||||
|-----------|---------|------|
|
||||
| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
|
||||
| `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
|
||||
| 未设置(默认)/ `SOL_TOKEN_ACCOUNT` (`So111...11111`) / `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | 优先旧版 `buy`/`sell`/`buy_exact_sol_in` | native SOL 配对;普通 SOL 结算走 V1,显式 WSOL 结算才走 V2 |
|
||||
| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) |
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
@@ -30,7 +30,7 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleBuyParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
|
||||
|
||||
@@ -50,7 +50,7 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Optional ALT to reduce transaction size. |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | Whether to wait for chain confirmation before returning. Default is `false`. |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | Fast-submit mode only: wait for every SWQoS lane response and return all signatures. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce info. Use `.durable_nonce(nonce_info)` or `SimpleSellParams::with_durable_nonce(...)`; do not combine with `recent_blockhash`. |
|
||||
| `simulate` | `bool` | ❌ | Build and simulate instead of submitting. Default is `false`. |
|
||||
| `with_tip` | `bool` | ❌ | Whether sells include relay tips. Default is `true`; set with `.with_tip(false)`. |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | Upstream receive timestamp in microseconds for latency tracing. |
|
||||
@@ -74,6 +74,31 @@ Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the
|
||||
| `CreateMissing` | Include ATA creation where possible. | Convenience matters more than smallest transaction size. |
|
||||
| `AssumePrepared` | Do not create or close token accounts; caller prepared everything. | Advanced deterministic flows. |
|
||||
|
||||
### Durable Nonce With Simple Params
|
||||
|
||||
`fetch_nonce_info` and `DurableNonceInfo` are re-exported from the crate root:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
recent_blockhash,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(nonce_info);
|
||||
```
|
||||
|
||||
Calling `.durable_nonce(...)` clears `recent_blockhash`; nonce transactions use the nonce value as the transaction blockhash.
|
||||
|
||||
## TradeBuyParams
|
||||
|
||||
`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags.
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)` 或 `SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 可选 ALT,用于减少交易体积。 |
|
||||
| `wait_tx_confirmed` | `bool` | ❌ | 是否等链上确认后再返回。默认 `false`。 |
|
||||
| `wait_for_all_submits` | `bool` | ❌ | fast-submit 模式下,是否等待所有 SWQoS 通道返回并拿到全部签名。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | durable nonce 信息。使用 `.durable_nonce(nonce_info)` 或 `SimpleSellParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
|
||||
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
|
||||
| `with_tip` | `bool` | ❌ | 卖出交易是否带 relay tip。默认 `true`,可通过 `.with_tip(false)` 关闭。 |
|
||||
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
|
||||
@@ -74,6 +74,31 @@
|
||||
| `CreateMissing` | 尽量在交易内创建缺失 ATA。 | 更重视方便,不追求最小交易体积。 |
|
||||
| `AssumePrepared` | 不创建也不关闭 token account,调用方保证都已准备好。 | 高级确定性流程。 |
|
||||
|
||||
### Simple 参数使用 Durable Nonce
|
||||
|
||||
`fetch_nonce_info` 和 `DurableNonceInfo` 已从 crate root 重新导出:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{fetch_nonce_info, SimpleBuyParams};
|
||||
|
||||
let nonce_info = fetch_nonce_info(&client.infrastructure.rpc, nonce_account)
|
||||
.await
|
||||
.expect("nonce account must be initialized");
|
||||
|
||||
let buy_params = SimpleBuyParams::new(
|
||||
dex_type,
|
||||
pay_with,
|
||||
mint,
|
||||
amount,
|
||||
extension_params,
|
||||
recent_blockhash,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.durable_nonce(nonce_info);
|
||||
```
|
||||
|
||||
调用 `.durable_nonce(...)` 会清空 `recent_blockhash`;nonce 交易会使用 nonce value 作为 transaction blockhash。
|
||||
|
||||
## TradeBuyParams
|
||||
|
||||
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它。
|
||||
|
||||
@@ -14,11 +14,11 @@ use sol_trade_sdk::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
AccountPolicy, BuyAmount, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade,
|
||||
TradeTokenType,
|
||||
AccountPolicy, BuyAmount, DurableNonceInfo, SellAmount, SimpleBuyParams, SimpleSellParams,
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -83,7 +83,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// client.buy_simple(buy_params).await?;
|
||||
let _ = buy_params;
|
||||
|
||||
// Durable nonce is supported by the same high-level params. In production,
|
||||
// fetch it with `sol_trade_sdk::fetch_nonce_info(...)` immediately before
|
||||
// building the transaction.
|
||||
let nonce_buy_params = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
BuyAmount::WithMaxInput { quote_amount: 100_000 },
|
||||
pumpfun_params.clone(),
|
||||
recent_blockhash,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.durable_nonce(DurableNonceInfo {
|
||||
nonce_account: Some(Pubkey::new_unique()),
|
||||
current_nonce: Some(Hash::new_unique()),
|
||||
})
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
let _ = nonce_buy_params;
|
||||
|
||||
let sell_params = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
SellAmount::ExactInput(1_000_000),
|
||||
pumpfun_params.clone(),
|
||||
client.infrastructure.rpc.get_latest_blockhash().await?,
|
||||
gas_fee_strategy.clone(),
|
||||
)
|
||||
.slippage_basis_points(300)
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
// client.sell_simple(sell_params).await?;
|
||||
let _ = sell_params;
|
||||
|
||||
let nonce_sell_params = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
mint,
|
||||
@@ -92,11 +126,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
client.infrastructure.rpc.get_latest_blockhash().await?,
|
||||
gas_fee_strategy,
|
||||
)
|
||||
.slippage_basis_points(300)
|
||||
.durable_nonce(DurableNonceInfo {
|
||||
nonce_account: Some(Pubkey::new_unique()),
|
||||
current_nonce: Some(Hash::new_unique()),
|
||||
})
|
||||
.account_policy(AccountPolicy::HotPathMinimal);
|
||||
|
||||
// client.sell_simple(sell_params).await?;
|
||||
let _ = sell_params;
|
||||
let _ = nonce_sell_params;
|
||||
|
||||
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
|
||||
Ok(())
|
||||
|
||||
+78
-3
@@ -173,9 +173,10 @@ pub struct SimpleBuyParams {
|
||||
pub dex_type: DexType,
|
||||
/// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`.
|
||||
///
|
||||
/// For PumpFun V2 SOL-paired coins, use `TradeTokenType::SOL` even when the
|
||||
/// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote
|
||||
/// account but keeps settlement in native SOL.
|
||||
/// For PumpFun SOL-paired coins, use `TradeTokenType::SOL` for the normal
|
||||
/// fast path even when parser data reports WSOL as the quote sentinel. The
|
||||
/// SDK will prefer V1; choose `WSOL` only when intentionally spending an
|
||||
/// existing WSOL ATA through V2.
|
||||
pub pay_with: TradeTokenType,
|
||||
/// Mint address of the token being bought.
|
||||
pub mint: Pubkey,
|
||||
@@ -335,6 +336,16 @@ impl SimpleBuyParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
|
||||
///
|
||||
/// This clears `recent_blockhash` because nonce transactions must use the
|
||||
/// nonce value as their transaction blockhash.
|
||||
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
|
||||
self.durable_nonce = Some(value);
|
||||
self.recent_blockhash = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait for confirmation before returning.
|
||||
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
|
||||
self.wait_tx_confirmed = value;
|
||||
@@ -444,6 +455,16 @@ impl SimpleSellParams {
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
|
||||
///
|
||||
/// This clears `recent_blockhash` because nonce transactions must use the
|
||||
/// nonce value as their transaction blockhash.
|
||||
pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
|
||||
self.durable_nonce = Some(value);
|
||||
self.recent_blockhash = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Wait for confirmation before returning.
|
||||
pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
|
||||
self.wait_tx_confirmed = value;
|
||||
@@ -1910,6 +1931,33 @@ mod tests {
|
||||
assert!(!low.wait_tx_confirmed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_buy_builder_can_use_durable_nonce() {
|
||||
let nonce_account = Pubkey::new_unique();
|
||||
let nonce_hash = Hash::new_unique();
|
||||
let durable_nonce = DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(nonce_hash),
|
||||
};
|
||||
|
||||
let simple = SimpleBuyParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
BuyAmount::ExactInput(10_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.durable_nonce(durable_nonce.clone());
|
||||
|
||||
let low: TradeBuyParams = simple.into();
|
||||
|
||||
assert!(low.recent_blockhash.is_none());
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sell_auto_creates_non_sol_output_ata() {
|
||||
let simple = SimpleSellParams {
|
||||
@@ -1939,4 +1987,31 @@ mod tests {
|
||||
assert!(!low.close_output_token_ata);
|
||||
assert!(!low.close_mint_token_ata);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_sell_builder_can_use_durable_nonce() {
|
||||
let nonce_account = Pubkey::new_unique();
|
||||
let nonce_hash = Hash::new_unique();
|
||||
let durable_nonce = DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(nonce_hash),
|
||||
};
|
||||
|
||||
let simple = SimpleSellParams::new(
|
||||
DexType::PumpFun,
|
||||
TradeTokenType::SOL,
|
||||
Pubkey::new_unique(),
|
||||
SellAmount::ExactInput(50_000),
|
||||
dummy_pumpfun_params(),
|
||||
Hash::new_unique(),
|
||||
GasFeeStrategy::new(),
|
||||
)
|
||||
.durable_nonce(durable_nonce.clone());
|
||||
|
||||
let low: TradeSellParams = simple.into();
|
||||
|
||||
assert!(low.recent_blockhash.is_none());
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
|
||||
assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
|
||||
}
|
||||
}
|
||||
|
||||
+138
-25
@@ -1,10 +1,11 @@
|
||||
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
|
||||
//!
|
||||
//! The SDK selects the legacy or V2 on-chain layout from `PumpFunParams.quote_mint`.
|
||||
//! `Pubkey::default()` and the Solscan SOL sentinel keep the smaller legacy SOL layout;
|
||||
//! explicit WSOL or USDC uses the V2 27/26-account unified metas.
|
||||
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
|
||||
//! Native SOL-paired coins (`Pubkey::default()`, Solscan SOL sentinel, or WSOL sentinel)
|
||||
//! keep the smaller legacy SOL layout by default. Non-native quote mints such as USDC use
|
||||
//! the V2 27/26-account unified metas.
|
||||
|
||||
use crate::swqos::TradeType;
|
||||
use crate::{
|
||||
common::bonding_curve::BondingCurveAccount,
|
||||
common::spl_token::close_account,
|
||||
@@ -52,8 +53,8 @@ fn effective_pump_mint_token_program(protocol_params: &PumpFunParams, mint: &Pub
|
||||
}
|
||||
|
||||
/// Resolve quote mint and its token program from PumpFunParams.
|
||||
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` means legacy SOL-paired; use WSOL mint when a
|
||||
/// downstream V2 helper needs a concrete SPL quote mint.
|
||||
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` / `WSOL_TOKEN_ACCOUNT` are native SOL-paired.
|
||||
/// V2 helpers still need a concrete SPL mint, so native SOL resolves to WSOL internally.
|
||||
#[inline]
|
||||
fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) {
|
||||
let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint();
|
||||
@@ -73,6 +74,16 @@ fn is_sol_quote_mint(quote_mint: &Pubkey) -> bool {
|
||||
|| *quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_native_sol_settlement_mint(mint: &Pubkey) -> bool {
|
||||
*mint == crate::constants::SOL_TOKEN_ACCOUNT || *mint == Pubkey::default()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_explicit_wsol_settlement_mint(mint: &Pubkey) -> bool {
|
||||
*mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn validate_v2_buy_quote_mint(input_mint: &Pubkey, quote_mint: &Pubkey) -> Result<()> {
|
||||
if is_sol_quote_mint(quote_mint) {
|
||||
@@ -130,9 +141,20 @@ fn should_use_v2_layout(params: &SwapParams) -> Result<bool> {
|
||||
.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("e_mint))
|
||||
if !is_sol_quote_mint("e_mint) {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Pump docs treat WSOL quote mint as the native SOL sentinel for SOL-paired curves.
|
||||
// Keep V1 for native SOL settlement; use V2 only when the caller explicitly wants to
|
||||
// spend/receive an existing WSOL ATA.
|
||||
Ok(match params.trade_type {
|
||||
TradeType::Buy | TradeType::CreateAndBuy => {
|
||||
is_explicit_wsol_settlement_mint(¶ms.input_mint)
|
||||
}
|
||||
TradeType::Sell => is_explicit_wsol_settlement_mint(¶ms.output_mint),
|
||||
TradeType::Create => false,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -171,6 +193,13 @@ fn build_buy_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
.downcast_ref::<PumpFunParams>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
||||
|
||||
if !is_native_sol_settlement_mint(¶ms.input_mint) {
|
||||
return Err(anyhow!(
|
||||
"PumpFun native SOL buy expects input_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL input only when spending an existing WSOL ATA.",
|
||||
params.input_mint
|
||||
));
|
||||
}
|
||||
|
||||
let lamports_in = params.input_amount.unwrap_or(0);
|
||||
if lamports_in == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
@@ -299,6 +328,13 @@ fn build_sell_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
.downcast_ref::<PumpFunParams>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
||||
|
||||
if !is_native_sol_settlement_mint(¶ms.output_mint) {
|
||||
return Err(anyhow!(
|
||||
"PumpFun native SOL sell expects output_mint SOL; got {}. Use the matching non-native quote mint for V2 pools or WSOL output only when receiving into an existing WSOL ATA.",
|
||||
params.output_mint
|
||||
));
|
||||
}
|
||||
|
||||
let token_amount = if let Some(amount) = params.input_amount {
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
@@ -1002,14 +1038,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_fixed_output_uses_buy_with_max_input_budget() {
|
||||
fn pumpfun_v2_usdc_fixed_output_uses_buy_with_max_input_budget() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.fixed_output_amount = Some(42);
|
||||
params.use_exact_sol_amount = Some(true);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
@@ -1024,7 +1061,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() {
|
||||
fn pumpfun_wsol_quote_regular_buy_uses_v1_native_sol() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
@@ -1043,13 +1080,13 @@ mod tests {
|
||||
params.slippage_basis_points.unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
|
||||
assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected);
|
||||
assert_eq!(buy_ix.accounts.len(), 27);
|
||||
assert_eq!(buy_ix.accounts.len(), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_regular_buy_skips_wsol_ata_create_on_hot_path() {
|
||||
fn pumpfun_wsol_quote_regular_buy_skips_wsol_ata_create_on_hot_path() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.create_input_mint_ata = true;
|
||||
@@ -1065,13 +1102,15 @@ mod tests {
|
||||
assert_eq!(instructions.len(), 1);
|
||||
assert_eq!(
|
||||
&instructions.last().unwrap().data[..8],
|
||||
crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR
|
||||
crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() {
|
||||
fn pumpfun_v2_explicit_wsol_input_with_output_ata_create_reports_oversized_without_dropping_priority(
|
||||
) {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.create_output_mint_ata = true;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
@@ -1080,6 +1119,41 @@ mod tests {
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let business_instructions = build_buy(¶ms).unwrap();
|
||||
let err = crate::trading::common::transaction_builder::build_transaction(
|
||||
¶ms.payer,
|
||||
150_000,
|
||||
500_000,
|
||||
&business_instructions,
|
||||
None,
|
||||
Some(solana_hash::Hash::new_unique()),
|
||||
None,
|
||||
"PumpFun",
|
||||
true,
|
||||
true,
|
||||
&Pubkey::new_unique(),
|
||||
0.001,
|
||||
None,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("transaction too large"), "{err}");
|
||||
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_explicit_wsol_input_hot_path_transaction_fits_when_output_ata_prepared() {
|
||||
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
|
||||
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.create_input_mint_ata = true;
|
||||
params.create_output_mint_ata = false;
|
||||
params.use_exact_sol_amount = Some(false);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let business_instructions = build_buy(¶ms).unwrap();
|
||||
let transaction = crate::trading::common::transaction_builder::build_transaction(
|
||||
¶ms.payer,
|
||||
@@ -1107,7 +1181,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_buy_v2() {
|
||||
fn pumpfun_from_trade_wsol_quote_regular_buy_selects_v1() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
@@ -1135,8 +1209,8 @@ mod tests {
|
||||
let instructions = build_buy(¶ms).unwrap();
|
||||
let buy_ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
|
||||
assert_eq!(buy_ix.accounts.len(), 27);
|
||||
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
|
||||
assert_eq!(buy_ix.accounts.len(), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1161,6 +1235,44 @@ mod tests {
|
||||
assert_eq!(instructions[0].accounts.len(), 27);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_sell_does_not_select_v2_from_base_mint_wsol() {
|
||||
let mut params = swap_params_for_buy(crate::constants::WSOL_TOKEN_ACCOUNT, TOKEN_PROGRAM);
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_sell(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_DISCRIMINATOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_sell_selects_v2_for_explicit_wsol_output_settlement() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = mint;
|
||||
params.output_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_sell(¶ms).unwrap();
|
||||
let ix = instructions.last().unwrap();
|
||||
|
||||
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpfun::SELL_V2_DISCRIMINATOR);
|
||||
assert_eq!(ix.accounts.len(), 26);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_usdc_dev_trade_uses_usdc_initial_quote_reserves() {
|
||||
let mint = pump_mint();
|
||||
@@ -1372,13 +1484,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_buyback_fee_recipient_is_writable() {
|
||||
fn pumpfun_v2_usdc_buyback_fee_recipient_is_writable() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.create_output_mint_ata = false;
|
||||
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let buy_ix = build_buy(¶ms).unwrap().pop().unwrap();
|
||||
@@ -1387,7 +1500,7 @@ mod tests {
|
||||
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = mint;
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let sell_ix = build_sell(¶ms).unwrap().pop().unwrap();
|
||||
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
|
||||
assert!(sell_ix.accounts[8].is_writable);
|
||||
@@ -1416,17 +1529,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_v2_sell_fixed_output_uses_min_sol_directly() {
|
||||
fn pumpfun_v2_usdc_sell_fixed_output_uses_min_quote_directly() {
|
||||
let mint = pump_mint();
|
||||
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
|
||||
params.trade_type = crate::swqos::TradeType::Sell;
|
||||
params.input_mint = mint;
|
||||
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
params.create_output_mint_ata = false;
|
||||
params.fixed_output_amount = Some(42);
|
||||
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
|
||||
*protocol_params =
|
||||
protocol_params.clone().with_quote_mint(crate::constants::WSOL_TOKEN_ACCOUNT);
|
||||
protocol_params.clone().with_quote_mint(crate::constants::USDC_TOKEN_ACCOUNT);
|
||||
}
|
||||
|
||||
let instructions = build_sell(¶ms).unwrap();
|
||||
|
||||
@@ -7,6 +7,7 @@ pub mod swqos;
|
||||
pub mod trading;
|
||||
pub mod utils;
|
||||
|
||||
pub use crate::common::nonce_cache::{fetch_nonce_info, DurableNonceInfo};
|
||||
// Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode)
|
||||
pub use crate::swqos::{AstralaneTransport, SwqosTransport};
|
||||
pub use client::{
|
||||
|
||||
@@ -46,7 +46,7 @@ pub fn build_transaction(
|
||||
tip_amount: f64,
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let transaction = build_transaction_with_compute_budget(
|
||||
let transaction = build_transaction_inner(
|
||||
payer,
|
||||
unit_limit,
|
||||
unit_price,
|
||||
@@ -63,44 +63,32 @@ pub fn build_transaction(
|
||||
)?;
|
||||
|
||||
let serialized_len = bincode::serialized_size(&transaction)? as usize;
|
||||
if serialized_len <= PACKET_DATA_SIZE
|
||||
|| durable_nonce.is_some()
|
||||
|| !with_tip
|
||||
|| tip_amount <= 0.0
|
||||
|| (unit_limit == 0 && unit_price == 0)
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
" [SDK][tx-size ] {} {} serialized={} bytes, business_ix={}, nonce={}, tip={}, cu_limit={}, cu_price={}, alt={}",
|
||||
protocol_name,
|
||||
if is_buy { "buy" } else { "sell" },
|
||||
serialized_len,
|
||||
business_instructions.len(),
|
||||
durable_nonce.is_some(),
|
||||
with_tip && tip_amount > 0.0,
|
||||
unit_limit,
|
||||
unit_price,
|
||||
address_lookup_table_account.is_some()
|
||||
);
|
||||
}
|
||||
if serialized_len <= PACKET_DATA_SIZE {
|
||||
return Ok(transaction);
|
||||
}
|
||||
|
||||
let compact_transaction = build_transaction_with_compute_budget(
|
||||
payer,
|
||||
0,
|
||||
0,
|
||||
business_instructions,
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
middleware_manager,
|
||||
protocol_name,
|
||||
is_buy,
|
||||
with_tip,
|
||||
tip_account,
|
||||
tip_amount,
|
||||
durable_nonce,
|
||||
)?;
|
||||
let compact_len = bincode::serialized_size(&compact_transaction)? as usize;
|
||||
if compact_len <= PACKET_DATA_SIZE {
|
||||
return Ok(compact_transaction);
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting",
|
||||
"transaction too large: {} > {}; SDK did not remove compute budget or relay tip because that changes transaction priority semantics. Use an address lookup table or pre-create token ATAs before submitting",
|
||||
serialized_len,
|
||||
PACKET_DATA_SIZE,
|
||||
compact_len
|
||||
PACKET_DATA_SIZE
|
||||
))
|
||||
}
|
||||
|
||||
fn build_transaction_with_compute_budget(
|
||||
fn build_transaction_inner(
|
||||
payer: &Arc<Keypair>,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
@@ -181,3 +169,41 @@ fn build_versioned_transaction(
|
||||
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
|
||||
fn oversized_instruction(account_count: usize, data_len: usize) -> Instruction {
|
||||
let accounts =
|
||||
(0..account_count).map(|_| AccountMeta::new(Pubkey::new_unique(), false)).collect();
|
||||
Instruction { program_id: Pubkey::new_unique(), accounts, data: vec![7; data_len] }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_transaction_returns_error_without_dropping_priority_semantics() {
|
||||
let payer = Arc::new(Keypair::new());
|
||||
let business_instructions = vec![oversized_instruction(36, 700)];
|
||||
let err = build_transaction(
|
||||
&payer,
|
||||
80_000,
|
||||
100_000,
|
||||
&business_instructions,
|
||||
None,
|
||||
Some(Hash::new_unique()),
|
||||
None,
|
||||
"test",
|
||||
true,
|
||||
true,
|
||||
&Pubkey::new_unique(),
|
||||
0.001,
|
||||
None,
|
||||
)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(err.contains("transaction too large"), "{err}");
|
||||
assert!(err.contains("did not remove compute budget or relay tip"), "{err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,10 @@ use std::sync::Arc;
|
||||
/// **Buy/sell**:`creator_vault` 及(若可得)**`tradeEvent` / CPI 日志中的 `creator`** 优先于陈旧的曲线快照;
|
||||
/// ix 组装与链下询价见 [`Self::effective_creator_for_trade`]、[`crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing`]。
|
||||
///
|
||||
/// **V2 instructions**: The SDK selects the layout automatically from `quote_mint`.
|
||||
/// Leave `quote_mint` default, or pass the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), for
|
||||
/// legacy SOL layout. Pass `WSOL_TOKEN_ACCOUNT` for SOL V2, or USDC for USDC-paired coins.
|
||||
/// **Instruction layout**: The SDK selects the smallest valid layout automatically from
|
||||
/// `quote_mint`. Native SOL-paired coins use the legacy SOL layout when `quote_mint`
|
||||
/// is default, the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), or the WSOL sentinel
|
||||
/// (`WSOL_TOKEN_ACCOUNT`). Non-native quote mints such as USDC use V2.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
@@ -41,8 +42,9 @@ pub struct PumpFunParams {
|
||||
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
|
||||
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值。
|
||||
pub fee_recipient: Pubkey,
|
||||
/// Quote mint layout selector. Default and `SOL_TOKEN_ACCOUNT` use legacy SOL layout.
|
||||
/// `WSOL_TOKEN_ACCOUNT` selects SOL v2; USDC selects USDC v2.
|
||||
/// Quote mint layout selector. Default, `SOL_TOKEN_ACCOUNT`, and `WSOL_TOKEN_ACCOUNT`
|
||||
/// are native SOL-paired and use the smaller legacy SOL layout by default.
|
||||
/// USDC and other non-native quote mints select V2.
|
||||
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
|
||||
pub quote_mint: Pubkey,
|
||||
}
|
||||
@@ -50,7 +52,10 @@ pub struct PumpFunParams {
|
||||
impl PumpFunParams {
|
||||
#[inline]
|
||||
fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey {
|
||||
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
|
||||
if quote_mint == Pubkey::default()
|
||||
|| quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
{
|
||||
Pubkey::default()
|
||||
} else {
|
||||
BondingCurveAccount::normalize_quote_mint(quote_mint)
|
||||
@@ -59,7 +64,10 @@ impl PumpFunParams {
|
||||
|
||||
#[inline]
|
||||
fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey {
|
||||
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
|
||||
if quote_mint == Pubkey::default()
|
||||
|| quote_mint == crate::constants::SOL_TOKEN_ACCOUNT
|
||||
|| quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
{
|
||||
crate::constants::SOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
BondingCurveAccount::normalize_quote_mint(quote_mint)
|
||||
@@ -176,8 +184,8 @@ impl PumpFunParams {
|
||||
}
|
||||
|
||||
/// Build PumpFun params from event/parser data. Pass `quote_mint` from the event:
|
||||
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` for legacy SOL layout,
|
||||
/// `WSOL_TOKEN_ACCOUNT` for SOL V2, and `USDC_TOKEN_ACCOUNT` for USDC V2.
|
||||
/// `Pubkey::default()` / `SOL_TOKEN_ACCOUNT` / `WSOL_TOKEN_ACCOUNT` for native SOL
|
||||
/// layout, and `USDC_TOKEN_ACCOUNT` or another non-native quote mint for V2.
|
||||
/// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts.
|
||||
///
|
||||
/// `mayhem_mode`:
|
||||
@@ -369,8 +377,9 @@ impl PumpFunParams {
|
||||
}
|
||||
|
||||
/// Sets `quote_mint`. The instruction builder derives V1/V2 layout from this value.
|
||||
/// For legacy SOL-paired coins, pass `SOL_TOKEN_ACCOUNT` or leave default.
|
||||
/// Pass `WSOL_TOKEN_ACCOUNT` only when you intentionally want SOL v2 layout.
|
||||
/// For native SOL-paired coins, pass `Pubkey::default()`, `SOL_TOKEN_ACCOUNT`, or
|
||||
/// `WSOL_TOKEN_ACCOUNT`; all three select the smaller V1 layout unless buy/sell params
|
||||
/// explicitly request WSOL settlement. Pass USDC or another non-native quote mint for V2.
|
||||
#[inline]
|
||||
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
|
||||
let effective_quote_mint = BondingCurveAccount::normalize_quote_mint(quote_mint);
|
||||
@@ -419,6 +428,10 @@ mod tests {
|
||||
PumpFunParams::quote_mint_for_rpc_return(crate::constants::SOL_TOKEN_ACCOUNT),
|
||||
crate::constants::SOL_TOKEN_ACCOUNT
|
||||
);
|
||||
assert_eq!(
|
||||
PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT),
|
||||
crate::constants::SOL_TOKEN_ACCOUNT
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -427,9 +440,22 @@ mod tests {
|
||||
PumpFunParams::quote_mint_for_rpc_return(crate::constants::USDC_TOKEN_ACCOUNT),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quote_mint_for_layout_normalizes_native_sol_sentinels_to_default() {
|
||||
assert_eq!(PumpFunParams::quote_mint_for_layout(Pubkey::default()), Pubkey::default());
|
||||
assert_eq!(
|
||||
PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
PumpFunParams::quote_mint_for_layout(crate::constants::SOL_TOKEN_ACCOUNT),
|
||||
Pubkey::default()
|
||||
);
|
||||
assert_eq!(
|
||||
PumpFunParams::quote_mint_for_layout(crate::constants::WSOL_TOKEN_ACCOUNT),
|
||||
Pubkey::default()
|
||||
);
|
||||
assert_eq!(
|
||||
PumpFunParams::quote_mint_for_layout(crate::constants::USDC_TOKEN_ACCOUNT),
|
||||
crate::constants::USDC_TOKEN_ACCOUNT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user