Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5e58bef89 | |||
| 9874cd5358 | |||
| 1d3c0ff5da | |||
| 34f400e89d | |||
| bd1772bed2 | |||
| 5449d51591 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.13"
|
||||
version = "4.0.16"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
|
||||
@@ -79,6 +79,12 @@ 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.16"`
|
||||
|
||||
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.
|
||||
|
||||
## ✨ 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)
|
||||
@@ -88,10 +94,11 @@ This SDK is available in multiple languages:
|
||||
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.16" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "4.0.13"
|
||||
sol-trade-sdk = "4.0.16"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -367,7 +374,7 @@ PumpFun has two instruction sets for bonding-curve trading:
|
||||
|
||||
The SDK-side builder is version-neutral: callers use the normal buy/sell flow, and `quote_mint` selects the correct on-chain discriminator and account layout internally. There is no user-facing V2 switch required.
|
||||
|
||||
**Default: V1**. When `quote_mint` is `Pubkey::default()`, the SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. Passing an explicit quote mint selects V2: WSOL/native SOL for SOL-paired V2, or USDC for USDC-paired pools.
|
||||
**Default: V1**. When `quote_mint` is `Pubkey::default()` or the Solscan SOL sentinel (`So11111111111111111111111111111111111111111`), the SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table. Passing `WSOL_TOKEN_ACCOUNT` selects SOL V2; passing USDC selects USDC V2.
|
||||
|
||||
**Key changes in v2 instructions:**
|
||||
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
|
||||
@@ -378,12 +385,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()` and Solscan SOL (`So11111111111111111111111111111111111111111`) mean the legacy SOL layout; `WSOL_TOKEN_ACCOUNT` means SOL V2; 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
|
||||
// - Legacy SOL pool: Pubkey::default() or Solscan SOL from parser data
|
||||
// - SOL V2 pool: WSOL_TOKEN_ACCOUNT
|
||||
// - USDC V2 pool: USDC mint
|
||||
let quote_mint = e.quote_mint;
|
||||
let params = PumpFunParams::from_trade(
|
||||
@@ -407,6 +414,7 @@ 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.
|
||||
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.
|
||||
|
||||
|
||||
+16
-7
@@ -79,6 +79,12 @@
|
||||
| **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.16"`
|
||||
|
||||
本版本刷新 PumpFun V2 与 USDC 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)
|
||||
@@ -88,10 +94,11 @@
|
||||
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.16" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "4.0.13"
|
||||
sol-trade-sdk = "4.0.16"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -374,10 +381,11 @@ SDK 侧调用入口保持统一:正常使用 `buy` / `sell` 流程即可,SDK
|
||||
|
||||
**使用方式:**
|
||||
|
||||
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint;`Pubkey::default()` 表示旧版 SOL 布局,显式 native SOL/WSOL 表示 SOL V2,USDC 表示 USDC V2:
|
||||
把事件里的 `quote_mint` 传给 `PumpFunParams::from_trade`。`quote_mint` 不是 PDA,它就是 quote SPL mint;`Pubkey::default()` 和 Solscan SOL(`So11111111111111111111111111111111111111111`)表示旧版 SOL 布局,`WSOL_TOKEN_ACCOUNT` 表示 SOL V2,USDC 表示 USDC V2:
|
||||
|
||||
```rust
|
||||
// SOL 池:log 事件里通常是 Pubkey::default(),parser 数据里可能是 native SOL/WSOL
|
||||
// legacy SOL 池:log 事件里可能是 Pubkey::default(),parser 数据里是 Solscan SOL sentinel
|
||||
// SOL V2 池:WSOL_TOKEN_ACCOUNT
|
||||
// USDC 池:就是 USDC mint
|
||||
let quote_mint = e.quote_mint;
|
||||
|
||||
@@ -406,11 +414,12 @@ client.sell(sell_params).await?;
|
||||
|
||||
USDC 配对币必须用 USDC 买入、卖出也结算为 USDC;SOL/WSOL 只适用于 SOL 配对的 PumpFun 曲线。SDK 会在提交前拒绝 USDC quote 池的 SOL 输入,避免链上 6063 失败。
|
||||
消费 parser 事件时,需要把 `quoteMint`、`virtualQuoteReserves`、`realQuoteReserves` 传进 `PumpFunParams::from_trade(...)`;USDC 池初始虚拟 quote reserve 是 `4_292_000_000`。
|
||||
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`) | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
|
||||
| `WSOL_TOKEN_ACCOUNT` (`So111...11112`) | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
|
||||
| `USDC_TOKEN_ACCOUNT` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) |
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+54
-1
@@ -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,其他类型返回未实现错误。
|
||||
@@ -134,8 +143,9 @@ impl TradingInfrastructure {
|
||||
|
||||
// Create SWQOS clients with blacklist checking(QUIC 握手可能较慢,单节点超时 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 +372,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 +429,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
|
||||
@@ -742,6 +762,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 +894,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,
|
||||
@@ -983,6 +1010,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,
|
||||
@@ -1251,3 +1279,28 @@ impl TradingClient {
|
||||
Ok(signature.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::swqos::SwqosRegion;
|
||||
|
||||
#[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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+76
-14
@@ -1,8 +1,8 @@
|
||||
//! 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.
|
||||
//! `Pubkey::default()` and the Solscan SOL sentinel keep the smaller legacy SOL layout;
|
||||
//! explicit WSOL or USDC uses the V2 27/26-account unified metas.
|
||||
//! Default (`false`) keeps the smaller legacy SOL-paired instruction layout for latency.
|
||||
|
||||
use crate::{
|
||||
@@ -39,7 +39,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 +52,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` means legacy SOL-paired; use WSOL mint when a
|
||||
/// downstream V2 helper needs a concrete SPL quote mint.
|
||||
#[inline]
|
||||
fn effective_quote_mint_and_token_program(protocol_params: &PumpFunParams) -> (Pubkey, Pubkey) {
|
||||
let curve_quote_mint = protocol_params.bonding_curve.effective_quote_mint();
|
||||
@@ -126,7 +130,9 @@ 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("e_mint))
|
||||
let explicit_v2_quote = protocol_params.quote_mint != Pubkey::default()
|
||||
&& protocol_params.quote_mint != crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
Ok(explicit_v2_quote || !is_sol_quote_mint("e_mint))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -187,7 +193,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, ¶ms.output_mint);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -342,7 +348,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, ¶ms.input_mint);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -460,7 +466,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, ¶ms.output_mint);
|
||||
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -691,7 +698,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, ¶ms.input_mint);
|
||||
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
@@ -916,6 +923,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 +943,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(¶ms).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(¶ms).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();
|
||||
@@ -1111,7 +1134,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 +1154,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(¶ms).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(¶ms).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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -15,8 +15,8 @@ use std::sync::Arc;
|
||||
/// 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.
|
||||
/// Leave `quote_mint` default, or pass the Solscan SOL sentinel (`SOL_TOKEN_ACCOUNT`), for
|
||||
/// legacy SOL layout. Pass `WSOL_TOKEN_ACCOUNT` for SOL V2, or USDC for USDC-paired coins.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
@@ -33,19 +33,39 @@ 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 and `SOL_TOKEN_ACCOUNT` use legacy SOL layout.
|
||||
/// `WSOL_TOKEN_ACCOUNT` selects SOL v2; USDC selects USDC v2.
|
||||
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
|
||||
pub quote_mint: Pubkey,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
#[inline]
|
||||
fn quote_mint_for_layout(quote_mint: Pubkey) -> Pubkey {
|
||||
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
|
||||
Pubkey::default()
|
||||
} else {
|
||||
BondingCurveAccount::normalize_quote_mint(quote_mint)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey {
|
||||
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_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 +171,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` for legacy SOL layout,
|
||||
/// `WSOL_TOKEN_ACCOUNT` for SOL V2, and `USDC_TOKEN_ACCOUNT` for USDC V2.
|
||||
/// Also pass `is_cashback_coin` from the event so sells include the correct remaining accounts.
|
||||
///
|
||||
/// `mayhem_mode`:
|
||||
@@ -218,11 +234,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 +329,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 +369,14 @@ 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 legacy SOL-paired coins, pass `SOL_TOKEN_ACCOUNT` or leave default.
|
||||
/// Pass `WSOL_TOKEN_ACCOUNT` only when you intentionally want SOL v2 layout.
|
||||
#[inline]
|
||||
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
|
||||
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 +404,32 @@ 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
|
||||
);
|
||||
}
|
||||
|
||||
#[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
|
||||
);
|
||||
assert_eq!(
|
||||
PumpFunParams::quote_mint_for_rpc_return(crate::constants::WSOL_TOKEN_ACCOUNT),
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user