Compare commits

..

10 Commits

Author SHA1 Message Date
0xfnzero d7326b55f4 fix pumpfun native sol quote routing 2026-06-08 03:13:04 +08:00
0xfnzero 7471bcba09 Release v4.0.19 2026-06-08 00:00:32 +08:00
0xfnzero b8325b2b52 Release v4.0.18 2026-06-07 22:22:26 +08:00
0xfnzero e459f175f5 Release sol-trade-sdk v4.0.17 2026-06-07 01:42:08 +08:00
0xfnzero e5e58bef89 Release sol-trade-sdk v4.0.16 2026-06-06 12:31:08 +08:00
0xfnzero 9874cd5358 Release sol-trade-sdk v4.0.15 2026-06-01 14:57:49 +08:00
0xfnzero 1d3c0ff5da fix: force Token-2022 for pump suffix mints 2026-05-28 05:21:24 +08:00
Wood 34f400e89d Merge pull request #103 from HelvetiCrypt/wait-for-all-submits
Add SwapParams.wait_for_all_submits to restore all-routes signature collection
2026-05-28 02:46:39 +08:00
HelvetiCrypt bd1772bed2 Add SwapParams.wait_for_all_submits to restore all-routes signature collection
v4.0.11 switched the fast-submit (`wait_tx_confirmed = false`) result loop
from `wait_for_all_submitted` to `wait_for_first_submitted`. The new
function returns as soon as one SWQOS route's HTTP submit completes and
drains whatever signatures are in `ResultCollector.results` at that
moment — slower routes' signatures arrive after the caller has returned.

This is correct for fire-and-forget low-latency submits, but breaks
callers that do their own on-chain confirmation against a pinned durable
nonce. Only one of the submitted txs can land (nonce is consumed by
whichever lands first), but the caller has no way of knowing in advance
which route it will be. Their post-submit logic feeds every returned
signature to `getSignatureStatuses` to find the landed one — with only
the fastest HTTP responder returned (often not the one that lands), the
landed tx isn't in the polled set.

