Compare commits

...

7 Commits

Author SHA1 Message Date
0xfnzero 57e4c5d221 Fix PumpFun V2 buyback fee mutability 2026-05-22 08:56:20 +08:00
0xfnzero 8ed2aef931 Fix trade execution correctness and release 4.0.11 2026-05-21 17:50:06 +08:00
0xfnzero 0cf0064e80 docs: clarify PumpFun V2 configuration 2026-05-21 14:48:43 +08:00
0xfnzero 4968c48a2e Release sol-trade-sdk v4.0.10
Sync Pump program IDLs with the latest definitions.
2026-05-20 19:41:16 +08:00
0xfnzero 5908c84798 Merge remote-tracking branch 'origin/main' into codex/swqos-dual-fee-mode 2026-05-19 06:21:44 +08:00
0xfnzero 6dab47f8f9 Optimize SWQOS dual fee lane submission 2026-05-19 06:21:37 +08:00
0xfnzero a076e0f8cd chore: flatten synced IDL files
Remove the redundant idl/bitquery source snapshot tree and keep synced Bitquery IDLs only under the SDK's canonical local IDL filenames.
2026-05-16 15:03:10 +08:00
29 changed files with 4610 additions and 19241 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.9"
version = "4.0.12"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
+7 -3
View File
@@ -152,7 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // default: false - filter SWQOS below min tip
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
// .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
// .use_pumpfun_v2(true) // PumpFun V2 (27 accounts, quote_mint); required for USDC-paired PumpFun coins
.build();
// Create TradingClient
@@ -376,7 +376,7 @@ PumpFun has two instruction sets for bonding-curve trading:
**How to enable V2:**
**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet):
**Method 1 — Global runtime flag** (recommended for `TradingClient`; required for USDC-paired coins):
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
@@ -384,7 +384,9 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.build();
```
**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
**Method 2 — Set the quote mint on `PumpFunParams`**:
When using the high-level `TradingClient`, keep `.use_pumpfun_v2(true)` enabled in `TradeConfig` and set `quote_mint` on the PumpFun params to select the pair:
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
@@ -399,6 +401,8 @@ let params = PumpFunParams::from_trade(/* ... */)
.with_quote_mint(USDC_TOKEN_ACCOUNT);
```
`with_quote_mint(...)` also marks the params as V2-capable for lower-level instruction builders, but the high-level `TradingClient` uses the client-level `use_pumpfun_v2` runtime flag when choosing V1 vs V2.
> **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.
#### PumpSwap: coin_creator_vault from events (no RPC)
+12 -1
View File
@@ -152,6 +152,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
// .mev_protection(false) // 默认: false - MEVAstralane QUIC :9000 或 HTTP mev-protect / BlockRazor
// .use_pumpfun_v2(true) // PumpFun V227 个账户,quote_mint);USDC 配对 PumpFun 币必须开启
.build();
// 创建 TradingClient
@@ -372,7 +373,15 @@ Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过
**使用方式:**
设置 `PumpFunParams``quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局
高层 `TradingClient` 需要先在 `TradeConfig` 开启 PumpFun V2USDC 配对币必须开启
```rust
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.use_pumpfun_v2(true)
.build();
```
然后在 `PumpFunParams` 设置 `quote_mint` 来选择 SOL/USDC 配对:
```rust
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
@@ -391,6 +400,8 @@ client.buy(buy_params).await?;
client.sell(sell_params).await?;
```
`with_quote_mint(...)` 会把参数标记为可使用 V2,底层 instruction builder 可据此走 V2;但高层 `TradingClient` 选择 V1/V2 时使用的是 client 级别的 `use_pumpfun_v2` 运行时开关。
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|-----------|-------------|---------|------|
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
+12 -10
View File
@@ -1,14 +1,16 @@
# IDL sources
# IDL files
Bitquery IDLs synced from `bitquery/solana-idl-lib` commit `f804b17`.
Supported DEX IDLs are stored directly in this directory. The files below were
synced from `bitquery/solana-idl-lib` commit `f804b17` and written to the SDK's
canonical local filenames.
| SDK protocol | Root IDL | Bitquery source |
| SDK protocol | Local IDL | Source path |
| --- | --- | --- |
| Bonk / Raydium Launchpad | `bonk.json` | `bitquery/raydium/launchpad.json` |
| Raydium CPMM | `raydium_cpmm.json` | `bitquery/raydium/raydium_cp.json` |
| Raydium AMM v4 | `raydium_amm_v4.json` | `bitquery/raydium/raydium_amm.json` |
| Meteora DAMM v2 | `meteora_damm_v2.json` | `bitquery/meteora/cp_amm_016.json` |
| PumpFun | `pump.json` | `bitquery/pumpfun/pump.json` |
| PumpSwap | `pump_amm.json` | `bitquery/pumpswap/amm.json` |
| Bonk / Raydium Launchpad | `bonk.json` | `raydium/launchpad.json` |
| Raydium CPMM | `raydium_cpmm.json` | `raydium/raydium_cp.json` |
| Raydium AMM v4 | `raydium_amm_v4.json` | `raydium/raydium_amm.json` |
| Meteora DAMM v2 | `meteora_damm_v2.json` | `meteora/cp_amm_016.json` |
| PumpFun | `pump.json` | `pumpfun/pump.json` |
| PumpSwap | `pump_amm.json` | `pumpswap/amm.json` |
The root PumpFun/PumpSwap IDLs remain the newer local copies used for cashback and v2 instruction parity. The Bitquery snapshots are kept under `idl/bitquery/` for source comparison.
No separate source snapshot tree is kept in the repository.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+288
View File
@@ -1943,6 +1943,117 @@
],
"args": []
},
{
"name": "collect_creator_fee_v2",
"docs": [
"Collects creator_fee from creator_vault to the coin creator account"
],
"discriminator": [207, 17, 138, 242, 4, 34, 19, 56],
"accounts": [
{
"name": "creator",
"writable": true
},
{
"name": "creator_token_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "creator"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "creator_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116
]
},
{
"kind": "account",
"path": "creator"
}
]
}
},
{
"name": "creator_vault_token_account",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "creator_vault"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "quote_mint"
},
{
"name": "quote_token_program"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111,
114, 105, 116, 121
]
}
]
}
},
{
"name": "program"
}
],
"args": []
},
{
"name": "create",
"docs": ["Creates a new coin and bonding curve."],
@@ -2446,6 +2557,153 @@
}
}
},
{
"name": "distribute_creator_fees_v2",
"discriminator": [255, 203, 19, 79, 244, 68, 8, 159],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "mint",
"relations": ["sharing_config"]
},
{
"name": "bonding_curve",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101
]
},
{
"kind": "account",
"path": "mint"
}
]
}
},
{
"name": "sharing_config",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
115, 104, 97, 114, 105, 110, 103, 45, 99, 111, 110, 102, 105,
103
]
},
{
"kind": "account",
"path": "mint"
}
],
"program": {
"kind": "const",
"value": [
12, 53, 255, 169, 5, 90, 142, 86, 141, 168, 247, 188, 7, 86, 21,
39, 76, 241, 201, 44, 164, 31, 64, 0, 156, 81, 106, 164, 20,
194, 124, 112
]
}
}
},
{
"name": "creator_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99, 114, 101, 97, 116, 111, 114, 45, 118, 97, 117, 108, 116
]
},
{
"kind": "account",
"path": "bonding_curve.creator",
"account": "BondingCurve"
}
]
}
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95, 95, 101, 118, 101, 110, 116, 95, 97, 117, 116, 104, 111,
114, 105, 116, 121
]
}
]
}
},
{
"name": "program",
"address": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"
},
{
"name": "creator_vault_quote_token_account",
"docs": [
"Deserialized manually in the handler for non-legacy quote mints."
],
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "creator_vault"
},
{
"kind": "account",
"path": "quote_token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "quote_mint"
},
{
"name": "quote_token_program"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
}
],
"args": [
{
"name": "initialize_ata",
"type": "bool"
}
],
"returns": {
"defined": {
"name": "DistributeCreatorFeesEvent"
}
}
},
{
"name": "extend_account",
"docs": ["Extends the size of program-owned accounts"],
@@ -5256,6 +5514,16 @@
"code": 6069,
"name": "QuoteMintNotEligibleForWhitelist",
"msg": "Quote mint cannot be added or removed via whitelist (default or native SOL mint)"
},
{
"code": 6070,
"name": "UnableToDistributeCreatorFeesToUninitializedAccount",
"msg": "Unable to distribute creator fees to uninitialized account"
},
{
"code": 6071,
"name": "MayhemModeQuoteMintNotAllowed",
"msg": "Mayhem mode quote mint not allowed"
}
],
"types": [
@@ -5495,6 +5763,10 @@
{
"name": "creator_fee",
"type": "u64"
},
{
"name": "quote_mint",
"type": "pubkey"
}
]
}
@@ -5563,6 +5835,10 @@
{
"name": "pool",
"type": "pubkey"
},
{
"name": "quote_mint",
"type": "pubkey"
}
]
}
@@ -5695,6 +5971,10 @@
{
"name": "distributed",
"type": "u64"
},
{
"name": "quote_mint",
"type": "pubkey"
}
]
}
@@ -6517,6 +6797,14 @@
{
"name": "total_cashback_claimed",
"type": "u64"
},
{
"name": "stable_cashback_earned",
"type": "u64"
},
{
"name": "total_stable_cashback_claimed",
"type": "u64"
}
]
}
+381 -1
View File
@@ -3965,6 +3965,254 @@
],
"args": []
},
{
"name": "transfer_creator_fees_to_pump_v2",
"discriminator": [
1,
33,
78,
185,
33,
67,
44,
92
],
"accounts": [
{
"name": "payer",
"writable": true,
"signer": true
},
{
"name": "quote_mint"
},
{
"name": "token_program"
},
{
"name": "system_program",
"address": "11111111111111111111111111111111"
},
{
"name": "associated_token_program",
"address": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"
},
{
"name": "coin_creator"
},
{
"name": "coin_creator_vault_authority",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99,
114,
101,
97,
116,
111,
114,
95,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "coin_creator"
}
]
}
},
{
"name": "coin_creator_vault_ata",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "coin_creator_vault_authority"
},
{
"kind": "account",
"path": "token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "const",
"value": [
140,
151,
37,
143,
78,
36,
137,
241,
187,
61,
16,
41,
20,
142,
13,
131,
11,
90,
19,
153,
218,
255,
16,
132,
4,
142,
123,
216,
219,
233,
248,
89
]
}
}
},
{
"name": "pump_creator_vault",
"writable": true,
"pda": {
"seeds": [
{
"kind": "const",
"value": [
99,
114,
101,
97,
116,
111,
114,
45,
118,
97,
117,
108,
116
]
},
{
"kind": "account",
"path": "coin_creator"
}
],
"program": {
"kind": "const",
"value": [
1,
86,
224,
246,
147,
102,
90,
207,
68,
219,
21,
104,
191,
23,
91,
170,
81,
137,
203,
151,
245,
210,
255,
59,
101,
93,
43,
182,
253,
109,
24,
176
]
}
}
},
{
"name": "pump_creator_vault_ata",
"writable": true,
"pda": {
"seeds": [
{
"kind": "account",
"path": "pump_creator_vault"
},
{
"kind": "account",
"path": "token_program"
},
{
"kind": "account",
"path": "quote_mint"
}
],
"program": {
"kind": "account",
"path": "associated_token_program"
}
}
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": []
},
{
"name": "update_admin",
"discriminator": [
@@ -4027,6 +4275,72 @@
],
"args": []
},
{
"name": "update_buyback_config",
"discriminator": [
251,
224,
171,
146,
160,
26,
113,
233
],
"accounts": [
{
"name": "admin",
"signer": true,
"relations": [
"global_config"
]
},
{
"name": "global_config",
"writable": true
},
{
"name": "event_authority",
"pda": {
"seeds": [
{
"kind": "const",
"value": [
95,
95,
101,
118,
101,
110,
116,
95,
97,
117,
116,
104,
111,
114,
105,
116,
121
]
}
]
}
},
{
"name": "program"
}
],
"args": [
{
"name": "buyback_basis_points",
"type": {
"option": "u64"
}
}
]
},
{
"name": "update_fee_config",
"discriminator": [
@@ -4835,7 +5149,34 @@
},
{
"code": 6052,
"name": "CashbackEarnedDoesNotMatchTokenInVault"
"name": "TokensInVaultLessThanCashbackEarned"
},
{
"code": 6053,
"name": "BuybackFeeRecipientNotAuthorized",
"msg": "Buyback fee recipient not authorized"
},
{
"code": 6054,
"name": "AllBuybackFeeRecipientsShouldBeNonZero"
},
{
"code": 6055,
"name": "NotUniqueBuybackFeeRecipients"
},
{
"code": 6056,
"name": "BuybackBasisPointsOutOfRange",
"msg": "buyback_basis_points must be <= 10_000"
},
{
"code": 6057,
"name": "WrongBuybackFeeRecipientsCount",
"msg": "buyback fee recipients require exactly 8 remaining accounts (or none)"
},
{
"code": 6058,
"name": "BuybackFeeRecipientMissing"
}
],
"types": [
@@ -5086,6 +5427,14 @@
{
"name": "cashback",
"type": "u64"
},
{
"name": "buyback_fee_basis_points",
"type": "u64"
},
{
"name": "buyback_fee",
"type": "u64"
}
]
}
@@ -5523,6 +5872,16 @@
}
}
}
},
{
"name": "stable_fee_tiers",
"type": {
"vec": {
"defined": {
"name": "FeeTier"
}
}
}
}
]
}
@@ -5646,6 +6005,19 @@
{
"name": "is_cashback_enabled",
"type": "bool"
},
{
"name": "buyback_fee_recipients",
"type": {
"array": [
"pubkey",
8
]
}
},
{
"name": "buyback_basis_points",
"type": "u64"
}
]
}
@@ -5941,6 +6313,14 @@
{
"name": "cashback",
"type": "u64"
},
{
"name": "buyback_fee_basis_points",
"type": "u64"
},
{
"name": "buyback_fee",
"type": "u64"
}
]
}
+3146 -4
View File
File diff suppressed because it is too large Load Diff
+25 -6
View File
@@ -14,6 +14,7 @@ use crate::constants::WSOL_TOKEN_ACCOUNT;
use crate::swqos::common::TradeError;
use crate::swqos::SwqosClient;
use crate::swqos::SwqosConfig;
use crate::swqos::SwqosType;
use crate::swqos::TradeType;
use crate::trading::core::params::BonkParams;
use crate::trading::core::params::DexParamEnum;
@@ -83,7 +84,7 @@ pub struct TradingInfrastructure {
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
/// Configuration used to create this infrastructure
pub config: InfrastructureConfig,
/// Precomputed at init: min(swqos_clients.len(), 2/3 * num_cores). Not computed on trade hot path.
/// Precomputed at init: min(max SWQOS submit lanes, 2/3 * num_cores). Not computed on trade hot path.
pub max_sender_concurrency: usize,
/// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path.
pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
@@ -225,11 +226,21 @@ impl TradingInfrastructure {
eprintln!("️ SWQOS 通道已就绪: {} 条 → [{}]", swqos_clients.len(), labels.join(", "));
}
let swqos_count = swqos_clients.len();
let max_submit_lanes = swqos_clients
.iter()
.map(|client| {
if matches!(client.get_swqos_type(), SwqosType::Default) {
1usize
} else {
2usize
}
})
.sum::<usize>()
.max(1);
let (max_sender_concurrency, effective_core_ids) = {
let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0);
let max_by_cores = (num_cores * 2 / 3).max(1);
let cap = swqos_count.min(max_by_cores).max(1);
let cap = max_submit_lanes.min(max_by_cores).max(1);
let ids = core_affinity::get_core_ids()
.map(|all| {
let v: Vec<_> = all.into_iter().collect();
@@ -713,7 +724,7 @@ impl TradingClient {
/// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores).
/// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs.
/// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores).
/// Concurrency and core count are capped internally (≤ max submit lanes, ≤ 2/3 of CPU cores).
/// - `None`: keep default (shared tokio pool).
/// - `Some(vec![])`: dedicated threads with default count, no core pinning.
/// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap).
@@ -875,7 +886,11 @@ impl TradingClient {
let swap_result = executor.swap(buy_params).await;
let result = swap_result.map(|(success, sigs, err, timings)| {
(success, sigs, err.map(TradeError::from), timings)
let legacy_timings = timings
.into_iter()
.map(|timing| (timing.swqos_type, timing.submit_done_us))
.collect();
(success, sigs, err.map(TradeError::from), legacy_timings)
});
result
}
@@ -987,7 +1002,11 @@ impl TradingClient {
let swap_result = executor.swap(sell_params).await;
let result = swap_result.map(|(success, sigs, err, timings)| {
(success, sigs, err.map(TradeError::from), timings)
let legacy_timings = timings
.into_iter()
.map(|timing| (timing.swqos_type, timing.submit_done_us))
.collect();
(success, sigs, err.map(TradeError::from), legacy_timings)
});
result
}
+64 -25
View File
@@ -4,7 +4,7 @@ use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
use crate::common::{
spl_associated_token_account::get_associated_token_address_with_program_id,
@@ -21,6 +21,22 @@ const MAX_PDA_CACHE_SIZE: usize = 100_000;
const MAX_ATA_CACHE_SIZE: usize = 100_000;
const MAX_INSTRUCTION_CACHE_SIZE: usize = 100_000;
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
// --------------------- Instruction Cache ---------------------
/// Instruction cache key for uniquely identifying instruction types and parameters
@@ -66,7 +82,10 @@ where
};
// Lock-free cache lookup with entry API
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
let instructions =
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone();
prune_cache(&INSTRUCTION_CACHE, MAX_INSTRUCTION_CACHE_SIZE);
instructions
}
// --------------------- Associated Token Account ---------------------
@@ -106,8 +125,26 @@ pub fn _create_associated_token_account_idempotent_fast(
use_seed,
};
// Only use seed if the mint address is not wSOL or SOL
// 🔧 修复:Token-2022 也支持 seed 方式(白名单方式更安全)
let build_standard_create = || {
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
};
// Only use seed if the mint address is not wSOL or SOL.
// Seed accounts are deterministic token accounts, not ATAs; callers must ensure they are not
// recreating an existing seeded account. Fall back to idempotent ATA if seed assembly fails.
let arc_instructions = if use_seed
&& !mint.eq(&crate::constants::WSOL_TOKEN_ACCOUNT)
&& !mint.eq(&crate::constants::SOL_TOKEN_ACCOUNT)
@@ -117,29 +154,18 @@ pub fn _create_associated_token_account_idempotent_fast(
// Use cache to get instruction
get_cached_instructions(cache_key, || {
super::seed::create_associated_token_account_use_seed(payer, owner, mint, token_program)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account setup failed for mint {}: {}; fallback to idempotent ATA",
mint,
err
);
build_standard_create()
})
})
} else {
// Use cache to get instruction
get_cached_instructions(cache_key, || {
// Get Associated Token Address using cache
let associated_token_address =
get_associated_token_address_with_program_id_fast(owner, mint, token_program);
// Create Associated Token Account instruction
// Reference implementation of spl_associated_token_account::instruction::create_associated_token_account
vec![Instruction {
program_id: crate::constants::ASSOCIATED_TOKEN_PROGRAM_ID,
accounts: vec![
AccountMeta::new(*payer, true), // Payer (signer, writable)
AccountMeta::new(associated_token_address, false), // ATA address (writable, non-signer)
AccountMeta::new_readonly(*owner, false), // Token account owner (readonly, non-signer)
AccountMeta::new_readonly(*mint, false), // Token mint address (readonly, non-signer)
crate::constants::SYSTEM_PROGRAM_META,
AccountMeta::new_readonly(*token_program, false), // Token program (readonly, non-signer)
],
data: vec![1],
}]
})
get_cached_instructions(cache_key, build_standard_create)
};
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
@@ -183,6 +209,7 @@ where
if let Some(pda) = pda_result {
PDA_CACHE.insert(cache_key, pda);
prune_cache(&PDA_CACHE, MAX_PDA_CACHE_SIZE);
}
pda_result
@@ -265,7 +292,18 @@ fn _get_associated_token_address_with_program_id_fast(
token_mint_address,
token_program_id,
)
.unwrap()
.unwrap_or_else(|err| {
tracing::warn!(
"seed token account address failed for mint {}: {}; fallback to ATA",
token_mint_address,
err
);
get_associated_token_address_with_program_id(
wallet_address,
token_mint_address,
token_program_id,
)
})
} else {
get_associated_token_address_with_program_id(
wallet_address,
@@ -276,6 +314,7 @@ fn _get_associated_token_address_with_program_id_fast(
// Store computation result in cache (lock-free)
ATA_CACHE.insert(cache_key, ata);
prune_cache(&ATA_CACHE, MAX_ATA_CACHE_SIZE);
ata
}
+179 -10
View File
@@ -1,6 +1,6 @@
use crate::swqos::{SwqosType, TradeType};
use arc_swap::ArcSwap;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -14,6 +14,15 @@ impl GasFeeStrategyType {
pub fn values() -> Vec<Self> {
vec![Self::Normal, Self::LowTipHighCuPrice, Self::HighTipLowCuPrice]
}
#[inline]
pub fn as_str(&self) -> &'static str {
match self {
Self::Normal => "Normal",
Self::LowTipHighCuPrice => "LowTipHighCuPrice",
Self::HighTipLowCuPrice => "HighTipLowCuPrice",
}
}
}
#[derive(Debug, Clone, Copy)]
@@ -84,6 +93,33 @@ impl GasFeeStrategy {
);
}
/// 设置 Default/RPC 的优先费-only 策略。Default 没有 relay tip account
/// 但仍应携带 ComputeBudget 优先费。
pub fn set_default_rpc_fee_strategy(
&self,
buy_cu_limit: u32,
sell_cu_limit: u32,
buy_cu_price: u64,
sell_cu_price: u64,
) {
self.set(
SwqosType::Default,
TradeType::Buy,
GasFeeStrategyType::Normal,
buy_cu_limit,
buy_cu_price,
0.0,
);
self.set(
SwqosType::Default,
TradeType::Sell,
GasFeeStrategyType::Normal,
sell_cu_limit,
sell_cu_price,
0.0,
);
}
/// 为多个服务类型添加高低费率策略,会移除(SwqosType,TradeType)的默认策略。
/// Add high-low fee strategies for multiple service types, Will remove the default strategy of (SwqosType,TradeType)
pub fn set_high_low_fee_strategies(
@@ -271,7 +307,7 @@ impl GasFeeStrategy {
) -> Vec<(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)> {
let strategies = self.strategies.load();
let mut result = Vec::new();
let mut swqos_types = std::collections::HashSet::new();
let mut swqos_types = HashSet::new();
for (swqos_type, t_type, _) in strategies.keys() {
if *t_type == trade_type {
swqos_types.insert(*swqos_type);
@@ -298,10 +334,17 @@ impl GasFeeStrategy {
/// 动态更新买入小费(保持其他参数不变)
/// Dynamically update buy tip (keep other parameters unchanged)
pub fn update_buy_tip(&self, buy_tip: f64) {
self.update_buy_tip_for_strategy(GasFeeStrategyType::Normal, buy_tip);
}
/// 动态更新指定买入策略的小费(保持其他参数不变)。
/// Dynamic updates should generally target Normal only; updating all strategies would
/// collapse the low-tip/high-tip dual-lane spread into the same tip.
pub fn update_buy_tip_for_strategy(&self, strategy_type: GasFeeStrategyType, buy_tip: f64) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy && *s_type == strategy_type {
value.tip = buy_tip;
}
}
@@ -312,10 +355,15 @@ impl GasFeeStrategy {
/// 动态更新卖出小费(保持其他参数不变)
/// Dynamically update sell tip (keep other parameters unchanged)
pub fn update_sell_tip(&self, sell_tip: f64) {
self.update_sell_tip_for_strategy(GasFeeStrategyType::Normal, sell_tip);
}
/// 动态更新指定卖出策略的小费(保持其他参数不变)。
pub fn update_sell_tip_for_strategy(&self, strategy_type: GasFeeStrategyType, sell_tip: f64) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell && *s_type == strategy_type {
value.tip = sell_tip;
}
}
@@ -326,10 +374,19 @@ impl GasFeeStrategy {
/// 动态更新买入优先费(保持其他参数不变)
/// Dynamically update buy compute unit price (keep other parameters unchanged)
pub fn update_buy_cu_price(&self, buy_cu_price: u64) {
self.update_buy_cu_price_for_strategy(GasFeeStrategyType::Normal, buy_cu_price);
}
/// 动态更新指定买入策略的优先费(保持其他参数不变)。
pub fn update_buy_cu_price_for_strategy(
&self,
strategy_type: GasFeeStrategyType,
buy_cu_price: u64,
) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy && *s_type == strategy_type {
value.cu_price = buy_cu_price;
}
}
@@ -340,10 +397,19 @@ impl GasFeeStrategy {
/// 动态更新卖出优先费(保持其他参数不变)
/// Dynamically update sell compute unit price (keep other parameters unchanged)
pub fn update_sell_cu_price(&self, sell_cu_price: u64) {
self.update_sell_cu_price_for_strategy(GasFeeStrategyType::Normal, sell_cu_price);
}
/// 动态更新指定卖出策略的优先费(保持其他参数不变)。
pub fn update_sell_cu_price_for_strategy(
&self,
strategy_type: GasFeeStrategyType,
sell_cu_price: u64,
) {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell {
for ((_swqos_type, trade_type, s_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell && *s_type == strategy_type {
value.cu_price = sell_cu_price;
}
}
@@ -365,3 +431,106 @@ impl GasFeeStrategy {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn find_strategy(
strategies: &[(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)],
swqos_type: SwqosType,
strategy_type: GasFeeStrategyType,
) -> GasFeeStrategyValue {
strategies
.iter()
.find(|(s, t, _)| *s == swqos_type && *t == strategy_type)
.map(|(_, _, v)| *v)
.expect("strategy exists")
}
#[test]
fn high_low_fee_strategy_expands_two_lanes_per_swqos() {
let strategy = GasFeeStrategy::new();
strategy.set_high_low_fee_strategies(
&[SwqosType::Jito, SwqosType::Helius],
TradeType::Buy,
100_000,
180_000,
400_000,
0.002,
0.005,
);
let strategies = strategy.get_strategies(TradeType::Buy);
assert_eq!(strategies.len(), 4);
for swqos_type in [SwqosType::Jito, SwqosType::Helius] {
let low_tip_high_cu =
find_strategy(&strategies, swqos_type, GasFeeStrategyType::LowTipHighCuPrice);
assert_eq!(low_tip_high_cu.cu_limit, 100_000);
assert_eq!(low_tip_high_cu.cu_price, 400_000);
assert_eq!(low_tip_high_cu.tip, 0.002);
let high_tip_low_cu =
find_strategy(&strategies, swqos_type, GasFeeStrategyType::HighTipLowCuPrice);
assert_eq!(high_tip_low_cu.cu_limit, 100_000);
assert_eq!(high_tip_low_cu.cu_price, 180_000);
assert_eq!(high_tip_low_cu.tip, 0.005);
}
}
#[test]
fn dynamic_updates_do_not_collapse_dual_lane_fees() {
let strategy = GasFeeStrategy::new();
strategy.set_high_low_fee_strategy(
SwqosType::Jito,
TradeType::Buy,
100_000,
180_000,
400_000,
0.002,
0.005,
);
strategy.update_buy_tip(0.009);
strategy.update_buy_cu_price(999_999);
let strategies = strategy.get_strategies(TradeType::Buy);
let low_tip_high_cu =
find_strategy(&strategies, SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice);
let high_tip_low_cu =
find_strategy(&strategies, SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice);
assert_eq!(low_tip_high_cu.cu_price, 400_000);
assert_eq!(low_tip_high_cu.tip, 0.002);
assert_eq!(high_tip_low_cu.cu_price, 180_000);
assert_eq!(high_tip_low_cu.tip, 0.005);
}
#[test]
fn default_rpc_strategy_uses_priority_fee_without_tip() {
let strategy = GasFeeStrategy::new();
strategy.set_default_rpc_fee_strategy(100_000, 90_000, 700_000, 800_000);
let buy = find_strategy(
&strategy.get_strategies(TradeType::Buy),
SwqosType::Default,
GasFeeStrategyType::Normal,
);
assert_eq!(buy.cu_limit, 100_000);
assert_eq!(buy.cu_price, 700_000);
assert_eq!(buy.tip, 0.0);
let sell = find_strategy(
&strategy.get_strategies(TradeType::Sell),
SwqosType::Default,
GasFeeStrategyType::Normal,
);
assert_eq!(sell.cu_limit, 90_000);
assert_eq!(sell.cu_price, 800_000);
assert_eq!(sell.tip, 0.0);
}
}
+12 -10
View File
@@ -84,7 +84,7 @@ pub fn print_sdk_timing_block(
start_us: Option<i64>,
build_end_us: Option<i64>,
before_submit_us: Option<i64>,
submit_timings: &[(crate::swqos::SwqosType, i64)],
submit_timings: &[crate::common::SwqosSubmitTiming],
confirm_us: Option<i64>,
) {
println!();
@@ -112,13 +112,14 @@ pub fn print_sdk_timing_block(
}
if let Some(confirm_done_us) = confirm_us {
let total_ms = (confirm_done_us - start_us) as f64 / 1000.0;
for (swqos_type, submit_done_us) in submit_timings {
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
let confirmed_ms = (confirm_done_us - *submit_done_us).max(0) as f64 / 1000.0;
for timing in submit_timings {
let submit_ms = (timing.submit_done_us - start_us).max(0) as f64 / 1000.0;
let confirmed_ms = (confirm_done_us - timing.submit_done_us).max(0) as f64 / 1000.0;
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
swqos_type.as_str(),
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
timing.swqos_type.as_str(),
dir,
timing.strategy_type.as_str(),
submit_ms,
confirmed_ms,
total_ms,
@@ -126,12 +127,13 @@ pub fn print_sdk_timing_block(
);
}
} else {
for (swqos_type, submit_done_us) in submit_timings {
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
for timing in submit_timings {
let submit_ms = (timing.submit_done_us - start_us).max(0) as f64 / 1000.0;
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
swqos_type.as_str(),
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
timing.swqos_type.as_str(),
dir,
timing.strategy_type.as_str(),
submit_ms,
submit_ms,
width = SWQOS_LABEL_WIDTH
+31 -34
View File
@@ -1,5 +1,4 @@
use crate::common::SolanaRpcClient;
use anyhow::anyhow;
use fnv::FnvHasher;
use once_cell::sync::Lazy;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
@@ -50,6 +49,26 @@ async fn fetch_rent_for_token_account(
Ok(client.get_minimum_balance_for_rent_exemption(165).await?)
}
#[inline]
fn derive_seed_from_mint(mint: &Pubkey) -> String {
// Keep the legacy 8-hex seed stable. Changing this derivation changes the token account
// address and can strand balances created by earlier buys.
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
let mut seed = String::with_capacity(8);
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
let byte = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
seed.push(byte as char);
}
seed
}
pub fn create_associated_token_account_use_seed(
payer: &Pubkey,
owner: &Pubkey,
@@ -63,33 +82,23 @@ pub fn create_associated_token_account_use_seed(
let rent = if is_2022_token {
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
} else {
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
DEFAULT_TOKEN_ACCOUNT_RENT
} else {
v
}
v
};
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(mint.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(mint);
// 🔧 修复:使用传入的 token_program 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program,地址自然一致
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
let ata_like = Pubkey::create_with_seed(payer, &seed, token_program)?;
let len = 165;
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
@@ -98,7 +107,7 @@ pub fn create_associated_token_account_use_seed(
payer,
&ata_like,
payer,
seed,
&seed,
rent,
len,
token_program,
@@ -118,21 +127,9 @@ pub fn get_associated_token_address_with_program_id_use_seed(
token_mint_address: &Pubkey,
token_program_id: &Pubkey,
) -> Result<Pubkey, anyhow::Error> {
let mut buf = [0u8; 8];
let mut hasher = FnvHasher::default();
hasher.write(token_mint_address.as_ref());
let hash = hasher.finish();
let v = (hash & 0xFFFF_FFFF) as u32;
for i in 0..8 {
let nibble = ((v >> (28 - i * 4)) & 0xF) as u8;
buf[i] = match nibble {
0..=9 => b'0' + nibble,
_ => b'a' + (nibble - 10),
};
}
let seed = unsafe { std::str::from_utf8_unchecked(&buf) };
let seed = derive_seed_from_mint(token_mint_address);
// 🔧 修复:使用传入的 token_program_id 生成地址(支持 Token 和 Token-2022
// 买入和卖出只要都使用事件中的 token_program_id,地址自然一致
let ata_like = Pubkey::create_with_seed(wallet_address, seed, token_program_id)?;
let ata_like = Pubkey::create_with_seed(wallet_address, &seed, token_program_id)?;
Ok(ata_like)
}
+9 -1
View File
@@ -1,4 +1,5 @@
use crate::swqos::SwqosConfig;
use crate::common::GasFeeStrategyType;
use crate::swqos::{SwqosConfig, SwqosType};
use solana_commitment_config::CommitmentConfig;
use std::hash::{Hash, Hasher};
@@ -75,6 +76,13 @@ impl PartialEq for InfrastructureConfig {
impl Eq for InfrastructureConfig {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SwqosSubmitTiming {
pub swqos_type: SwqosType,
pub strategy_type: GasFeeStrategyType,
pub submit_done_us: i64,
}
#[derive(Debug, Clone)]
pub struct TradeConfig {
pub rpc_url: String,
+40 -10
View File
@@ -30,11 +30,14 @@ use crate::{
},
};
use anyhow::{anyhow, Result};
use solana_sdk::instruction::AccountMeta;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signer::Signer,
};
#[inline]
fn effective_pump_mint_token_program(_mint: &Pubkey, protocol_params: &PumpFunParams) -> Pubkey {
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
let tp = protocol_params.token_program;
if tp == Pubkey::default() {
TOKEN_PROGRAM_2022
@@ -116,7 +119,7 @@ fn build_buy_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(&params.output_mint, protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -270,7 +273,7 @@ fn build_sell_v1(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let token_program = effective_pump_mint_token_program(&params.input_mint, protocol_params);
let token_program = effective_pump_mint_token_program(protocol_params);
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -384,8 +387,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program =
effective_pump_mint_token_program(&params.output_mint, protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -513,7 +515,7 @@ fn build_buy_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
fee_recipient_meta,
AccountMeta::new(associated_quote_fee_recipient, false),
AccountMeta::new_readonly(buyback_fee_recipient, false),
AccountMeta::new(buyback_fee_recipient, false),
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
AccountMeta::new(bonding_curve_addr, false),
AccountMeta::new(associated_base_bonding_curve, false),
@@ -605,7 +607,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
})?;
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
let base_token_program = effective_pump_mint_token_program(&params.input_mint, protocol_params);
let base_token_program = effective_pump_mint_token_program(protocol_params);
let base_token_program_meta = if base_token_program == TOKEN_PROGRAM_2022 {
crate::constants::TOKEN_PROGRAM_2022_META
} else {
@@ -703,7 +705,7 @@ fn build_sell_v2(params: &SwapParams) -> Result<Vec<Instruction>> {
AccountMeta::new_readonly(accounts::ASSOCIATED_TOKEN_PROGRAM, false),
fee_recipient_meta,
AccountMeta::new(associated_quote_fee_recipient, false),
AccountMeta::new_readonly(buyback_fee_recipient, false),
AccountMeta::new(buyback_fee_recipient, false),
AccountMeta::new(associated_quote_buyback_fee_recipient, false),
AccountMeta::new(bonding_curve_addr, false),
AccountMeta::new(associated_base_bonding_curve, false),
@@ -857,10 +859,20 @@ mod tests {
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.len(), 18);
assert_eq!(instructions[2].data.len(), 25);
}
#[test]
fn non_pump_buy_respects_explicit_legacy_token_program() {
crate::common::seed::set_default_rents();
let params = swap_params_for_buy(Pubkey::new_unique(), TOKEN_PROGRAM);
let instructions = build_buy_v1(&params).unwrap();
assert_eq!(instructions[2].accounts[8].pubkey, TOKEN_PROGRAM);
}
#[test]
fn pumpfun_v1_fixed_output_uses_buy_with_max_input_budget() {
let mut params = swap_params_for_buy(pump_mint(), TOKEN_PROGRAM);
@@ -925,6 +937,24 @@ mod tests {
}
}
#[test]
fn pumpfun_v2_buyback_fee_recipient_is_writable() {
let mint = pump_mint();
let mut params = swap_params_for_buy(mint, TOKEN_PROGRAM);
params.create_output_mint_ata = false;
let buy_ix = build_buy_v2(&params).unwrap().pop().unwrap();
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&buy_ix.accounts[8].pubkey));
assert!(buy_ix.accounts[8].is_writable);
params.trade_type = crate::swqos::TradeType::Sell;
params.input_mint = mint;
params.output_mint = crate::constants::SOL_TOKEN_ACCOUNT;
let sell_ix = build_sell_v2(&params).unwrap().pop().unwrap();
assert!(global_constants::BUYBACK_FEE_RECIPIENTS.contains(&sell_ix.accounts[8].pubkey));
assert!(sell_ix.accounts[8].is_writable);
}
#[test]
fn pumpfun_v1_sell_fixed_output_uses_min_sol_directly() {
let mint = pump_mint();
+29 -19
View File
@@ -212,7 +212,7 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
#[inline]
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
*pubkey == global_constants::FEE_RECIPIENT
}
#[inline]
@@ -242,11 +242,12 @@ pub fn reconcile_mayhem_mode_for_trade(
#[inline]
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
let is_m = is_mayhem_fee_recipient(pk);
let is_amm = is_amm_fee_recipient(pk);
let is_s = is_standard_bonding_fee_recipient(pk);
if is_mayhem_mode {
!(is_s && !is_m)
is_m || (!is_s && !is_amm && *pk != Pubkey::default())
} else {
!(is_m && !is_s)
is_s || (!is_m && !is_amm && *pk != Pubkey::default())
}
}
@@ -260,18 +261,10 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
#[inline]
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
const POOL: &[Pubkey] = &[
global_constants::FEE_RECIPIENT,
global_constants::PUMPFUN_AMM_FEE_1,
global_constants::PUMPFUN_AMM_FEE_2,
global_constants::PUMPFUN_AMM_FEE_3,
global_constants::PUMPFUN_AMM_FEE_4,
global_constants::PUMPFUN_AMM_FEE_5,
global_constants::PUMPFUN_AMM_FEE_6,
global_constants::PUMPFUN_AMM_FEE_7,
];
let recipient = *POOL.choose(&mut rand::rng()).unwrap_or(&global_constants::FEE_RECIPIENT);
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
// Historical name kept for API compatibility. Do not randomize across static AMM fee
// recipients for bonding-curve buy/sell; stale AMM protocol fee accounts can fail
// Pump.fun authorization with error 6000 when Global has rotated.
AccountMeta { pubkey: global_constants::FEE_RECIPIENT, is_signer: false, is_writable: true }
}
#[inline]
@@ -707,12 +700,12 @@ mod tests {
#[test]
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
let fee = global_constants::FEE_RECIPIENT;
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
}
#[test]
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
fn pump_fee_meta_rejects_amm_fee_when_building_mayhem_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_4;
let m = pump_fun_fee_recipient_meta(fee, true);
assert!(
@@ -723,11 +716,28 @@ mod tests {
}
#[test]
fn pump_fee_meta_uses_observed_standard_fee_for_standard_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_7;
fn pump_fee_meta_uses_observed_non_amm_fee_for_standard_ix() {
let fee = Pubkey::new_unique();
let m = pump_fun_fee_recipient_meta(fee, false);
assert_eq!(m.pubkey, fee);
assert!(m.is_writable);
assert!(!m.is_signer);
}
#[test]
fn pump_fee_meta_rejects_amm_fee_for_standard_ix() {
let fee = global_constants::PUMPFUN_AMM_FEE_7;
let m = pump_fun_fee_recipient_meta(fee, false);
assert_eq!(m.pubkey, global_constants::FEE_RECIPIENT);
assert!(m.is_writable);
assert!(!m.is_signer);
}
#[test]
fn pump_fee_meta_default_standard_uses_main_fee_recipient() {
let m = pump_fun_fee_recipient_meta(Pubkey::default(), false);
assert_eq!(m.pubkey, global_constants::FEE_RECIPIENT);
assert!(m.is_writable);
assert!(!m.is_signer);
}
}
+21 -1
View File
@@ -3,7 +3,9 @@ use once_cell::sync::Lazy;
use smallvec::SmallVec;
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction;
use std::sync::Arc;
use std::{hash::Hash, sync::Arc};
const MAX_COMPUTE_BUDGET_CACHE_SIZE: usize = 4_096;
/// Cache key containing all parameters for compute budget instructions
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -17,6 +19,22 @@ struct ComputeBudgetCacheKey {
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, Arc<SmallVec<[Instruction; 2]>>>> =
Lazy::new(|| DashMap::new());
#[inline]
fn prune_cache<K, V>(cache: &DashMap<K, V>, max_size: usize)
where
K: Eq + Hash + Clone,
{
let len = cache.len();
if len <= max_size {
return;
}
let remove_count = (len - max_size).max(max_size / 16).min(len);
let keys: Vec<K> = cache.iter().take(remove_count).map(|entry| entry.key().clone()).collect();
for key in keys {
cache.remove(&key);
}
}
/// Extend `instructions` with compute budget instructions; on cache hit extends from cached Arc (no SmallVec clone).
#[inline(always)]
pub fn extend_compute_budget_instructions(
@@ -41,6 +59,7 @@ pub fn extend_compute_budget_instructions(
let arc = Arc::new(insts);
instructions.extend(arc.iter().cloned());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
}
/// Returns compute budget instructions (allocates on cache hit; prefer `extend_compute_budget_instructions` on hot path).
@@ -59,5 +78,6 @@ pub fn compute_budget_instructions(unit_price: u64, unit_limit: u32) -> SmallVec
}
let arc = Arc::new(insts.clone());
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
prune_cache(&COMPUTE_BUDGET_CACHE, MAX_COMPUTE_BUDGET_CACHE_SIZE);
insts
}
+6 -5
View File
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use solana_hash::Hash;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{
@@ -93,19 +94,19 @@ fn build_versioned_transaction(
// 使用预分配的交易构建器以降低延迟
let mut builder = acquire_builder();
let versioned_msg = builder.build_zero_alloc(
let build_result = builder.build_zero_alloc(
&payer.pubkey(),
&full_instructions,
address_lookup_table_account,
blockhash,
);
release_builder(builder);
let versioned_msg = build_result?;
let msg_bytes = versioned_msg.serialize();
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
let signature =
payer.as_ref().try_sign_message(&msg_bytes).map_err(|e| anyhow!("sign failed: {e}"))?;
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
// 归还构建器到池
release_builder(builder);
Ok(tx)
}
+306 -77
View File
@@ -35,8 +35,8 @@ use fnv::FnvHasher;
type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
use crate::{
common::nonce_cache::DurableNonceInfo,
common::GasFeeStrategy,
common::gas_fee_strategy::{GasFeeStrategyType, GasFeeStrategyValue},
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SwqosSubmitTiming},
swqos::{SwqosClient, SwqosType, TradeType},
trading::core::params::SenderConcurrencyConfig,
trading::{common::build_transaction, MiddlewareManager},
@@ -71,6 +71,7 @@ struct SwqosJob {
tip_account: Arc<Pubkey>,
swqos_client: Arc<SwqosClient>,
swqos_type: SwqosType,
strategy_type: GasFeeStrategyType,
core_id: Option<core_affinity::CoreId>,
use_affinity: bool,
}
@@ -107,6 +108,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
signature: Signature::default(),
error: Some(e),
swqos_type: job.swqos_type,
strategy_type: job.strategy_type,
landed_on_chain: false,
submit_done_us: crate::common::clock::now_micros(),
});
@@ -136,6 +138,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
signature: sig,
error: err,
swqos_type: job.swqos_type,
strategy_type: job.strategy_type,
landed_on_chain,
submit_done_us: crate::common::clock::now_micros(),
});
@@ -153,33 +156,31 @@ async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>, notify: Arc<Notify>
static SWQOS_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
static SWQOS_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
static SWQOS_WORKERS_STARTED: AtomicBool = AtomicBool::new(false);
static SWQOS_WORKER_COUNT: AtomicUsize = AtomicUsize::new(0);
/// Dedicated OS-thread sender pool. Queue and notify are in OnceCell so hot path never takes a lock after init.
static DEDICATED_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
static DEDICATED_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
static DEDICATED_WORKER_COUNT: AtomicUsize = AtomicUsize::new(0);
/// JoinHandles kept so dedicated threads are not detached; only touched during init under lock.
static DEDICATED_INIT: Mutex<Option<Vec<std::thread::JoinHandle<()>>>> = Mutex::new(None);
fn ensure_dedicated_pool(
fn desired_dedicated_workers(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
return (q.clone(), n.clone());
}
let mut guard = DEDICATED_INIT.lock();
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
return (q.clone(), n.clone());
}
let n = sender_thread_cores
) -> usize {
sender_thread_cores
.map(|v| v.len().min(max_sender_concurrency))
.unwrap_or_else(|| SWQOS_DEDICATED_DEFAULT_THREADS.min(max_sender_concurrency))
.min(32)
.max(1);
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
let notify = Arc::new(Notify::new());
let core_ids: Vec<core_affinity::CoreId> = core_affinity::get_core_ids()
.max(1)
}
fn dedicated_core_ids(
sender_thread_cores: Option<&[usize]>,
n: usize,
) -> Vec<core_affinity::CoreId> {
core_affinity::get_core_ids()
.map(|all_ids| {
sender_thread_cores
.map(|indices| {
@@ -187,9 +188,83 @@ fn ensure_dedicated_pool(
})
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
})
.unwrap_or_default();
let mut handles = Vec::with_capacity(n);
for i in 0..n {
.unwrap_or_default()
}
fn ensure_dedicated_pool(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
let target_workers = desired_dedicated_workers(sender_thread_cores, max_sender_concurrency);
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) >= target_workers {
return (q.clone(), n.clone());
}
ensure_dedicated_worker_count(q.clone(), n.clone(), sender_thread_cores, target_workers);
return (q.clone(), n.clone());
}
let mut guard = DEDICATED_INIT.lock();
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) < target_workers {
ensure_dedicated_worker_count_locked(
q.clone(),
n.clone(),
sender_thread_cores,
target_workers,
&mut guard,
);
}
return (q.clone(), n.clone());
}
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
let notify = Arc::new(Notify::new());
let _ = DEDICATED_QUEUE.set(queue.clone());
let _ = DEDICATED_NOTIFY.set(notify.clone());
*guard = Some(Vec::with_capacity(target_workers));
ensure_dedicated_worker_count_locked(
queue.clone(),
notify.clone(),
sender_thread_cores,
target_workers,
&mut guard,
);
(queue, notify)
}
fn ensure_dedicated_worker_count(
queue: Arc<ArrayQueue<SwqosJob>>,
notify: Arc<Notify>,
sender_thread_cores: Option<&[usize]>,
target_workers: usize,
) {
if DEDICATED_WORKER_COUNT.load(Ordering::Acquire) >= target_workers {
return;
}
let mut guard = DEDICATED_INIT.lock();
ensure_dedicated_worker_count_locked(
queue,
notify,
sender_thread_cores,
target_workers,
&mut guard,
);
}
fn ensure_dedicated_worker_count_locked(
queue: Arc<ArrayQueue<SwqosJob>>,
notify: Arc<Notify>,
sender_thread_cores: Option<&[usize]>,
target_workers: usize,
guard: &mut Option<Vec<std::thread::JoinHandle<()>>>,
) {
let current = DEDICATED_WORKER_COUNT.load(Ordering::Acquire);
if current >= target_workers {
return;
}
let core_ids = dedicated_core_ids(sender_thread_cores, target_workers);
let handles = guard.get_or_insert_with(Vec::new);
handles.reserve(target_workers.saturating_sub(current));
for i in current..target_workers {
let queue = queue.clone();
let notify = notify.clone();
let core_id = core_ids.get(i).cloned();
@@ -205,19 +280,23 @@ fn ensure_dedicated_pool(
});
handles.push(handle);
}
let _ = DEDICATED_QUEUE.set(queue.clone());
let _ = DEDICATED_NOTIFY.set(notify.clone());
*guard = Some(handles);
(queue, notify)
DEDICATED_WORKER_COUNT.store(target_workers, Ordering::Release);
}
fn ensure_swqos_pool(queue: Arc<ArrayQueue<SwqosJob>>, max_sender_concurrency: usize) {
if SWQOS_WORKERS_STARTED.swap(true, Ordering::AcqRel) {
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
let mut current = SWQOS_WORKER_COUNT.load(Ordering::Acquire);
while current < n {
match SWQOS_WORKER_COUNT.compare_exchange(current, n, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => break,
Err(actual) => current = actual,
}
}
if current >= n {
return;
}
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
let notify = SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone();
for _ in 0..n {
for _ in current..n {
tokio::spawn(swqos_worker_loop(queue.clone(), notify.clone()));
}
}
@@ -228,6 +307,7 @@ struct TaskResult {
signature: Signature,
error: Option<anyhow::Error>,
swqos_type: SwqosType,
strategy_type: GasFeeStrategyType,
landed_on_chain: bool,
/// Microsecond timestamp when this task finished (SWQOS returned); for per-SWQOS event→submit timing.
submit_done_us: i64,
@@ -295,7 +375,7 @@ impl ResultCollector {
async fn wait_for_success(
&self,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let timeout = std::time::Duration::from_secs(5);
let poll_interval = std::time::Duration::from_millis(1000);
@@ -307,7 +387,7 @@ impl ResultCollector {
let mut submit_timings = Vec::new();
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
if result.success {
has_success = true;
}
@@ -325,7 +405,7 @@ impl ResultCollector {
let mut submit_timings = Vec::new();
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
// Prefer the error from the tx that actually landed
if result.landed_on_chain && result.error.is_some() {
landed_error = result.error;
@@ -344,7 +424,7 @@ impl ResultCollector {
let mut submit_timings = Vec::new();
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
if result.success {
any_success = true;
}
@@ -367,7 +447,7 @@ impl ResultCollector {
fn get_first(
&self,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let mut signatures = Vec::new();
let mut has_success = false;
let mut last_error = None;
@@ -375,7 +455,7 @@ impl ResultCollector {
while let Some(result) = self.results.pop() {
signatures.push(result.signature);
submit_timings.push((result.swqos_type, result.submit_done_us));
submit_timings.push(result.submit_timing());
if result.success {
has_success = true;
}
@@ -391,12 +471,35 @@ 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
/// block post-buy monitoring / sell scheduling.
async fn wait_for_first_submitted(
&self,
timeout: Duration,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let poll_interval = Duration::from_millis(1);
loop {
if self.success_flag.load(Ordering::Acquire)
|| self.landed_failed_flag.load(Ordering::Acquire)
|| self.completed_count.load(Ordering::Acquire) >= self.total_tasks
|| start.elapsed() >= timeout
{
return self.get_first();
}
tokio::time::sleep(poll_interval).await;
}
}
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
#[allow(dead_code)]
async fn wait_for_all_submitted(
&self,
timeout_secs: u64,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
let start = Instant::now();
let primary = Duration::from_secs(timeout_secs);
let poll_interval = Duration::from_millis(2);
@@ -410,8 +513,7 @@ impl ResultCollector {
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
if all_submitted {
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
tokio::time::sleep(Duration::from_millis(35)).await;
tokio::task::yield_now().await;
} else {
tokio::time::sleep(Duration::from_millis(600)).await;
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
@@ -426,6 +528,63 @@ impl ResultCollector {
}
}
type GasFeeConfig = (SwqosType, GasFeeStrategyType, GasFeeStrategyValue);
#[derive(Debug, Clone, Copy)]
struct SwqosTaskConfig {
task_ordinal: usize,
swqos_index: usize,
gas_fee_config: GasFeeConfig,
}
impl TaskResult {
#[inline]
fn submit_timing(&self) -> SwqosSubmitTiming {
SwqosSubmitTiming {
swqos_type: self.swqos_type,
strategy_type: self.strategy_type,
submit_done_us: self.submit_done_us,
}
}
}
fn select_swqos_task_configs(
swqos_types: &[SwqosType],
gas_fee_configs: &[GasFeeConfig],
with_tip: bool,
check_min_tip: bool,
min_tip_by_swqos: impl Fn(SwqosType) -> f64,
) -> Vec<SwqosTaskConfig> {
let mut task_configs = Vec::with_capacity(swqos_types.len() * 3);
for (i, swqos_type) in swqos_types.iter().copied().enumerate() {
if !with_tip && !matches!(swqos_type, SwqosType::Default) {
continue;
}
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
let min_tip = if check_tip { min_tip_by_swqos(swqos_type) } else { 0.0 };
for config in gas_fee_configs {
if config.0 != swqos_type {
continue;
}
if check_tip && config.2.tip < min_tip {
if crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
config.0, config.2.tip, min_tip
);
}
continue;
}
task_configs.push(SwqosTaskConfig {
task_ordinal: task_configs.len(),
swqos_index: i,
gas_fee_config: *config,
});
}
}
task_configs
}
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
///
/// `sender_config` merges sender_thread_cores, effective_core_ids, max_sender_concurrency (precomputed at SDK init; no get_core_ids on hot path).
@@ -445,7 +604,7 @@ pub async fn execute_parallel(
use_dedicated_sender_threads: bool,
sender_config: SenderConcurrencyConfig,
check_min_tip: bool,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
if swqos_clients.is_empty() {
return Err(anyhow!("swqos_clients is empty"));
}
@@ -464,47 +623,33 @@ pub async fn execute_parallel(
// One get_strategies call per batch (avoid N calls in loop).
let gas_fee_configs =
gas_fee_strategy.get_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
for (i, swqos_client) in swqos_clients.iter().enumerate() {
let swqos_type = swqos_client.get_swqos_type();
if !with_tip && !matches!(swqos_type, SwqosType::Default) {
continue;
}
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
let min_tip = if check_tip { swqos_client.min_tip_sol() } else { 0.0 };
for config in &gas_fee_configs {
if config.0 != swqos_type {
continue;
}
if check_tip {
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
config.0, config.2.tip, min_tip
);
}
if config.2.tip < min_tip {
continue;
}
}
task_configs.push((i, swqos_client.clone(), *config));
}
}
let swqos_types: Vec<SwqosType> =
swqos_clients.iter().map(|swqos| swqos.get_swqos_type()).collect();
let selected_task_configs = select_swqos_task_configs(
&swqos_types,
&gas_fee_configs,
with_tip,
check_min_tip,
|swqos_type| {
swqos_clients
.iter()
.find(|swqos| swqos.get_swqos_type() == swqos_type)
.map(|swqos| swqos.min_tip_sol())
.unwrap_or(0.0)
},
);
if task_configs.is_empty() {
if selected_task_configs.is_empty() {
return Err(anyhow!("No available gas fee strategy configs"));
}
if is_buy && task_configs.len() > 1 && durable_nonce.is_none() {
if is_buy && selected_task_configs.len() > 1 && durable_nonce.is_none() {
return Err(anyhow!("Multiple swqos transactions require durable_nonce to be set.",));
}
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
let channel_count = task_configs.len().max(1);
let channel_count = selected_task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
let submit_timeout_secs: u64 =
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
@@ -534,10 +679,15 @@ pub async fn execute_parallel(
let effective_core_ids = sender_config.effective_core_ids.as_slice();
let core_len = effective_core_ids.len().max(1);
let mut tip_cache: FnvHashMap<*const (), Arc<Pubkey>> =
FnvHashMap::with_capacity_and_hasher(task_configs.len(), BuildHasherDefault::default());
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
let core_id = effective_core_ids.get(i % core_len).copied();
FnvHashMap::with_capacity_and_hasher(
selected_task_configs.len(),
BuildHasherDefault::default(),
);
for task_config in selected_task_configs {
let swqos_client = swqos_clients[task_config.swqos_index].clone();
let core_id = effective_core_ids.get(task_config.task_ordinal % core_len).copied();
let swqos_type = swqos_client.get_swqos_type();
let gas_fee_strategy_config = task_config.gas_fee_config;
let key = Arc::as_ptr(&swqos_client) as *const ();
let tip_account = match tip_cache.get(&key) {
Some(t) => t.clone(),
@@ -561,10 +711,21 @@ pub async fn execute_parallel(
tip_account,
swqos_client,
swqos_type,
strategy_type: gas_fee_strategy_config.1,
core_id,
use_affinity: !effective_core_ids.is_empty(),
};
let _ = queue.push(job);
if let Err(job) = queue.push(job) {
shared.collector.submit(TaskResult {
success: false,
signature: Signature::default(),
error: Some(anyhow!("SWQOS sender queue is full")),
swqos_type: job.swqos_type,
strategy_type: job.strategy_type,
landed_on_chain: false,
submit_done_us: crate::common::clock::now_micros(),
});
}
}
}
@@ -573,12 +734,10 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
let ret = collector.wait_for_first_submitted(Duration::from_millis(500)).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
Some(anyhow!("No SWQOS result within fast submit window")),
vec![],
));
let (success, signatures, last_error, submit_timings) = ret;
@@ -592,3 +751,73 @@ pub async fn execute_parallel(
Err(anyhow!("All transactions failed"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn value(cu_price: u64, tip: f64) -> GasFeeStrategyValue {
GasFeeStrategyValue { cu_limit: 100_000, cu_price, tip }
}
#[test]
fn select_task_configs_keeps_two_fee_lanes_per_swqos() {
let swqos_types = [SwqosType::Jito, SwqosType::Helius];
let configs = [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
(SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
(SwqosType::Helius, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
];
let selected = select_swqos_task_configs(&swqos_types, &configs, true, false, |_| 0.0);
assert_eq!(selected.len(), 4);
assert_eq!(
selected.iter().filter(|task| task.gas_fee_config.0 == SwqosType::Jito).count(),
2
);
assert_eq!(
selected.iter().filter(|task| task.gas_fee_config.0 == SwqosType::Helius).count(),
2
);
assert_eq!(
selected.iter().map(|task| task.task_ordinal).collect::<Vec<_>>(),
vec![0, 1, 2, 3]
);
assert_eq!(
selected.iter().map(|task| task.swqos_index).collect::<Vec<_>>(),
vec![0, 0, 1, 1]
);
}
#[test]
fn select_task_configs_applies_min_tip_per_lane() {
let swqos_types = [SwqosType::Jito];
let configs = [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.0001)),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, value(180_000, 0.005)),
];
let selected = select_swqos_task_configs(&swqos_types, &configs, true, true, |_| 0.001);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].gas_fee_config.1, GasFeeStrategyType::HighTipLowCuPrice);
}
#[test]
fn select_task_configs_without_tip_keeps_default_priority_fee_only() {
let swqos_types = [SwqosType::Jito, SwqosType::Default];
let configs = [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, value(400_000, 0.002)),
(SwqosType::Default, GasFeeStrategyType::Normal, value(700_000, 0.0)),
];
let selected = select_swqos_task_configs(&swqos_types, &configs, false, false, |_| 0.0);
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].gas_fee_config.0, SwqosType::Default);
assert_eq!(selected[0].gas_fee_config.2.cu_price, 700_000);
assert_eq!(selected[0].gas_fee_config.2.tip, 0.0);
}
}
+19 -14
View File
@@ -12,10 +12,9 @@ use std::{
use tracing::{info, trace, warn};
use super::{params::SwapParams, traits::InstructionBuilder};
use crate::swqos::SwqosType;
use crate::swqos::TradeType;
use crate::{
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient, SwqosSubmitTiming},
perf::syscall_bypass::SystemCallBypassManager,
swqos::common::poll_any_transaction_confirmation,
trading::core::{
@@ -56,7 +55,7 @@ impl TradeExecutor for GenericTradeExecutor {
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
let timing_start_us: Option<i64> = if params.log_enabled {
@@ -187,7 +186,7 @@ impl TradeExecutor for GenericTradeExecutor {
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e)), vec![]),
};
// submit_timings 为完成先后顺序(先完成的先 push),打印不排序、不增加延迟
let submit_timings_ref: &[(crate::swqos::SwqosType, i64)] = submit_timings.as_slice();
let submit_timings_ref: &[SwqosSubmitTiming] = submit_timings.as_slice();
let result = if need_confirm {
let confirm_result = if let Some(rpc) = params.rpc.as_ref() {
@@ -257,7 +256,7 @@ async fn simulate_transaction(
is_buy: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)> {
use crate::trading::common::build_transaction;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_commitment_config::CommitmentLevel;
@@ -351,6 +350,7 @@ async fn simulate_transaction(
#[cfg(test)]
mod tests {
use crate::common::GasFeeStrategyType;
use crate::swqos::SwqosType;
/// 运行 `cargo test -p sol-trade-sdk log_timing_preview -- --nocapture` 查看日志打印效果
@@ -377,15 +377,17 @@ mod tests {
);
println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
(SwqosType::Jito, 45.12, 83.38, 128.50),
(SwqosType::Helius, 52.30, 76.20, 128.50),
(SwqosType::ZeroSlot, 48.90, 79.60, 128.50),
for (swqos_type, strategy_type, submit_ms, confirmed_ms, total_ms) in [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 45.12, 83.38, 128.50),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 46.08, 82.42, 128.50),
(SwqosType::Helius, GasFeeStrategyType::LowTipHighCuPrice, 52.30, 76.20, 128.50),
(SwqosType::ZeroSlot, GasFeeStrategyType::Normal, 48.90, 79.60, 128.50),
] {
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
swqos_type.as_str(),
dir,
strategy_type.as_str(),
submit_ms,
confirmed_ms,
total_ms,
@@ -396,13 +398,16 @@ mod tests {
println!(
"\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n"
);
for (swqos_type, submit_ms, total_ms) in
[(SwqosType::Jito, 44.20, 44.20), (SwqosType::Helius, 51.80, 51.80)]
{
for (swqos_type, strategy_type, submit_ms, total_ms) in [
(SwqosType::Jito, GasFeeStrategyType::LowTipHighCuPrice, 44.20, 44.20),
(SwqosType::Jito, GasFeeStrategyType::HighTipLowCuPrice, 45.10, 45.10),
(SwqosType::Helius, GasFeeStrategyType::Normal, 51.80, 51.80),
] {
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
" [SDK][{:width$}] {} {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
swqos_type.as_str(),
dir,
strategy_type.as_str(),
submit_ms,
total_ms,
width = w
+3 -2
View File
@@ -32,12 +32,13 @@ pub struct PumpFunParams {
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
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 可覆盖该默认值
/// **`Pubkey::default()`**ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致)。
/// 显式传入 Legacy 或 Token-2022 id 时严格按该值组装,不再用 mint 字符串后缀猜测。
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()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)
/// `Pubkey::default()` 时只能使用 SDK 静态 fallback,可能落后于主网 Global;交易热路径应优先传入 gRPC / parser 观测值
pub fee_recipient: Pubkey,
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
+2 -2
View File
@@ -1,4 +1,4 @@
use crate::swqos::SwqosType;
use crate::common::SwqosSubmitTiming;
use crate::trading::SwapParams;
use anyhow::Result;
use solana_sdk::{instruction::Instruction, signature::Signature};
@@ -12,7 +12,7 @@ pub trait TradeExecutor: Send + Sync {
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)>;
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<SwqosSubmitTiming>)>;
/// 获取协议名称
fn protocol_name(&self) -> &'static str;
}
+5 -5
View File
@@ -17,6 +17,7 @@ const PARALLEL_SENDER_COUNT: usize = 18;
/// 启动时预填充数量,必须 >= PARALLEL_SENDER_COUNT,否则 18 路并发 build 会触发分配或争抢
const TX_BUILDER_POOL_PREFILL: usize = 64;
use anyhow::Result;
use crossbeam_queue::ArrayQueue;
use once_cell::sync::Lazy;
use solana_message::AddressLookupTableAccount;
@@ -82,7 +83,7 @@ impl PreallocatedTxBuilder {
instructions: &[Instruction],
address_lookup_table_account: Option<&AddressLookupTableAccount>,
recent_blockhash: Hash,
) -> VersionedMessage {
) -> Result<VersionedMessage> {
self.reset();
self.instructions.extend_from_slice(instructions);
@@ -92,14 +93,13 @@ impl PreallocatedTxBuilder {
&self.instructions,
std::slice::from_ref(alt),
recent_blockhash,
)
.expect("v0 message compile failed");
VersionedMessage::V0(message)
)?;
Ok(VersionedMessage::V0(message))
} else {
// ✅ 没有查找表,使用 Legacy 消息(兼容所有 RPC
let message =
Message::new_with_blockhash(&self.instructions, Some(payer), &recent_blockhash);
VersionedMessage::Legacy(message)
Ok(VersionedMessage::Legacy(message))
}
}
}
+12 -29
View File
@@ -1,22 +1,10 @@
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
use solana_sdk::pubkey::Pubkey;
fn sol_to_lamports(sol: f64) -> u64 {
(sol * 1_000_000_000.0) as u64
}
use crate::{
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
utils::calc::common::compute_fee,
};
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
#[inline]
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
}
/// Calculates the amount of tokens that can be purchased with a given SOL amount
/// using the bonding curve formula.
///
@@ -54,26 +42,21 @@ pub fn get_buy_token_amount_from_sol_amount(
let input_amount = amount_128
.checked_mul(10_000)
.unwrap()
.checked_div(total_fee_basis_points_128 + 10_000)
.unwrap();
.and_then(|v| v.checked_div(total_fee_basis_points_128 + 10_000))
.unwrap_or(0);
let denominator = virtual_sol_reserves + input_amount;
let mut tokens_received =
input_amount.checked_mul(virtual_token_reserves).unwrap().checked_div(denominator).unwrap();
tokens_received = tokens_received.min(real_token_reserves);
if tokens_received <= 100 * 1_000_000_u128 {
tokens_received = if amount > sol_str_to_lamports("0.01").unwrap_or(0) {
25547619 * 1_000_000_u128
} else {
255476 * 1_000_000_u128
};
let Some(denominator) = virtual_sol_reserves.checked_add(input_amount) else { return 0 };
if denominator == 0 {
return 0;
}
tokens_received as u64
let tokens_received = input_amount
.checked_mul(virtual_token_reserves)
.and_then(|v| v.checked_div(denominator))
.unwrap_or(0)
.min(real_token_reserves);
tokens_received.min(u64::MAX as u128) as u64
}
/// Calculates the amount of SOL that will be received when selling a given token amount