Compare commits

..

6 Commits

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

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

Adds an opt-in `SwapParams.wait_for_all_submits: bool` (default false,
plumbed through `TradeBuyParams` / `TradeSellParams`). When true and
`wait_tx_confirmed = false`, `execute_parallel` uses
`wait_for_all_submitted` (still in the codebase, was
`#[allow(dead_code)]`) so every submitted signature is returned. Zero
behaviour change for current users.
2026-05-27 08:02:49 +00:00
29 changed files with 320 additions and 34 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.14"
version = "4.0.17"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+11 -4
View File
@@ -79,19 +79,26 @@ This SDK is available in multiple languages:
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
## 🔖 Current Release
**Rust crate:** `sol-trade-sdk = "4.0.17"`
This release refreshes PumpFun 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
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.14" }
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.14"
sol-trade-sdk = "4.0.17"
```
## 🛠️ Usage Examples
+11 -4
View File
@@ -79,19 +79,26 @@
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
## 🔖 当前版本
**Rust crate:** `sol-trade-sdk = "4.0.17"`
本版本刷新 PumpFun 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) 的交易操作
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.14" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.17" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.14"
sol-trade-sdk = "4.0.17"
```
## 🛠️ 使用示例
+1
View File
@@ -179,6 +179,7 @@ async fn pumpfun_copy_trade_with_grpc(
)),
address_lookup_table_account,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
+2
View File
@@ -171,6 +171,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: true,
@@ -222,6 +223,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
with_tip: false,
durable_nonce: None,
create_output_token_ata: false,
+2
View File
@@ -139,6 +139,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -183,6 +184,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+10
View File
@@ -628,6 +628,7 @@ async fn handle_buy_pumpfun(
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -684,6 +685,7 @@ async fn handle_buy_pumpswap(
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -740,6 +742,7 @@ async fn handle_buy_bonk(
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -800,6 +803,7 @@ async fn handle_buy_raydium_v4(
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -861,6 +865,7 @@ async fn handle_buy_raydium_cpmm(
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: false,
create_mint_ata: create_mint_ata,
@@ -1031,6 +1036,7 @@ async fn handle_sell_pumpfun(
extension_params: DexParamEnum::PumpFun(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1090,6 +1096,7 @@ async fn handle_sell_pumpswap(
extension_params: DexParamEnum::PumpSwap(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1149,6 +1156,7 @@ async fn handle_sell_bonk(
extension_params: DexParamEnum::Bonk(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1211,6 +1219,7 @@ async fn handle_sell_raydium_v4(
extension_params: DexParamEnum::RaydiumAmmV4(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -1274,6 +1283,7 @@ async fn handle_sell_raydium_cpmm(
extension_params: DexParamEnum::RaydiumCpmm(param),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -43,6 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
close_input_token_ata: false, //if input token is SOL/WSOL,set to true,if input token is USDC,set to false.
create_mint_ata: true,
@@ -84,6 +85,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_output_token_ata: false, //if output token is SOL/WSOL,set to true,if output token is USDC,set to false.
close_mint_token_ata: false,
+1
View File
@@ -104,6 +104,7 @@ async fn test_middleware() -> AnyResult<()> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
+1
View File
@@ -175,6 +175,7 @@ async fn pumpfun_copy_trade_with_grpc(
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -169,6 +169,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
@@ -220,6 +221,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: false,
close_output_token_ata: false,
close_mint_token_ata: false,
@@ -158,6 +158,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -203,6 +204,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
)),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -81,6 +82,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+2
View File
@@ -236,6 +236,7 @@ async fn pumpswap_trade_with_grpc(
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_sol,
close_input_token_ata: is_sol,
create_mint_ata: true,
@@ -277,6 +278,7 @@ async fn pumpswap_trade_with_grpc(
extension_params: DexParamEnum::PumpSwap(params.clone()),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_sol,
close_output_token_ata: is_sol,
close_mint_token_ata: false,
@@ -157,6 +157,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -199,6 +200,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
extension_params: DexParamEnum::RaydiumAmmV4(params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
@@ -161,6 +161,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
extension_params: DexParamEnum::RaydiumCpmm(buy_params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: is_wsol,
close_input_token_ata: is_wsol,
create_mint_ata: true,
@@ -201,6 +202,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
extension_params: DexParamEnum::RaydiumCpmm(sell_params),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: is_wsol,
close_output_token_ata: is_wsol,
close_mint_token_ata: false,
+2
View File
@@ -42,6 +42,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_input_token_ata: true,
close_input_token_ata: true,
create_mint_ata: true,
@@ -84,6 +85,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
),
address_lookup_table_account: None,
wait_tx_confirmed: true,
wait_for_all_submits: false,
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
+54 -1
View File
@@ -50,6 +50,15 @@ fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool {
}
}
#[inline]
fn normalize_swqos_configs(rpc_url: &str, configs: &[SwqosConfig]) -> Vec<SwqosConfig> {
let mut out = configs.to_vec();
if !out.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)) {
out.push(SwqosConfig::Default(rpc_url.to_string()));
}
out
}
/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。
///
/// * `dex_type`PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。
@@ -134,8 +143,9 @@ impl TradingInfrastructure {
// Create SWQOS clients with blacklist checkingQUIC 握手可能较慢,单节点超时 15s)
const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
let swqos_configs = normalize_swqos_configs(&config.rpc_url, &config.swqos_configs);
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
for swqos in &config.swqos_configs {
for swqos in &swqos_configs {
if swqos.is_blacklisted() {
if sdk_log::sdk_log_enabled() {
warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
@@ -362,6 +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));
}
}
+9 -4
View File
@@ -156,8 +156,13 @@ mod tests {
#[test]
fn test_fast_now_overhead() {
// 测试调用开销
let iterations = 10_000;
// This is a coarse regression guard, not a benchmark. On non-Linux targets
// the fast timer uses `Instant::elapsed()`, and CI/desktop scheduler jitter can
// move a single run above 100ns even when the code path is still sub-microsecond.
let iterations = 100_000;
for _ in 0..1_000 {
let _ = fast_now_nanos();
}
let start = Instant::now();
for _ in 0..iterations {
@@ -171,8 +176,8 @@ mod tests {
println!("Average fast_now_nanos() call: {}ns", avg_per_call);
}
// 快速时间戳应该非常快(< 100ns per call
assert!(avg_per_call < 100);
// Keep the hot-path timer safely sub-microsecond without making the test flaky.
assert!(avg_per_call < 500);
}
#[test]
+1
View File
@@ -382,6 +382,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+1
View File
@@ -364,6 +364,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+107 -10
View File
@@ -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
@@ -190,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, &params.output_mint);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -345,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, &params.input_mint);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -463,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, &params.output_mint);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -582,7 +586,10 @@ fn build_buy_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
}
};
if params.create_input_mint_ata {
if params.create_input_mint_ata
|| (quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
&& params.input_mint == crate::constants::SOL_TOKEN_ACCOUNT)
{
push_create_or_wrap_user_token_account(
&mut instructions,
&params.payer.pubkey(),
@@ -694,7 +701,7 @@ fn build_sell_unified(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params, &params.input_mint);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -919,6 +926,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,
@@ -938,18 +946,33 @@ mod tests {
}
#[test]
fn pump_suffix_buy_respects_explicit_legacy_token_program() {
fn pump_suffix_buy_forces_token_2022_even_with_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
let params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
let instructions = build_buy(&params).unwrap();
assert_eq!(instructions.len(), 3);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM);
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM_2022);
assert_eq!(instructions[1].program_id, TOKEN_PROGRAM_2022);
assert_eq!(instructions[2].accounts.len(), 18);
assert_eq!(instructions[2].data.len(), 25);
}
#[test]
fn pump_suffix_sell_forces_token_2022_even_with_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = pump_mint();
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
params.create_output_mint_ata = false;
let instructions = build_sell(&params).unwrap();
let ix = instructions.last().unwrap();
assert_eq!(ix.accounts[9].pubkey, TOKEN_PROGRAM_2022);
}
#[test]
fn non_pump_buy_respects_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
@@ -1005,7 +1028,6 @@ mod tests {
fn pumpfun_v2_regular_buy_wraps_max_quote_budget() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.create_output_mint_ata = false;
params.create_input_mint_ata = true;
params.use_exact_sol_amount = Some(false);
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
*protocol_params =
@@ -1014,6 +1036,7 @@ mod tests {
let instructions = build_buy(&params).unwrap();
let transfer_ix = &instructions[1];
let buy_ix = instructions.last().unwrap();
let system_ix = bincode::deserialize::<
solana_system_interface::instruction::SystemInstruction,
>(&transfer_ix.data)
@@ -1029,6 +1052,63 @@ mod tests {
}
other => panic!("unexpected system instruction: {:?}", other),
}
assert_eq!(&buy_ix.data[..8], crate::instruction::utils::pumpfun::BUY_V2_DISCRIMINATOR);
assert_eq!(buy_ix.accounts.len(), 27);
}
#[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]
@@ -1160,6 +1240,23 @@ mod tests {
assert_eq!(ix.accounts.len(), 18);
}
#[test]
fn pumpfun_rpc_sol_sentinel_quote_mint_selects_v1() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
params.create_output_mint_ata = false;
if let DexParamEnum::PumpFun(protocol_params) = &mut params.protocol_params {
protocol_params.quote_mint = crate::constants::SOL_TOKEN_ACCOUNT;
assert_eq!(protocol_params.quote_mint, crate::constants::SOL_TOKEN_ACCOUNT);
}
let ix = build_buy(&params).unwrap().pop().unwrap();
assert_eq!(
&ix.data[..8],
crate::instruction::utils::pumpfun::BUY_EXACT_SOL_IN_DISCRIMINATOR
);
assert_eq!(ix.accounts.len(), 18);
}
#[test]
fn pumpfun_v2_usdc_buy_rejects_sol_input() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
+1
View File
@@ -589,6 +589,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+1
View File
@@ -382,6 +382,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+1
View File
@@ -394,6 +394,7 @@ mod tests {
gas_fee_strategy: GasFeeStrategy::new(),
simulate: true,
log_enabled: false,
wait_for_all_submits: false,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: 0,
+35 -8
View File
@@ -147,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;
}
}
}
@@ -232,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>,
@@ -496,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,
@@ -600,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,
@@ -735,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(FAST_SUBMIT_RESULT_TIMEOUT).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
));
let ret = if wait_for_all_submits {
collector.wait_for_all_submitted(FAST_SUBMIT_RESULT_TIMEOUT.as_secs()).await.unwrap_or(
(
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
),
)
} else {
collector.wait_for_first_submitted(FAST_SUBMIT_RESULT_TIMEOUT).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within submit result window")),
vec![],
))
};
let (success, signatures, last_error, submit_timings) = ret;
return Ok((success, signatures, last_error, submit_timings));
}
+5
View File
@@ -157,6 +157,10 @@ impl TradeExecutor for GenericTradeExecutor {
}
let need_confirm = params.wait_tx_confirmed;
// When the caller confirms externally (need_confirm = false) and opts in
// via SwapParams.wait_for_all_submits, return every route's signature so
// pinned-nonce confirmation can poll all of them.
let wait_for_all_submits = !need_confirm && params.wait_for_all_submits;
let sender_config = params.sender_concurrency_config();
let result = execute_parallel(
params.swqos_clients.as_slice(),
@@ -169,6 +173,7 @@ impl TradeExecutor for GenericTradeExecutor {
self.protocol_name,
is_buy,
false, // submit only here; confirmation and log timing handled below
wait_for_all_submits,
if is_buy { true } else { params.with_tip },
params.gas_fee_strategy,
params.use_dedicated_sender_threads,
+8
View File
@@ -82,6 +82,14 @@ pub struct SwapParams {
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
/// SWQOS route's HTTP submit response so all submitted signatures are
/// returned. Defaults to false (post-4.0.11 behaviour: return after the
/// first route accepts). Set to true when the caller does its own on-chain
/// confirmation against a pinned durable nonce — only one route's tx can
/// land, but the caller cannot know which in advance, so it needs every
/// signature to poll via `getSignatureStatuses`.
pub wait_for_all_submits: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
+41 -2
View File
@@ -33,7 +33,8 @@ 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>,
@@ -56,6 +57,15 @@ impl PumpFunParams {
}
}
#[inline]
fn quote_mint_for_rpc_return(quote_mint: Pubkey) -> Pubkey {
if quote_mint == Pubkey::default() || quote_mint == crate::constants::SOL_TOKEN_ACCOUNT {
crate::constants::SOL_TOKEN_ACCOUNT
} else {
BondingCurveAccount::normalize_quote_mint(quote_mint)
}
}
pub fn immediate_sell(
creator_vault: Pubkey,
token_program: Pubkey,
@@ -319,7 +329,7 @@ impl PumpFunParams {
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
quote_mint: Self::quote_mint_for_layout(quote_mint),
quote_mint: Self::quote_mint_for_rpc_return(quote_mint),
})
}
@@ -394,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
);
}
}