Adds an opt-in `SwapParams.wait_for_all_submits: bool` (default false,
plumbed through `TradeBuyParams` / `TradeSellParams`). When true and
`wait_tx_confirmed = false`, `execute_parallel` uses
`wait_for_all_submitted` (still in the codebase, was
`#[allow(dead_code)]`) so every submitted signature is returned. Zero
behaviour change for current users.
2026-05-27 08:02:49 +00:00
0xfnzero 5449d51591 Release sol-trade-sdk 4.0.14 2026-05-25 22:01:15 +08:00
35 changed files with 1912 additions and 156 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.13"
version = "4.0.20"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -34,6 +34,7 @@ members = [
"examples/cli_trading",
"examples/gas_fee_strategy",
"examples/meteora_damm_v2_direct_trading",
"examples/simple_trading",
]
[lib]
+104 -40
View File
@@ -79,19 +79,26 @@ This SDK is available in multiple languages:
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
## 🔖 Current Release
**Rust crate:** `sol-trade-sdk = "4.0.17"`
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 + 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
5. **Raydium AMM V4 Trading**: Support for Raydium AMM V4 (Automated Market Maker) trading operations
6. **Meteora DAMM V2 Trading**: Support for Meteora DAMM V2 (Dynamic AMM) trading operations
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane and other services
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
8. **Concurrent Trading**: Submit through every configured SWQoS provider plus the default RPC lane; the first accepted result can return early while slower routes continue submitting
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage
12. **Hot-Path RPC Boundary**: Trade execution uses caller-supplied blockhash or durable nonce and never queries RPC for blockhash, account, or balance data
## 📦 Installation
@@ -108,14 +115,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.13" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.13"
sol-trade-sdk = "4.0.17"
```
## 🛠️ Usage Examples
@@ -189,40 +196,74 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001,
For detailed information about all trading parameters, see the [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
```rust
// Import DexParamEnum for protocol-specific parameters
use sol_trade_sdk::trading::core::params::DexParamEnum;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpSwap,
input_token_type: TradeTokenType::WSOL,
mint: mint_pubkey,
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
// Use DexParamEnum for type-safe protocol parameters (zero-overhead abstraction)
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: None, // Optional: specify exact output amount
gas_fee_strategy: gas_fee_strategy.clone(), // Gas fee strategy configuration
simulate: false, // Set to true for simulation only
use_exact_sol_amount: None, // Use exact SOL input for PumpFun/PumpSwap (defaults to true)
use sol_trade_sdk::{
AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType,
trading::core::params::DexParamEnum,
};
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
// Token used to pay. For PumpFun V2 SOL/WSOL quote pools, keep this as SOL
// when you want to spend native SOL; the SDK will still use V2 accounts.
TradeTokenType::SOL,
// Mint of the meme/token you want to buy.
mint_pubkey,
// Regular PumpFun/PumpSwap buy. The SDK estimates token output and applies
// slippage to the maximum quote cost.
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
// Protocol state from parser/RPC cache, for example PumpFunParams::from_trade(...).
DexParamEnum::PumpFun(pumpfun_params),
// Pass a cached recent blockhash; the SDK does not fetch it on the hot path.
recent_blockhash,
gas_fee_strategy.clone(),
)
// 300 = 3%.
.slippage_basis_points(300)
// For bots/sniping: assume ATAs are already prepared and keep the tx small.
.account_policy(AccountPolicy::HotPathMinimal);
```
#### 4. Execute Trading
```rust
client.buy(buy_params).await?;
client.buy_simple(buy_params).await?;
```
### ⚡ Trading Parameters
For comprehensive information about all trading parameters including `TradeBuyParams` and `TradeSellParams`, see the dedicated [Trading Parameters Reference](docs/TRADING_PARAMETERS.md).
Use `SimpleBuyParams` / `SimpleSellParams` for new integrations. They describe trading intent and hide low-level ATA flags. Most users only choose:
- `pay_with` / `receive_as`: quote token direction. Use `SOL` when the wallet spends or receives native SOL. For PumpFun V2 SOL-paired pools whose quote mint is WSOL, still use `SOL` if you want native SOL settlement.
- `amount`: trade sizing intent. Pick one enum variant instead of combining `input_token_amount`, `fixed_output_token_amount`, and `use_exact_sol_amount`.
- `account_policy`: account creation behavior. Bots usually use `HotPathMinimal`; normal apps can keep the default `Auto`.
| Parameter | Meaning | Recommendation |
|---|---|---|
| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum output. | Normal swaps |
| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy with slippage applied to max quote cost. | Sniping/arbitrage |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount with a max quote budget. | Exact-output workflows |
| `SellAmount::ExactInput(amount)` | Sell exactly this token amount. | Normal sells |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where the DEX supports it. | Exact-output sells |
| `AccountPolicy::Auto` | SDK creates practical ATAs when needed. | General usage |
| `AccountPolicy::HotPathMinimal` | Avoid ATA create/close instructions in the trade tx. | Bots, sniping, latency-sensitive flows |
| `AccountPolicy::CreateMissing` | Include ATA creation instructions where possible. | Convenience over transaction size |
| `AccountPolicy::AssumePrepared` | Caller prepared every required ATA. | Deterministic advanced flows |
Optional builder methods:
| Method | Meaning |
|---|---|
| `.slippage_basis_points(300)` | Set slippage. `300` means 3%. |
| `.address_lookup_table_account(alt)` | Attach an ALT to reduce transaction size. Useful for large PumpFun V2 transactions. |
| `.wait_tx_confirmed(true)` | Return only after confirmation. Usually disabled for fastest submit paths. |
| `.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. |
| `.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).
#### About ShredStream
@@ -233,6 +274,7 @@ Please ensure that the parameters your trading logic depends on are available in
| Description | Run Command | Source Code |
|-------------|-------------|-------------|
| Simple buy/sell parameter API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
| Create and configure TradingClient instance | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
| Share infrastructure across multiple wallets | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
| PumpFun token sniping trading | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
@@ -282,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)
@@ -362,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()`, the SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. Passing an explicit quote mint selects V2: WSOL/native SOL for SOL-paired V2, or USDC for USDC-paired pools.
**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
@@ -378,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()` means the legacy SOL layout; explicit native SOL or WSOL means the SOL V2 layout; USDC means the USDC V2 layout.
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() from log events
// - SOL V2 pool: native SOL or WSOL from parser data
// - 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,
@@ -405,10 +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)
+102 -37
View File
@@ -79,19 +79,26 @@
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
## 🔖 当前版本
**Rust crate:** `sol-trade-sdk = "4.0.17"`
本版本刷新 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 + USDC
1. **PumpFun 交易**: SDK 侧统一为 `buy``sell``buy_exact_quote_in` 流程,native SOL 优先走 V1USDC/非 SOL quote 或显式 WSOL 结算才走 V2
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
3. **Bonk 交易**: 支持 Bonk 的交易操作
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
8. **并发交易**: 所有已配置的 SWQoS 通道和默认 RPC 通道都会发出提交;首个成功只影响返回,较慢通道会继续提交
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
12. **热路径 RPC 边界**: 交易执行使用调用方传入的 blockhash 或 durable nonce,不在热路径查询 blockhash、账户或余额
## 📦 安装
@@ -108,14 +115,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.13" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.13"
sol-trade-sdk = "4.0.17"
```
## 🛠️ 使用示例
@@ -188,40 +195,74 @@ gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001,
有关所有交易参数的详细信息,请参阅 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
```rust
// 导入 DexParamEnum 用于协议特定参数
use sol_trade_sdk::trading::core::params::DexParamEnum;
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpSwap,
input_token_type: TradeTokenType::WSOL,
mint: mint_pubkey,
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
// 使用 DexParamEnum 实现类型安全的协议参数(零开销抽象)
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: None, // 可选:指定精确输出数量
gas_fee_strategy: gas_fee_strategy.clone(), // Gas 费用策略配置
simulate: false, // 设为 true 仅进行模拟
use_exact_sol_amount: None, // 对 PumpFun/PumpSwap 使用精确 SOL 输入(默认为 true)
use sol_trade_sdk::{
AccountPolicy, BuyAmount, DexType, SimpleBuyParams, TradeTokenType,
trading::core::params::DexParamEnum,
};
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
// 支付币种。PumpFun V2 的 SOL/WSOL quote 池,如果你想花原生 SOL,
// 这里仍然传 SOL;SDK 内部会按 V2 账户布局处理。
TradeTokenType::SOL,
// 要买入的 meme/token mint。
mint_pubkey,
// 常规 PumpFun/PumpSwap buy。SDK 先估算能买到多少 token,
// 再把滑点应用到最大 quote 成本上。
BuyAmount::WithMaxInput { quote_amount: buy_sol_amount },
// 协议状态参数,通常来自 parser/RPC 缓存,例如 PumpFunParams::from_trade(...)。
DexParamEnum::PumpFun(pumpfun_params),
// 传入外部缓存的 recent_blockhashSDK 不在热路径里临时获取。
recent_blockhash,
gas_fee_strategy.clone(),
)
// 300 = 3%。
.slippage_basis_points(300)
// Bot/狙击推荐:假设 ATA 已提前准备好,交易内不创建/关闭 ATA,体积更小。
.account_policy(AccountPolicy::HotPathMinimal);
```
#### 4. 执行交易
```rust
client.buy(buy_params).await?;
client.buy_simple(buy_params).await?;
```
### ⚡ 交易参数
有关所有交易参数(包括 `TradeBuyParams` `TradeSellParams`)的详细信息,请参阅专门的 [交易参数参考手册](docs/TRADING_PARAMETERS_CN.md)。
新接入建议优先使用 `SimpleBuyParams` / `SimpleSellParams`。它们描述交易意图,SDK 内部处理底层 ATA 参数。多数用户只需要选择:
- `pay_with` / `receive_as`:买入时用什么 quote 支付,卖出时收什么 quote。钱包实际花/收原生 SOL 就传 `SOL`。PumpFun V2 的 SOL 配对池虽然 `quote_mint` 是 WSOL,但你想用原生 SOL 结算时这里仍传 `SOL`
- `amount`:交易数量语义。用一个枚举表达意图,不再同时理解 `input_token_amount``fixed_output_token_amount``use_exact_sol_amount`
- `account_policy`:账户创建策略。Bot 通常用 `HotPathMinimal`;普通应用可以保留默认 `Auto`
| 参数 | 含义 | 推荐场景 |
|---|---|---|
| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量;滑点保护最小买到数量。 | 普通买入 |
| `BuyAmount::WithMaxInput { quote_amount }` | PumpFun/PumpSwap 常规 buy,滑点作用在最大 quote 成本上。 | 狙击、套利 |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最大 quote 成本。 | 精确输出 |
| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量。 | 普通卖出 |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | 精确输出卖出 |
| `AccountPolicy::Auto` | SDK 按交易路径创建必要 ATA。 | 普通用户 |
| `AccountPolicy::HotPathMinimal` | 交易内避免创建/关闭 ATA。 | Bot、狙击、低延迟 |
| `AccountPolicy::CreateMissing` | 尽量在交易内创建缺失 ATA。 | 优先方便,不追求最小交易体积 |
| `AccountPolicy::AssumePrepared` | 调用方保证所有 ATA 已准备好。 | 高级确定性流程 |
可选 builder 方法:
| 方法 | 含义 |
|---|---|
| `.slippage_basis_points(300)` | 设置滑点。`300` 表示 3%。 |
| `.address_lookup_table_account(alt)` | 传入 ALT 以减少交易体积。PumpFun V2 交易较大时很有用。 |
| `.wait_tx_confirmed(true)` | 等链上确认后再返回。追求最快提交时通常关闭。 |
| `.wait_for_all_submits(true)` | fast-submit 模式下等待所有 SWQoS 通道返回,并拿到全部签名。 |
| `.simulate(true)` | 只构建并模拟交易,不真正发送。 |
| `.grpc_recv_us(ts)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
| `.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)。
#### 关于shredstream
@@ -232,6 +273,7 @@ client.buy(buy_params).await?;
| 描述 | 运行命令 | 源码路径 |
|------|---------|----------|
| 简化买卖参数 API | `cargo run --package simple_trading` | [examples/simple_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/simple_trading/src/main.rs) |
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
@@ -281,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?;
```
#### AstralaneBinary / Plain / QUIC
@@ -364,21 +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()` 表示旧版 SOL 布局,显式 native SOL/WSOL 表示 SOL V2USDC 表示 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
// SOL 池:log 事件里通常是 Pubkey::default()parser 数据里可能是 native SOL/WSOL
// 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(
@@ -404,13 +468,14 @@ client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败
USDC 配对币必须用 USDC 买入、卖出也结算为 USDCSOL/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 | 实际使用的指令 | 说明 |
|-----------|---------|------|
| 未设置(默认) | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
| `WSOL_TOKEN_ACCOUNT` / native SOL | `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 保护服务
+91 -1
View File
@@ -4,14 +4,104 @@ This document provides a comprehensive reference for all trading parameters used
## 📋 Table of Contents
- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams)
- [TradeBuyParams](#tradebuyparams)
- [TradeSellParams](#tradesellparams)
- [Parameter Categories](#parameter-categories)
- [Important Notes](#important-notes)
## SimpleBuyParams / SimpleSellParams
Use `SimpleBuyParams` and `SimpleSellParams` for new integrations. They keep the public API focused on trading intent and map to the lower-level `TradeBuyParams` / `TradeSellParams` internally.
### SimpleBuyParams
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. |
| `pay_with` | `TradeTokenType` | ✅ | Quote token used to pay for the buy. Use `SOL` when the wallet spends native SOL. For PumpFun V2 SOL/WSOL quote pools, still use `SOL` if you want native SOL settlement. |
| `mint` | `Pubkey` | ✅ | Mint of the token being bought. |
| `amount` | `BuyAmount` | ✅ | Buy sizing intent. Choose one enum variant instead of combining low-level amount flags. |
| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache, such as `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`. |
| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. The SDK does not fetch this on the hot path. |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
| `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 `.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. |
### SimpleSellParams
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `dex_type` | `DexType` | ✅ | Protocol to trade through, for example `DexType::PumpFun`. |
| `receive_as` | `TradeTokenType` | ✅ | Quote token to receive from the sell. Use `SOL` when you want native SOL output. |
| `mint` | `Pubkey` | ✅ | Mint of the token being sold. |
| `amount` | `SellAmount` | ✅ | Sell sizing intent. |
| `extension_params` | `DexParamEnum` | ✅ | Protocol state from parser/RPC cache. |
| `recent_blockhash` | `Hash` | ✅ for `new` | Cached recent blockhash for non-nonce transactions. |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | Compute unit price/limit and relay tip configuration. |
| `slippage_basis_points` | `Option<u64>` | ❌ | Optional slippage override. `100` means 1%. |
| `account_policy` | `AccountPolicy` | ❌ | ATA creation/close behavior. Default is `Auto`. |
| `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 `.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. |
### Amount Selection
| Variant | Meaning | Low-level mapping |
|---------|---------|-------------------|
| `BuyAmount::ExactInput(amount)` | Spend exactly this quote amount; slippage protects minimum token output. | `input_token_amount = amount`, `use_exact_sol_amount = Some(true)` |
| `BuyAmount::WithMaxInput { quote_amount }` | Regular PumpFun/PumpSwap buy. The SDK estimates output and applies slippage to max quote cost. | `input_token_amount = quote_amount`, `use_exact_sol_amount = Some(false)` |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | Buy an exact token amount while limiting max quote input. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` |
| `SellAmount::ExactInput(amount)` | Sell exactly this token amount; slippage protects minimum quote output. | `input_token_amount = amount` |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | Receive an exact quote amount while limiting token input, where supported. | `fixed_output_token_amount = Some(output_amount)`, `input_token_amount = max_input_amount` |
### AccountPolicy
| Variant | Behavior | Use when |
|---------|----------|----------|
| `Auto` | SDK creates practical ATAs when needed. Buy creates the target mint ATA; sell creates the output ATA for non-SOL outputs. | Normal apps and manual trading tools. |
| `HotPathMinimal` | No ATA create/close instructions in the trade transaction. | Bots, sniping, arbitrage, and any path sensitive to transaction size. |
| `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
The `TradeBuyParams` struct contains all parameters required for executing buy orders across different DEX protocols.
`TradeBuyParams` is the advanced low-level buy API. New integrations should prefer `SimpleBuyParams` unless they need direct control over individual ATA flags.
### Basic Trading Parameters
+91 -1
View File
@@ -4,14 +4,104 @@
## 📋 目录
- [SimpleBuyParams / SimpleSellParams](#simplebuyparams--simplesellparams)
- [TradeBuyParams](#tradebuyparams)
- [TradeSellParams](#tradesellparams)
- [参数分类](#参数分类)
- [重要说明](#重要说明)
## SimpleBuyParams / SimpleSellParams
新接入优先使用 `SimpleBuyParams``SimpleSellParams`。这两个结构体描述交易意图,SDK 内部会转换成低层 `TradeBuyParams` / `TradeSellParams`
### SimpleBuyParams
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 |
| `pay_with` | `TradeTokenType` | ✅ | 买入时用什么 quote 支付。钱包实际花原生 SOL 就传 `SOL`。PumpFun V2 的 SOL/WSOL quote 池,如果你想用原生 SOL 结算,也仍然传 `SOL`。 |
| `mint` | `Pubkey` | ✅ | 要买入的 token mint。 |
| `amount` | `BuyAmount` | ✅ | 买入数量语义。选择一个枚举,不再组合多个低层数量字段。 |
| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存,例如 `DexParamEnum::PumpFun(PumpFunParams::from_trade(...))`。 |
| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。SDK 不会在热路径临时获取。 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
| `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 信息。使用 `.durable_nonce(nonce_info)``SimpleBuyParams::with_durable_nonce(...)` 设置,不要和 `recent_blockhash` 混用。 |
| `simulate` | `bool` | ❌ | 只构建并模拟交易,不提交。默认 `false`。 |
| `grpc_recv_us` | `Option<i64>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
### SimpleSellParams
| 参数 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `dex_type` | `DexType` | ✅ | 使用哪个协议交易,例如 `DexType::PumpFun`。 |
| `receive_as` | `TradeTokenType` | ✅ | 卖出后接收什么 quote。想收原生 SOL 就传 `SOL`。 |
| `mint` | `Pubkey` | ✅ | 要卖出的 token mint。 |
| `amount` | `SellAmount` | ✅ | 卖出数量语义。 |
| `extension_params` | `DexParamEnum` | ✅ | 协议状态参数,来自 parser/RPC 缓存。 |
| `recent_blockhash` | `Hash` | ✅,使用 `new` 时 | 非 nonce 交易使用的 recent blockhash。 |
| `gas_fee_strategy` | `GasFeeStrategy` | ✅ | CU price/limit 和 relay tip 配置。 |
| `slippage_basis_points` | `Option<u64>` | ❌ | 可选滑点覆盖。`100` 表示 1%。 |
| `account_policy` | `AccountPolicy` | ❌ | ATA 创建/关闭策略。默认 `Auto`。 |
| `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 信息。使用 `.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>` | ❌ | 上游收到事件的微秒时间戳,用于延迟追踪。 |
### 数量如何选择
| 枚举 | 含义 | 底层映射 |
|------|------|----------|
| `BuyAmount::ExactInput(amount)` | 精确花费指定 quote 数量,滑点保护最小买到 token 数量。 | `input_token_amount = amount``use_exact_sol_amount = Some(true)` |
| `BuyAmount::WithMaxInput { quote_amount }` | 常规 PumpFun/PumpSwap buy。SDK 估算输出,并把滑点作用在最大 quote 成本上。 | `input_token_amount = quote_amount``use_exact_sol_amount = Some(false)` |
| `BuyAmount::ExactOutput { output_amount, max_input_amount }` | 精确买到指定 token 数量,并限制最多花多少 quote。 | `fixed_output_token_amount = Some(output_amount)``input_token_amount = max_input_amount` |
| `SellAmount::ExactInput(amount)` | 精确卖出指定 token 数量,滑点保护最少收到 quote 数量。 | `input_token_amount = amount` |
| `SellAmount::ExactOutput { output_amount, max_input_amount }` | 精确收到指定 quote 数量,并限制最多卖出多少 token;取决于 DEX 是否支持。 | `fixed_output_token_amount = Some(output_amount)``input_token_amount = max_input_amount` |
### AccountPolicy
| 枚举 | 行为 | 适用场景 |
|------|------|----------|
| `Auto` | SDK 按实际路径创建必要 ATA。买入会创建目标 token ATA;卖出接收非 SOL 时会创建输出 ATA。 | 普通应用、手动交易工具。 |
| `HotPathMinimal` | 交易内不创建/关闭 ATA。 | Bot、狙击、套利、对交易体积敏感的路径。 |
| `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` 结构体包含在不同 DEX 协议上执行买入订单所需的所有参数
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它
### 基础交易参数
+1
View File
@@ -179,6 +179,7 @@ async fn pumpfun_copy_trade_with_grpc(
)),
address_lookup_table_account,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
+2
View File
@@ -171,6 +171,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: true,
@@ -222,6 +223,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
with_tip: false,
durable_nonce: None,
create_output_token_ata: false,
+2
View File
@@ -139,6 +139,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -183,6 +184,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+10
View File
@@ -628,6 +628,7 @@ async fn handle_buy_pumpfun(
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -684,6 +685,7 @@ async fn handle_buy_pumpswap(
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -740,6 +742,7 @@ async fn handle_buy_bonk(
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -800,6 +803,7 @@ async fn handle_buy_raydium_v4(
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -861,6 +865,7 @@ async fn handle_buy_raydium_cpmm(
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -1031,6 +1036,7 @@ async fn handle_sell_pumpfun(
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1090,6 +1096,7 @@ async fn handle_sell_pumpswap(
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1149,6 +1156,7 @@ async fn handle_sell_bonk(
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1211,6 +1219,7 @@ async fn handle_sell_raydium_v4(
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1274,6 +1283,7 @@ async fn handle_sell_raydium_cpmm(
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -43,6 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
close_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
create_mint_ata: true,
@@ -84,6 +85,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_mint_token_ata: false,
+1
View File
@@ -104,6 +104,7 @@ async fn test_middleware() -> AnyResult<()> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
+1
View File
@@ -175,6 +175,7 @@ async fn pumpfun_copy_trade_with_grpc(
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -169,6 +169,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -220,6 +221,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -158,6 +158,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -203,6 +204,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -81,6 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+2
View File
@@ -236,6 +236,7 @@ async fn pumpswap_trade_with_grpc(
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_sol,
close_input_token_ata: is_sol,
create_mint_ata: true,
@@ -277,6 +278,7 @@ async fn pumpswap_trade_with_grpc(
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_sol,
close_output_token_ata: is_sol,
close_mint_token_ata: false,
@@ -157,6 +157,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -199,6 +200,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
@@ -161,6 +161,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -201,6 +202,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
+2
View File
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -84,6 +85,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "simple_trading"
version = "0.1.0"
edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
solana-sdk = "3.0.0"
tokio = { version = "1", features = ["full"] }
+138
View File
@@ -0,0 +1,138 @@
//! Simple high-level trading API example.
//!
//! This example focuses on parameter construction. Replace the placeholder
//! PumpFun params with real params from your parser/RPC cache before sending.
use std::sync::Arc;
use sol_trade_sdk::{
common::{GasFeeStrategy, TradeConfig},
constants::{TOKEN_PROGRAM_2022, WSOL_TOKEN_ACCOUNT},
instruction::utils::pumpfun::global_constants,
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
AccountPolicy, BuyAmount, DurableNonceInfo, SellAmount, SimpleBuyParams, SimpleSellParams,
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::Keypair, signer::Signer};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::processed();
let swqos_configs = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// Default is true. WSOL ATA is prepared on startup, not in the hot trade tx.
.create_wsol_ata_on_startup(true)
.build();
let client = SolanaTrade::new(Arc::new(payer), trade_config).await;
let mint = Pubkey::new_unique();
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150_000, 150_000, 500_000, 500_000, 0.001, 0.001);
// In production, fill these fields from your parser/RPC cache. They are
// protocol state, not user preferences.
let pumpfun_params = DexParamEnum::PumpFun(PumpFunParams::from_trade(
Pubkey::new_unique(), // bonding_curve
Pubkey::new_unique(), // associated_bonding_curve
mint,
// WSOL quote_mint selects PumpFun V2 SOL layout. Users still pay with
// native SOL below by setting `TradeTokenType::SOL`.
WSOL_TOKEN_ACCOUNT,
Pubkey::new_unique(), // creator from event/cache
Pubkey::default(), // creator_vault; SDK derives it if unavailable
1_073_000_000_000_000, // virtual_token_reserves
30_000_000_000, // virtual_quote_reserves
793_100_000_000_000, // real_token_reserves
0, // real_quote_reserves
None, // close_token_account_when_sell
global_constants::FEE_RECIPIENT,
// If parser/cache does not know the mint owner, PumpFun now defaults to
// Token-2022. Passing it explicitly makes the example easier to read.
TOKEN_PROGRAM_2022,
false, // is_cashback_coin
Some(false), // mayhem_mode
));
let buy_params = SimpleBuyParams::new(
DexType::PumpFun,
// For PumpFun V2 SOL-paired coins, keep this as SOL even though V2
// account metas use the WSOL mint as quote_mint.
TradeTokenType::SOL,
mint,
// Regular PumpFun/PumpSwap buy. The SDK estimates expected output and
// applies slippage to the maximum quote cost.
BuyAmount::WithMaxInput { quote_amount: 100_000 },
pumpfun_params.clone(),
recent_blockhash,
gas_fee_strategy.clone(),
)
.slippage_basis_points(300)
// Best for bots: do not create/close ATAs in the hot transaction.
// Use AccountPolicy::Auto for normal integrations.
.account_policy(AccountPolicy::HotPathMinimal);
// 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,
SellAmount::ExactInput(1_000_000),
pumpfun_params,
client.infrastructure.rpc.get_latest_blockhash().await?,
gas_fee_strategy,
)
.durable_nonce(DurableNonceInfo {
nonce_account: Some(Pubkey::new_unique()),
current_nonce: Some(Hash::new_unique()),
})
.account_policy(AccountPolicy::HotPathMinimal);
let _ = nonce_sell_params;
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
Ok(())
}
+765 -1
View File
@@ -50,6 +50,15 @@ fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool {
}
}
#[inline]
fn normalize_swqos_configs(rpc_url: &str, configs: &[SwqosConfig]) -> Vec<SwqosConfig> {
let mut out = configs.to_vec();
if !out.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)) {
out.push(SwqosConfig::Default(rpc_url.to_string()));
}
out
}
/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。
///
/// * `dex_type`PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。
@@ -73,6 +82,420 @@ pub enum TradeTokenType {
USDC,
}
/// Account lifecycle policy for high-level trade requests.
///
/// This replaces low-level flags such as `create_input_token_ata`,
/// `close_input_token_ata`, `create_mint_ata`, and `create_output_token_ata`.
/// Use this when calling [`TradingClient::buy_simple`] or [`TradingClient::sell_simple`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccountPolicy {
/// SDK chooses a practical default for the protocol and token route.
///
/// Buy: create the target token ATA because the wallet usually needs a place
/// to receive the bought token.
///
/// Sell: create the output ATA only when receiving an SPL token such as USDC
/// or WSOL. Receiving native SOL does not need an ATA.
Auto,
/// Keep the transaction small for sniping/arbitrage; assume hot accounts are prepared.
///
/// This avoids ATA create/close instructions in the trade transaction. It is
/// the recommended policy for latency-sensitive bots that pre-create WSOL
/// and token ATAs outside the hot path.
HotPathMinimal,
/// Include ATA create instructions when the route needs them.
///
/// This is more convenient for normal trading but can make PumpFun V2 and
/// other large transactions exceed Solana packet size limits unless an ALT
/// is supplied or the transaction builder can compact optional instructions.
CreateMissing,
/// Do not create or close token accounts in the trade transaction.
///
/// Use this when the caller has already prepared all required ATAs and wants
/// deterministic instruction layout. Unlike `HotPathMinimal`, this name is
/// intended for correctness/readability rather than bot latency.
AssumePrepared,
}
/// High-level buy sizing intent.
///
/// This replaces `input_token_amount`, `fixed_output_token_amount`, and
/// `use_exact_sol_amount` in [`TradeBuyParams`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuyAmount {
/// Spend this exact quote amount and apply slippage to minimum output.
///
/// Example: spend exactly `0.1 SOL` or exactly `100 USDC` worth of quote
/// token; if the received token amount is below the slippage-adjusted
/// minimum, the transaction fails.
ExactInput(u64),
/// Buy an exact token amount, using `max_input_amount` as the quote budget.
///
/// Example: buy exactly `1_000_000` token base units, but fail if the quote
/// cost would exceed `max_input_amount`.
ExactOutput { output_amount: u64, max_input_amount: u64 },
/// Regular PumpFun/PumpSwap buy: estimate output from `quote_amount` and apply slippage to max quote cost.
///
/// This maps to `use_exact_sol_amount = Some(false)` in the old API. It is
/// useful for bots that want to know the intended token output from the SDK
/// calculation while letting the chain fail if the max quote budget is
/// exceeded.
WithMaxInput { quote_amount: u64 },
}
/// High-level sell sizing intent.
///
/// This replaces `input_token_amount` and `fixed_output_token_amount` in
/// [`TradeSellParams`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SellAmount {
/// Sell this exact token amount and apply slippage to minimum output.
///
/// Example: sell exactly `1_000_000` token base units and fail if the
/// received quote amount is below the slippage-adjusted minimum.
ExactInput(u64),
/// Exact-output sell where supported; `max_input_amount` is the token budget.
///
/// Example: receive exactly `0.1 SOL` or `100 USDC`, but spend no more than
/// `max_input_amount` token base units.
ExactOutput { output_amount: u64, max_input_amount: u64 },
}
/// Simpler buy request that describes trade intent instead of low-level ATA flags.
///
/// Prefer constructing this with [`SimpleBuyParams::new`] or
/// [`SimpleBuyParams::with_durable_nonce`], then optionally set slippage,
/// account policy, ALT, simulation, or confirmation flags through builder-style
/// methods.
#[derive(Clone)]
pub struct SimpleBuyParams {
/// DEX/protocol to route through, such as `DexType::PumpFun`.
pub dex_type: DexType,
/// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`.
///
/// 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,
/// Buy sizing intent. See [`BuyAmount`].
pub amount: BuyAmount,
/// Optional slippage in basis points. `100` means 1%.
pub slippage_basis_points: Option<u64>,
/// Recent blockhash for non-nonce transactions.
///
/// The SDK intentionally does not fetch blockhash on the hot path. Use
/// [`SimpleBuyParams::with_durable_nonce`] instead when submitting multiple
/// SWQoS lanes against a pinned nonce.
pub recent_blockhash: Option<Hash>,
/// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
pub extension_params: DexParamEnum,
/// Compute unit price/limit and relay tip configuration.
pub gas_fee_strategy: GasFeeStrategy,
/// ATA creation/close behavior. See [`AccountPolicy`].
pub account_policy: AccountPolicy,
/// Optional Address Lookup Table to reduce transaction size.
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Wait until the transaction is confirmed before returning.
pub wait_tx_confirmed: bool,
/// Fast-submit mode only: wait for every SWQoS route's submit response so all
/// signatures can be returned.
pub wait_for_all_submits: bool,
/// Durable nonce info. Mutually exclusive with `recent_blockhash`.
pub durable_nonce: Option<DurableNonceInfo>,
/// Build and simulate the transaction instead of submitting it.
pub simulate: bool,
/// Optional upstream receive timestamp in microseconds for latency tracing.
pub grpc_recv_us: Option<i64>,
}
/// Simpler sell request that describes trade intent instead of low-level ATA flags.
///
/// Prefer constructing this with [`SimpleSellParams::new`] or
/// [`SimpleSellParams::with_durable_nonce`].
#[derive(Clone)]
pub struct SimpleSellParams {
/// DEX/protocol to route through, such as `DexType::PumpFun`.
pub dex_type: DexType,
/// Quote token to receive from the sell: `SOL`, `WSOL`, `USDC`, or `USD1`.
pub receive_as: TradeTokenType,
/// Mint address of the token being sold.
pub mint: Pubkey,
/// Sell sizing intent. See [`SellAmount`].
pub amount: SellAmount,
/// Optional slippage in basis points. `100` means 1%.
pub slippage_basis_points: Option<u64>,
/// Recent blockhash for non-nonce transactions.
pub recent_blockhash: Option<Hash>,
/// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
pub extension_params: DexParamEnum,
/// Compute unit price/limit and relay tip configuration.
pub gas_fee_strategy: GasFeeStrategy,
/// ATA creation/close behavior. See [`AccountPolicy`].
pub account_policy: AccountPolicy,
/// Optional Address Lookup Table to reduce transaction size.
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Wait until the transaction is confirmed before returning.
pub wait_tx_confirmed: bool,
/// Fast-submit mode only: wait for every SWQoS route's submit response so all
/// signatures can be returned.
pub wait_for_all_submits: bool,
/// Durable nonce info. Mutually exclusive with `recent_blockhash`.
pub durable_nonce: Option<DurableNonceInfo>,
/// Build and simulate the transaction instead of submitting it.
pub simulate: bool,
/// Whether to include relay tips for sell transactions.
pub with_tip: bool,
/// Optional upstream receive timestamp in microseconds for latency tracing.
pub grpc_recv_us: Option<i64>,
}
impl SimpleBuyParams {
/// Create a simple buy request using a recent blockhash.
///
/// Defaults:
/// - `account_policy = AccountPolicy::Auto`
/// - `wait_tx_confirmed = false`
/// - `wait_for_all_submits = false`
/// - `simulate = false`
/// - no ALT
/// - no slippage override
pub fn new(
dex_type: DexType,
pay_with: TradeTokenType,
mint: Pubkey,
amount: BuyAmount,
extension_params: DexParamEnum,
recent_blockhash: Hash,
gas_fee_strategy: GasFeeStrategy,
) -> Self {
Self {
dex_type,
pay_with,
mint,
amount,
slippage_basis_points: None,
recent_blockhash: Some(recent_blockhash),
extension_params,
gas_fee_strategy,
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
simulate: false,
grpc_recv_us: None,
}
}
/// Create a simple buy request using a durable nonce instead of a recent blockhash.
///
/// Use this when sending through multiple SWQoS lanes with the same nonce.
pub fn with_durable_nonce(
dex_type: DexType,
pay_with: TradeTokenType,
mint: Pubkey,
amount: BuyAmount,
extension_params: DexParamEnum,
durable_nonce: DurableNonceInfo,
gas_fee_strategy: GasFeeStrategy,
) -> Self {
Self {
durable_nonce: Some(durable_nonce),
recent_blockhash: None,
..Self::new(
dex_type,
pay_with,
mint,
amount,
extension_params,
Hash::default(),
gas_fee_strategy,
)
}
}
/// Set slippage in basis points. `100` means 1%.
pub fn slippage_basis_points(mut self, value: u64) -> Self {
self.slippage_basis_points = Some(value);
self
}
/// Set account lifecycle behavior. Bots usually want `HotPathMinimal`;
/// normal integrations can keep the default `Auto`.
pub fn account_policy(mut self, value: AccountPolicy) -> Self {
self.account_policy = value;
self
}
/// Attach an Address Lookup Table to reduce transaction size.
pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
self.address_lookup_table_account = Some(value);
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;
self
}
/// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
pub fn wait_for_all_submits(mut self, value: bool) -> Self {
self.wait_for_all_submits = value;
self
}
/// Simulate instead of submitting.
pub fn simulate(mut self, value: bool) -> Self {
self.simulate = value;
self
}
/// Attach upstream receive timestamp for latency tracing.
pub fn grpc_recv_us(mut self, value: i64) -> Self {
self.grpc_recv_us = Some(value);
self
}
}
impl SimpleSellParams {
/// Create a simple sell request using a recent blockhash.
///
/// Defaults:
/// - `account_policy = AccountPolicy::Auto`
/// - `with_tip = true`
/// - `wait_tx_confirmed = false`
/// - `wait_for_all_submits = false`
/// - `simulate = false`
/// - no ALT
/// - no slippage override
pub fn new(
dex_type: DexType,
receive_as: TradeTokenType,
mint: Pubkey,
amount: SellAmount,
extension_params: DexParamEnum,
recent_blockhash: Hash,
gas_fee_strategy: GasFeeStrategy,
) -> Self {
Self {
dex_type,
receive_as,
mint,
amount,
slippage_basis_points: None,
recent_blockhash: Some(recent_blockhash),
extension_params,
gas_fee_strategy,
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
simulate: false,
with_tip: true,
grpc_recv_us: None,
}
}
/// Create a simple sell request using a durable nonce instead of a recent blockhash.
pub fn with_durable_nonce(
dex_type: DexType,
receive_as: TradeTokenType,
mint: Pubkey,
amount: SellAmount,
extension_params: DexParamEnum,
durable_nonce: DurableNonceInfo,
gas_fee_strategy: GasFeeStrategy,
) -> Self {
Self {
durable_nonce: Some(durable_nonce),
recent_blockhash: None,
..Self::new(
dex_type,
receive_as,
mint,
amount,
extension_params,
Hash::default(),
gas_fee_strategy,
)
}
}
/// Set slippage in basis points. `100` means 1%.
pub fn slippage_basis_points(mut self, value: u64) -> Self {
self.slippage_basis_points = Some(value);
self
}
/// Set account lifecycle behavior. Bots usually want `HotPathMinimal`;
/// normal integrations can keep the default `Auto`.
pub fn account_policy(mut self, value: AccountPolicy) -> Self {
self.account_policy = value;
self
}
/// Attach an Address Lookup Table to reduce transaction size.
pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
self.address_lookup_table_account = Some(value);
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;
self
}
/// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
pub fn wait_for_all_submits(mut self, value: bool) -> Self {
self.wait_for_all_submits = value;
self
}
/// Simulate instead of submitting.
pub fn simulate(mut self, value: bool) -> Self {
self.simulate = value;
self
}
/// Enable or disable relay tips for sell transactions.
pub fn with_tip(mut self, value: bool) -> Self {
self.with_tip = value;
self
}
/// Attach upstream receive timestamp for latency tracing.
pub fn grpc_recv_us(mut self, value: i64) -> Self {
self.grpc_recv_us = Some(value);
self
}
}
/// Shared infrastructure components that can be reused across multiple wallets
///
/// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients)
@@ -134,8 +557,9 @@ impl TradingInfrastructure {
// Create SWQOS clients with blacklist checkingQUIC 握手可能较慢,单节点超时 15s)
const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
let swqos_configs = normalize_swqos_configs(&config.rpc_url, &config.swqos_configs);
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
for swqos in &config.swqos_configs {
for swqos in &swqos_configs {
if swqos.is_blacklisted() {
if sdk_log::sdk_log_enabled() {
warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
@@ -362,6 +786,11 @@ pub struct TradeBuyParams {
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Whether to wait for transaction confirmation before returning
pub wait_tx_confirmed: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Set to true when confirming externally against a pinned
/// durable nonce; defaults to false. See `SwapParams.wait_for_all_submits`.
pub wait_for_all_submits: bool,
/// Whether to create input token associated token account
pub create_input_token_ata: bool,
/// Whether to close input token associated token account after trade
@@ -414,6 +843,11 @@ pub struct TradeSellParams {
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Whether to wait for transaction confirmation before returning
pub wait_tx_confirmed: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Set to true when confirming externally against a pinned
/// durable nonce; defaults to false. See `SwapParams.wait_for_all_submits`.
pub wait_for_all_submits: bool,
/// Whether to create output token associated token account
pub create_output_token_ata: bool,
/// Whether to close output token associated token account after trade
@@ -433,6 +867,96 @@ pub struct TradeSellParams {
pub grpc_recv_us: Option<i64>,
}
#[inline]
fn buy_account_flags(policy: AccountPolicy) -> (bool, bool, bool) {
match policy {
AccountPolicy::Auto => (false, true, false),
AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false),
AccountPolicy::CreateMissing => (true, true, false),
}
}
#[inline]
fn sell_account_flags(policy: AccountPolicy, receive_as: &TradeTokenType) -> (bool, bool, bool) {
match policy {
AccountPolicy::Auto => (*receive_as != TradeTokenType::SOL, false, false),
AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false),
AccountPolicy::CreateMissing => (true, false, false),
}
}
impl From<SimpleBuyParams> for TradeBuyParams {
fn from(params: SimpleBuyParams) -> Self {
let (input_token_amount, fixed_output_token_amount, use_exact_sol_amount) =
match params.amount {
BuyAmount::ExactInput(amount) => (amount, None, Some(true)),
BuyAmount::ExactOutput { output_amount, max_input_amount } => {
(max_input_amount, Some(output_amount), Some(true))
}
BuyAmount::WithMaxInput { quote_amount } => (quote_amount, None, Some(false)),
};
let (create_input_token_ata, create_mint_ata, close_input_token_ata) =
buy_account_flags(params.account_policy);
TradeBuyParams {
dex_type: params.dex_type,
input_token_type: params.pay_with,
mint: params.mint,
input_token_amount,
slippage_basis_points: params.slippage_basis_points,
recent_blockhash: params.recent_blockhash,
extension_params: params.extension_params,
address_lookup_table_account: params.address_lookup_table_account,
wait_tx_confirmed: params.wait_tx_confirmed,
wait_for_all_submits: params.wait_for_all_submits,
create_input_token_ata,
close_input_token_ata,
create_mint_ata,
durable_nonce: params.durable_nonce,
fixed_output_token_amount,
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
use_exact_sol_amount,
grpc_recv_us: params.grpc_recv_us,
}
}
}
impl From<SimpleSellParams> for TradeSellParams {
fn from(params: SimpleSellParams) -> Self {
let (input_token_amount, fixed_output_token_amount) = match params.amount {
SellAmount::ExactInput(amount) => (amount, None),
SellAmount::ExactOutput { output_amount, max_input_amount } => {
(max_input_amount, Some(output_amount))
}
};
let (create_output_token_ata, close_output_token_ata, close_mint_token_ata) =
sell_account_flags(params.account_policy, &params.receive_as);
TradeSellParams {
dex_type: params.dex_type,
output_token_type: params.receive_as,
mint: params.mint,
input_token_amount,
slippage_basis_points: params.slippage_basis_points,
recent_blockhash: params.recent_blockhash,
with_tip: params.with_tip,
extension_params: params.extension_params,
address_lookup_table_account: params.address_lookup_table_account,
wait_tx_confirmed: params.wait_tx_confirmed,
wait_for_all_submits: params.wait_for_all_submits,
create_output_token_ata,
close_output_token_ata,
close_mint_token_ata,
durable_nonce: params.durable_nonce,
fixed_output_token_amount,
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
grpc_recv_us: params.grpc_recv_us,
}
}
}
impl TradingClient {
/// Create a TradingClient from shared infrastructure (fast path)
///
@@ -742,6 +1266,12 @@ impl TradingClient {
Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v }));
}
}
if self.use_dedicated_sender_threads {
crate::trading::core::async_executor::warm_dedicated_sender_pool(
self.sender_thread_cores.as_ref().map(|v| v.as_slice()),
self.max_sender_concurrency,
);
}
self
}
@@ -868,6 +1398,7 @@ impl TradingClient {
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
log_enabled: self.log_enabled,
wait_for_all_submits: params.wait_for_all_submits,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
@@ -888,6 +1419,18 @@ impl TradingClient {
result
}
/// Execute a high-level buy request.
#[inline]
pub async fn buy_simple(
&self,
params: SimpleBuyParams,
) -> Result<
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
anyhow::Error,
> {
self.buy(params.into()).await
}
/// Execute a sell order for a specified token
///
/// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
@@ -983,6 +1526,7 @@ impl TradingClient {
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
log_enabled: self.log_enabled,
wait_for_all_submits: params.wait_for_all_submits,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
@@ -1003,6 +1547,18 @@ impl TradingClient {
result
}
/// Execute a high-level sell request.
#[inline]
pub async fn sell_simple(
&self,
params: SimpleSellParams,
) -> Result<
(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
anyhow::Error,
> {
self.sell(params.into()).await
}
/// Execute a sell order for a percentage of the specified token amount
///
/// This is a convenience function that calculates the exact amount to sell based on
@@ -1251,3 +1807,211 @@ impl TradingClient {
Ok(signature.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::instruction::utils::pumpfun::global_constants;
use crate::swqos::SwqosRegion;
use std::sync::Arc;
fn dummy_pumpfun_params() -> DexParamEnum {
DexParamEnum::PumpFun(PumpFunParams {
bonding_curve: Arc::new(Default::default()),
associated_bonding_curve: Pubkey::default(),
observed_trade_creator: None,
creator_vault: Pubkey::default(),
fee_sharing_creator_vault_if_active: None,
token_program: Pubkey::default(),
close_token_account_when_sell: None,
fee_recipient: global_constants::FEE_RECIPIENT,
quote_mint: Pubkey::default(),
})
}
#[test]
fn normalize_swqos_configs_adds_default_rpc_route() {
let configs = vec![SwqosConfig::Jito("uuid".to_string(), SwqosRegion::Frankfurt, None)];
let normalized = normalize_swqos_configs("https://rpc.example", &configs);
assert_eq!(normalized.len(), 2);
assert!(normalized.iter().any(|c| matches!(c.swqos_type(), SwqosType::Jito)));
assert!(normalized.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)));
}
#[test]
fn normalize_swqos_configs_does_not_duplicate_default_rpc_route() {
let configs = vec![SwqosConfig::Default("https://rpc.example".to_string())];
let normalized = normalize_swqos_configs("https://rpc.example", &configs);
assert_eq!(normalized.len(), 1);
assert!(matches!(normalized[0].swqos_type(), SwqosType::Default));
}
#[test]
fn simple_buy_hot_path_maps_to_low_level_params() {
let simple = SimpleBuyParams {
dex_type: DexType::PumpFun,
pay_with: TradeTokenType::SOL,
mint: Pubkey::new_unique(),
amount: BuyAmount::WithMaxInput { quote_amount: 10_000 },
slippage_basis_points: Some(100),
recent_blockhash: Some(Hash::new_unique()),
extension_params: dummy_pumpfun_params(),
gas_fee_strategy: GasFeeStrategy::new(),
account_policy: AccountPolicy::HotPathMinimal,
address_lookup_table_account: None,
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
simulate: false,
grpc_recv_us: None,
};
let low: TradeBuyParams = simple.into();
assert!(matches!(low.input_token_type, TradeTokenType::SOL));
assert_eq!(low.input_token_amount, 10_000);
assert_eq!(low.use_exact_sol_amount, Some(false));
assert_eq!(low.fixed_output_token_amount, None);
assert!(!low.create_input_token_ata);
assert!(!low.close_input_token_ata);
assert!(!low.create_mint_ata);
}
#[test]
fn simple_buy_auto_creates_target_mint_ata() {
let simple = SimpleBuyParams {
dex_type: DexType::PumpFun,
pay_with: TradeTokenType::SOL,
mint: Pubkey::new_unique(),
amount: BuyAmount::ExactOutput { output_amount: 42, max_input_amount: 10_000 },
slippage_basis_points: None,
recent_blockhash: Some(Hash::new_unique()),
extension_params: dummy_pumpfun_params(),
gas_fee_strategy: GasFeeStrategy::new(),
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
simulate: false,
grpc_recv_us: None,
};
let low: TradeBuyParams = simple.into();
assert_eq!(low.input_token_amount, 10_000);
assert_eq!(low.fixed_output_token_amount, Some(42));
assert_eq!(low.use_exact_sol_amount, Some(true));
assert!(low.create_mint_ata);
assert!(!low.create_input_token_ata);
}
#[test]
fn simple_buy_builder_sets_defaults_and_overrides() {
let simple = SimpleBuyParams::new(
DexType::PumpFun,
TradeTokenType::SOL,
Pubkey::new_unique(),
BuyAmount::ExactInput(10_000),
dummy_pumpfun_params(),
Hash::new_unique(),
GasFeeStrategy::new(),
)
.slippage_basis_points(250)
.account_policy(AccountPolicy::HotPathMinimal);
let low: TradeBuyParams = simple.into();
assert_eq!(low.slippage_basis_points, Some(250));
assert_eq!(low.use_exact_sol_amount, Some(true));
assert!(!low.create_input_token_ata);
assert!(!low.create_mint_ata);
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 {
dex_type: DexType::PumpFun,
receive_as: TradeTokenType::USDC,
mint: Pubkey::new_unique(),
amount: SellAmount::ExactInput(50_000),
slippage_basis_points: None,
recent_blockhash: Some(Hash::new_unique()),
extension_params: dummy_pumpfun_params(),
gas_fee_strategy: GasFeeStrategy::new(),
account_policy: AccountPolicy::Auto,
address_lookup_table_account: None,
wait_tx_confirmed: false,
wait_for_all_submits: false,
durable_nonce: None,
simulate: false,
with_tip: true,
grpc_recv_us: None,
};
let low: TradeSellParams = simple.into();
assert!(matches!(low.output_token_type, TradeTokenType::USDC));
assert_eq!(low.input_token_amount, 50_000);
assert!(low.create_output_token_ata);
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));
}
}
+9 -4
View File
@@ -156,8 +156,13 @@ mod tests {
#[test]
fn test_fast_now_overhead() {
// 测试调用开销
let iterations = 10_000;
// This is a coarse regression guard, not a benchmark. On non-Linux targets
// the fast timer uses `Instant::elapsed()`, and CI/desktop scheduler jitter can
// move a single run above 100ns even when the code path is still sub-microsecond.
let iterations = 100_000;
for _ in 0..1_000 {
let _ = fast_now_nanos();
}
let start = Instant::now();
for _ in 0..iterations {
@@ -171,8 +176,8 @@ mod tests {
println!("Average fast_now_nanos() call: {}ns", avg_per_call);
}
// 快速时间戳应该非常快(< 100ns per call
assert!(avg_per_call < 100);
// Keep the hot-path timer safely sub-microsecond without making the test flaky.
assert!(avg_per_call < 500);
}
#[test]
+1
View File
@@ -382,6 +382,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+1
View File
@@ -364,6 +364,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+321 -36
View File
@@ -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()` keeps the smaller legacy SOL layout; explicit WSOL/native SOL
//! or USDC uses the V2 27/26-account unified metas.
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
//! 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,
@@ -39,7 +40,10 @@ use solana_sdk::{
};
#[inline]
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams, mint: &Pubkey) -> Pubkey {
if mint.to_string().ends_with("pump") {
return TOKEN_PROGRAM_2022;
}
let tp = protocol_params.token_program;
if tp == Pubkey::default() {
TOKEN_PROGRAM_2022
@@ -49,7 +53,8 @@ fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey
}
/// Resolve quote mint and its token program from PumpFunParams.
/// `Pubkey::default()` means legacy SOL-paired → use WSOL mint for V2 instructions.
/// `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();
@@ -69,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) {
@@ -126,7 +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);
Ok(protocol_params.quote_mint != Pubkey::default() || !is_sol_quote_mint(&quote_mint))
if !is_sol_quote_mint(&quote_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(&params.input_mint)
}
TradeType::Sell => is_explicit_wsol_settlement_mint(&params.output_mint),
TradeType::Create => false,
})
}
#[inline]
@@ -165,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(&params.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"));
@@ -187,7 +222,7 @@ fn build_buy_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params, &params.output_mint);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -293,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(&params.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"));
@@ -342,7 +384,7 @@ fn build_sell_legacy(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params, &params.input_mint);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -460,7 +502,8 @@ fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program =
effective_pump_mint_token_program(protocol_params, &params.output_mint);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -579,7 +622,9 @@ fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
}
};
if params.create_input_mint_ata {
let should_use_native_sol_for_wsol_quote = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
&& params.input_mint == crate::constants::SOL_TOKEN_ACCOUNT;
if params.create_input_mint_ata && !should_use_native_sol_for_wsol_quote {
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
@@ -622,7 +667,7 @@ fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, buy_data.as_slice(), metas));
if params.close_input_mint_ata {
if params.close_input_mint_ata && !should_use_native_sol_for_wsol_quote {
push_close_wsol_if_needed(&mut instructions, &params.payer.pubkey(), &quote_mint);
}
@@ -691,7 +736,7 @@ fn build_sell_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params, &params.input_mint);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -916,6 +961,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: false,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
@@ -935,18 +981,33 @@ mod tests {
}
#[test]
fn pump_suffix_buy_respects_explicit_legacy_token_program() {
fn pump_suffix_buy_forces_token_2022_even_with_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
let instructions = build_buy(&params).unwrap();
assert_eq!(instructions.len(), 3);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM_2022);
assert_eq!(instructions[2].accounts.len(), 18);
assert_eq!(instructions[2].data.len(), 25);
}
#[test]
fn pump_suffix_sell_forces_token_2022_even_with_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = pump_mint();
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
params.create_output_mint_ata = false;
let instructions = build_sell(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts[9].pubkey, TOKEN_PROGRAM_2022);
}
#[test]
fn non_pump_buy_respects_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
@@ -977,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(&params).unwrap();
@@ -999,10 +1061,11 @@ mod tests {
}
#[test]
fn pumpfun_v2_regular_buy_wraps_max_quote_budget() {
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;
params.close_input_mint_ata = true;
params.use_exact_sol_amount = Some(false);
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
@@ -1010,22 +1073,204 @@ mod tests {
}
let instructions = build_buy(&params).unwrap();
let transfer_ix = &instructions[1];
let system_ix = bincode::deserialize::<
solana_system_interface::instruction::SystemInstruction,
>(&transfer_ix.data)
.unwrap();
assert_eq!(instructions.len(), 1);
let buy_ix = instructions.last().unwrap();
let expected = crate::utils::calc::common::calculate_with_slippage_buy(
params.input_amount.unwrap(),
params.slippage_basis_points.unwrap(),
);
match system_ix {
solana_system_interface::instruction::SystemInstruction::Transfer { lamports } => {
assert_eq!(lamports, expected);
}
other => panic!("unexpected system instruction: {:?}", other),
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(), 18);
}
#[test]
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;
params.close_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(&params).unwrap();
assert_eq!(instructions.len(), 1);
assert_eq!(
&instructions.last().unwrap().data[..8],
crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR
);
}
#[test]
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);
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(&params).unwrap();
let err = crate::trading::common::transaction_builder::build_transaction(
&params.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(&params).unwrap();
let transaction = crate::trading::common::transaction_builder::build_transaction(
&params.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();
let serialized = bincode::serialize(&transaction).unwrap();
assert!(
serialized.len() <= 1232,
"serialized transaction too large: {} > 1232",
serialized.len()
);
}
#[test]
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;
params.use_exact_sol_amount = Some(false);
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params = PumpFunParams::from_trade(
Pubkey::default(),
Pubkey::default(),
mint,
crate::constants::WSOL_TOKEN_ACCOUNT,
protocol_params.effective_creator_for_trade(),
protocol_params.creator_vault,
protocol_params.bonding_curve.virtual_token_reserves,
protocol_params.bonding_curve.virtual_sol_reserves,
protocol_params.bonding_curve.real_token_reserves,
protocol_params.bonding_curve.real_sol_reserves,
None,
protocol_params.fee_recipient,
TOKEN_PROGRAM,
false,
Some(false),
);
}
let instructions = build_buy(&params).unwrap();
let buy_ix = instructions.last().unwrap();
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_DISCRIMINATOR);
assert_eq!(buy_ix.accounts.len(), 18);
}
#[test]
fn pumpfun_v2_wsol_input_does_not_auto_wrap_when_ata_create_disabled() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.input_mint = crate::constants::WSOL_TOKEN_ACCOUNT;
params.create_output_mint_ata = false;
params.create_input_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 instructions = build_buy(&params).unwrap();
assert_eq!(instructions.len(), 1);
assert_eq!(
&instructions[0].data[..8],
crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR
);
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(&params).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(&params).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]
@@ -1111,7 +1356,7 @@ mod tests {
}
#[test]
fn pumpfun_native_sol_quote_mint_normalizes_to_wsol() {
fn pumpfun_solscan_sol_quote_mint_keeps_legacy_layout() {
let mint = pump_mint();
let params = PumpFunParams::from_trade(
Pubkey::default(),
@@ -1131,10 +1376,49 @@ mod tests {
Some(false),
);
assert_eq!(params.quote_mint, crate::constants::WSOL_TOKEN_ACCOUNT);
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_rpc_sol_sentinel_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.quote_mint = crate::constants::SOL_TOKEN_ACCOUNT;
assert_eq!(protocol_params.quote_mint, crate::constants::SOL_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);
@@ -1200,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(&params).unwrap().pop().unwrap();
@@ -1215,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(&params).unwrap().pop().unwrap();
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
assert!(sell_ix.accounts[8].is_writable);
@@ -1244,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(&params).unwrap();
+1
View File
@@ -589,6 +589,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+1
View File
@@ -382,6 +382,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+1
View File
@@ -394,6 +394,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+4 -2
View File
@@ -7,9 +7,11 @@ 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::{
find_pool_by_mint, recommended_sender_thread_core_indices, SolanaTrade, TradeBuyParams,
TradeSellParams, TradeTokenType, TradingClient, TradingInfrastructure,
find_pool_by_mint, recommended_sender_thread_core_indices, AccountPolicy, BuyAmount,
SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade, TradeBuyParams, TradeSellParams,
TradeTokenType, TradingClient, TradingInfrastructure,
};
+97
View File
@@ -17,6 +17,8 @@ use crate::{
},
};
const PACKET_DATA_SIZE: usize = 1232;
/// Convert SOL amount (f64) to lamports without string allocation (hot path).
#[inline(always)]
fn sol_f64_to_lamports(sol: f64) -> u64 {
@@ -43,6 +45,63 @@ pub fn build_transaction(
tip_account: &Pubkey,
tip_amount: f64,
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<VersionedTransaction, anyhow::Error> {
let transaction = build_transaction_inner(
payer,
unit_limit,
unit_price,
business_instructions,
address_lookup_table_account,
recent_blockhash,
middleware_manager,
protocol_name,
is_buy,
with_tip,
tip_account,
tip_amount,
durable_nonce,
)?;
let serialized_len = bincode::serialized_size(&transaction)? as usize;
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);
}
Err(anyhow!(
"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
))
}
fn build_transaction_inner(
payer: &Arc<Keypair>,
unit_limit: u32,
unit_price: u64,
business_instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
middleware_manager: Option<&Arc<MiddlewareManager>>,
protocol_name: &str,
is_buy: bool,
with_tip: bool,
tip_account: &Pubkey,
tip_amount: f64,
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<VersionedTransaction, anyhow::Error> {
let mut instructions = Vec::with_capacity(business_instructions.len() + 5);
@@ -110,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}");
}
}
+37 -9
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 {
@@ -146,10 +147,15 @@ async fn run_one_swqos_job(job: SwqosJob) {
async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>, notify: Arc<Notify>) {
loop {
let notified = notify.notified();
tokio::pin!(notified);
notified.as_mut().enable();
if let Some(job) = queue.pop() {
drop(notified);
run_one_swqos_job(job).await;
} else {
notify.notified().await;
notified.await;
}
}
}
@@ -231,6 +237,15 @@ fn ensure_dedicated_pool(
(queue, notify)
}
/// Pre-spawn dedicated sender threads during SDK initialization, avoiding first-submit thread
/// creation cost on the trading hot path.
pub fn warm_dedicated_sender_pool(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) {
let _ = ensure_dedicated_pool(sender_thread_cores, max_sender_concurrency);
}
fn ensure_dedicated_worker_count(
queue: Arc<ArrayQueue<SwqosJob>>,
notify: Arc<Notify>,
@@ -473,7 +488,7 @@ 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 a short submit window expires. Slow HTTP routes continue in worker tasks but no longer
/// 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,
@@ -495,7 +510,8 @@ impl ResultCollector {
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
#[allow(dead_code)]
/// Re-enabled via `SwapParams.wait_for_all_submits` for callers that confirm
/// externally against a pinned durable nonce and need every submitted sig.
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
@@ -599,6 +615,7 @@ pub async fn execute_parallel(
protocol_name: &'static str,
is_buy: bool,
wait_transaction_confirmed: bool,
wait_for_all_submits: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
use_dedicated_sender_threads: bool,
@@ -734,12 +751,23 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
let ret = collector.wait_for_first_submitted(Duration::from_millis(500)).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within fast submit window")),
vec![],
));
let ret = if wait_for_all_submits {
collector.wait_for_all_submitted(FAST_SUBMIT_RESULT_TIMEOUT.as_secs()).await.unwrap_or(
(
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
),
)
} else {
collector.wait_for_first_submitted(FAST_SUBMIT_RESULT_TIMEOUT).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
))
};
let (success, signatures, last_error, submit_timings) = ret;
return Ok((success, signatures, last_error, submit_timings));
}
+5
View File
@@ -157,6 +157,10 @@ impl TradeExecutor for GenericTradeExecutor {
}
let need_confirm = params.wait_tx_confirmed;
// When the caller confirms externally (need_confirm = false) and opts in
// via SwapParams.wait_for_all_submits, return every route's signature so
// pinned-nonce confirmation can poll all of them.
let wait_for_all_submits = !need_confirm && params.wait_for_all_submits;
let sender_config = params.sender_concurrency_config();
let result = execute_parallel(
params.swqos_clients.as_slice(),
@@ -169,6 +173,7 @@ impl TradeExecutor for GenericTradeExecutor {
self.protocol_name,
is_buy,
false, // submit only here; confirmation and log timing handled below
wait_for_all_submits,
if is_buy { true } else { params.with_tip },
params.gas_fee_strategy,
params.use_dedicated_sender_threads,
+8
View File
@@ -82,6 +82,14 @@ pub struct SwapParams {
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Defaults to false (post-4.0.11 behaviour: return after the
/// first route accepts). Set to true when the caller does its own on-chain
/// confirmation against a pinned durable nonce — only one route's tx can
/// land, but the caller cannot know which in advance, so it needs every
/// signature to poll via `getSignatureStatuses`.
pub wait_for_all_submits: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
+90 -24
View File
@@ -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 for legacy SOL layout, pass WSOL/native SOL for SOL V2,
/// or pass 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>,
@@ -33,19 +34,46 @@ pub struct PumpFunParams {
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 时严格按该值组装,不再用 mint 字符串后缀猜测。
/// `*.pump` mint 在 Pump.fun 指令构造层会强制使用 Token-2022,避免陈旧 parser/cache
/// 传入 legacy Token Program 后创建出 owner 不匹配的临时 token account。
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()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值。
pub fee_recipient: Pubkey,
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
/// 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,
}
impl PumpFunParams {
#[inline]
fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey {
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)
}
}
#[inline]
fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey {
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)
}
}
pub fn immediate_sell(
creator_vault: Pubkey,
token_program: Pubkey,
@@ -151,17 +179,13 @@ impl PumpFunParams {
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
quote_mint: if quote_mint == Pubkey::default() {
Pubkey::default()
} else {
effective_quote_mint
},
quote_mint: Self::quote_mint_for_layout(quote_mint),
}
}
/// Build PumpFun params from event/parser data. Pass `quote_mint` from the event:
/// `Pubkey::default()` for legacy SOL layout, `WSOL_TOKEN_ACCOUNT`/native SOL for SOL V2,
/// and `USDC_TOKEN_ACCOUNT` for USDC V2.
/// `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`:
@@ -218,11 +242,7 @@ impl PumpFunParams {
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
quote_mint: if quote_mint == Pubkey::default() {
Pubkey::default()
} else {
effective_quote_mint
},
quote_mint: Self::quote_mint_for_layout(quote_mint),
}
}
@@ -317,7 +337,7 @@ impl PumpFunParams {
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
quote_mint,
quote_mint: Self::quote_mint_for_rpc_return(quote_mint),
})
}
@@ -357,15 +377,15 @@ impl PumpFunParams {
}
/// Sets `quote_mint`. The instruction builder derives V1/V2 layout from this value.
/// For SOL-paired coins, pass `WSOL_TOKEN_ACCOUNT` or leave default.
/// 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);
self.quote_mint =
if quote_mint == Pubkey::default() { Pubkey::default() } else { effective_quote_mint };
if let Some(curve) = Arc::get_mut(&mut self.bonding_curve) {
*curve = curve.clone().with_quote_mint(effective_quote_mint);
}
self.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
}
@@ -393,3 +413,49 @@ impl PumpFunParams {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rpc_return_quote_mint_normalizes_legacy_sol_to_sol_sentinel() {
assert_eq!(
PumpFunParams::quote_mint_for_rpc_return(Pubkey::default()),
crate::constants::SOL_TOKEN_ACCOUNT
);
assert_eq!(
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]
fn rpc_return_quote_mint_keeps_real_quote_mints() {
assert_eq!(
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_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
);
}
}