Compare commits

...

2 Commits

Author SHA1 Message Date
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
11 changed files with 1206 additions and 76 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.16"
version = "4.0.18"
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]
+63 -29
View File
@@ -81,13 +81,13 @@ This SDK is available in multiple languages:
## 🔖 Current Release
**Rust crate:** `sol-trade-sdk = "4.0.16"`
**Rust crate:** `sol-trade-sdk = "4.0.17"`
This release refreshes PumpFun V2 and USDC quote-pool handling, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
This release refreshes PumpFun V2 WSOL quote-pool handling, keeps the default RPC submit lane active alongside SWQoS lanes, restores the fast-submit result window to 5 seconds, and aligns Raydium CPMM fixed-output swaps with the on-chain `swap_base_out` instruction. Trade execution requires a caller-supplied `recent_blockhash` or durable nonce; hot-path execution does not query RPC for blockhash, account, or balance data.
## ✨ 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, selecting legacy or V2 on-chain instructions as needed (SOL/WSOL + USDC)
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
3. **Bonk Trading**: Support for Bonk trading operations
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
@@ -115,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.16" }
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.16"
sol-trade-sdk = "4.0.17"
```
## 🛠️ Usage Examples
@@ -196,40 +196,73 @@ 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. |
| `SimpleBuyParams::with_durable_nonce(...)` / `SimpleSellParams::with_durable_nonce(...)` | Use 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
@@ -240,6 +273,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) |
+63 -29
View File
@@ -81,13 +81,13 @@
## 🔖 当前版本
**Rust crate:** `sol-trade-sdk = "4.0.16"`
**Rust crate:** `sol-trade-sdk = "4.0.17"`
本版本刷新 PumpFun V2 与 USDC quote 池处理逻辑,确保默认 RPC 提交通道会和 SWQoS 通道一起发出,快速提交结果等待窗口恢复为 5 秒,并将 Raydium CPMM fixed-output 交易对齐到链上 `swap_base_out` 指令。交易执行必须由调用方传入 `recent_blockhash` 或 durable nonce;热路径不会查询 RPC 获取 blockhash、账户或余额数据。
本版本刷新 PumpFun V2 WSOL quote 池处理逻辑,确保默认 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` 流程,内部按需选择旧版或 V2 链上指令(SOL/WSOL + USDC
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
3. **Bonk 交易**: 支持 Bonk 的交易操作
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
@@ -115,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.16" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.16"
sol-trade-sdk = "4.0.17"
```
## 🛠️ 使用示例
@@ -195,40 +195,73 @@ 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)` | 传入上游收到事件的微秒时间戳,用于延迟追踪。 |
| `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
@@ -239,6 +272,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) |
+66 -1
View File
@@ -4,14 +4,79 @@ 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 `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 `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. |
## 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
+66 -1
View File
@@ -4,14 +4,79 @@
## 📋 目录
- [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 信息。使用 `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 信息。使用 `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,调用方保证都已准备好。 | 高级确定性流程。 |
## TradeBuyParams
`TradeBuyParams` 结构体包含在不同 DEX 协议上执行买入订单所需的所有参数
`TradeBuyParams` 是高级低层买入 API。新接入建议优先使用 `SimpleBuyParams`,只有需要直接控制单个 ATA flag 时再使用它
### 基础交易参数
+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"] }
+103
View File
@@ -0,0 +1,103 @@
//! 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, SellAmount, SimpleBuyParams, SimpleSellParams, SolanaTrade,
TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{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;
let 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,
)
.slippage_basis_points(300)
.account_policy(AccountPolicy::HotPathMinimal);
// client.sell_simple(sell_params).await?;
let _ = sell_params;
println!("Built simple buy/sell params for payer {}", client.payer.pubkey());
Ok(())
}
+636
View File
@@ -82,6 +82,399 @@ 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 V2 SOL-paired coins, use `TradeTokenType::SOL` even when the
/// protocol quote mint account is WSOL. The SDK passes WSOL as the V2 quote
/// account but keeps settlement in native SOL.
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
}
/// 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
}
/// 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)
@@ -453,6 +846,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)
///
@@ -915,6 +1398,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并发交易
@@ -1031,6 +1526,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
@@ -1283,7 +1790,23 @@ impl TradingClient {
#[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() {
@@ -1303,4 +1826,117 @@ mod tests {
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_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);
}
}
+123 -13
View File
@@ -586,7 +586,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(),
@@ -629,7 +631,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);
}
@@ -1022,10 +1024,11 @@ mod tests {
}
#[test]
fn pumpfun_v2_regular_buy_wraps_max_quote_budget() {
fn pumpfun_v2_regular_buy_uses_native_sol_for_wsol_quote() {
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 =
@@ -1033,22 +1036,129 @@ 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_V2_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(buy_ix.data[16..24].try_into().unwrap()), expected);
assert_eq!(buy_ix.accounts.len(), 27);
}
#[test]
fn pumpfun_v2_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_V2_DISCRIMINATOR
);
}
#[test]
fn pumpfun_v2_wsol_quote_hot_path_transaction_fits_packet_with_output_ata_create() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
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 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_buy_v2() {
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_V2_DISCRIMINATOR);
assert_eq!(buy_ix.accounts.len(), 27);
}
#[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]
+3 -2
View File
@@ -10,6 +10,7 @@ pub mod utils;
// 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,
};
+71
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,75 @@ pub fn build_transaction(
tip_account: &Pubkey,
tip_amount: f64,
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<VersionedTransaction, anyhow::Error> {
let transaction = build_transaction_with_compute_budget(
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 serialized_len <= PACKET_DATA_SIZE
|| durable_nonce.is_some()
|| !with_tip
|| tip_amount <= 0.0
|| (unit_limit == 0 && unit_price == 0)
{
return Ok(transaction);
}
let compact_transaction = build_transaction_with_compute_budget(
payer,
0,
0,
business_instructions,
address_lookup_table_account,
recent_blockhash,
middleware_manager,
protocol_name,
is_buy,
with_tip,
tip_account,
tip_amount,
durable_nonce,
)?;
let compact_len = bincode::serialized_size(&compact_transaction)? as usize;
if compact_len <= PACKET_DATA_SIZE {
return Ok(compact_transaction);
}
Err(anyhow!(
"transaction too large: {} > {}; compact without compute budget is {} bytes. Use an address lookup table or pre-create token ATAs before submitting",
serialized_len,
PACKET_DATA_SIZE,
compact_len
))
}
fn build_transaction_with_compute_budget(
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);