Compare commits

...

14 Commits

Author SHA1 Message Date
Wood 48b9b17ab1 chore: release v3.5.5
Made-with: Cursor
2026-02-27 15:07:18 +08:00
Wood 0cd276d6cb feat: Helius Sender SWQOS support and global check_min_tip option
- Add Helius Sender client (helius.rs) with dual routing and swqos_only mode
- Helius min tip: 0.0002 SOL default, 0.000005 SOL when swqos_only=true
- Add TradeConfig.check_min_tip (default false) to skip min-tip validation for lower latency
- Thread check_min_tip through SwapParams and execute_parallel; only call min_tip_sol when enabled
- Add SWQOS_ENDPOINTS_HELIUS and HELIUS_TIP_ACCOUNTS in constants
- Extend SwqosConfig/SwqosType for Helius; add Helius to get_endpoint and get_swqos_client
- Update trading_client, shared_infrastructure and middleware_system examples

Made-with: Cursor
2026-02-27 15:00:13 +08:00
Wood 82438479d3 chore: fix all examples to compile and expand workspace
- Add pumpfun_copy_trading and pumpfun_sniper_trading to workspace members
- Add grpc_recv_us field to all TradeBuyParams/TradeSellParams in examples
- Switch address_lookup and nonce_cache to sol-parser-sdk, use event is_cashback_coin
- Remove invalid rustls call from pumpfun examples for successful build
- All 18 examples pass cargo check --workspace

Made-with: Cursor
2026-02-27 13:44:22 +08:00
Wood 3d062279d9 chore: release v3.5.4
- Bump version to 3.5.4; update README and README_CN
- Astralane: irisb binary API (no Base64), POST /irisb with query api-key&method; constants use /irisb endpoints; ping POST getHealth
- BlockRazor: Send Transaction v2 (plain Base64 body, Content-Type text/plain, auth in URI only); ping POST /v2/health; use default_http_client_builder
- SWQOS: all clients use default_http_client_builder (nextblock, temporal, zeroslot, astralane, lightspeed, jito, flashblock); remove unused Duration imports

Made-with: Cursor
2026-02-27 02:30:52 +08:00
Wood 15e07e9130 docs: remove version release notes from README_CN, fix crates.io version to 3.5.3
Made-with: Cursor
2026-02-27 00:42:43 +08:00
Wood 6eaafde4fd docs: remove version release notes from README
Made-with: Cursor
2026-02-27 00:42:17 +08:00
Wood 0f37950adf chore: release v3.5.3 (full release with IDL + instructions)
- Bump version to 3.5.3; README What's new in 3.5.3 includes SWQoS + PumpFun/PumpSwap updates
- Replaces incomplete v3.5.2 release

Made-with: Cursor
2026-02-27 00:29:20 +08:00
Wood dd4f42324e feat: IDL updates, PumpFun/PumpSwap instruction and utils, fast_fn, release notes
- idl: pump.json and pump_amm.json updates
- instruction: pumpfun.rs, pumpswap.rs and utils (pumpfun, pumpswap)
- common: fast_fn.rs
- docs: release_notes_v3.5.0.md

Made-with: Cursor
2026-02-27 00:27:36 +08:00
Wood d0897b53d3 chore: release v3.5.2
- Bump version to 3.5.2 in Cargo.toml
- Update README and README_CN with 3.5.2 and What's new in 3.5.2

Made-with: Cursor
2026-02-27 00:24:10 +08:00
Wood e67a21df58 chore: bump README dependency examples to 3.5.1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-26 01:03:34 +08:00
Wood 147c6068d1 Release v3.5.1: bump version, update README
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-26 00:58:48 +08:00
Wood 6ce8ee582e Merge pull request #79 from HelvetiCrypt/fix/confirm-all-signatures
Fix: poll all channel signatures during confirmation
2026-02-26 00:52:02 +08:00
vibes bcc6860bc3 Exclude examples requiring sol-parser-sdk from workspace
These examples depend on a local path to sol-parser-sdk which is not
available in the public repo, causing build failures for consumers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:19:03 +00:00
vibes 1b7e3781c7 Fix confirmation polling to check all channel signatures
In v3.5.0, confirmation was split out of execute_parallel into
poll_transaction_confirmation, but it only polled sig[0]. When
multi-channel submit is used, each channel produces a different
signature and only one lands on-chain. If the landed sig is not
sig[0], the poll times out after 15s and reports failure despite
a successful on-chain transaction.

Add poll_any_transaction_confirmation() that passes all signatures
to get_signature_statuses in a single RPC call per poll, returning
the first one that confirms. The executor now uses this instead of
polling a single signature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 09:16:50 +00:00
48 changed files with 914 additions and 531 deletions
+16 -16
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "3.5.0"
version = "3.5.5"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -19,9 +19,9 @@ members = [
"examples/trading_client",
"examples/shared_infrastructure",
"examples/middleware_system",
"examples/pumpswap_trading",
"examples/pumpfun_copy_trading",
"examples/pumpfun_sniper_trading",
"examples/pumpswap_trading",
"examples/bonk_sniper_trading",
"examples/bonk_copy_trading",
"examples/raydium_cpmm_trading",
@@ -45,23 +45,23 @@ perf-trace = [] # 性能追踪特性,生产环境应禁用以获得最佳性
[dependencies]
solana-sdk = "3.0.0"
solana-client = "3.0.8"
solana-client = "3.1.9"
solana-program = "3.0.0"
solana-rpc-client = "3.0.8"
solana-rpc-client-api = "3.0.8"
solana-transaction-status = "3.0.8"
solana-account-decoder = "3.0.8"
solana-rpc-client = "3.1.9"
solana-rpc-client-api = "3.1.9"
solana-transaction-status = "3.1.9"
solana-account-decoder = "3.1.9"
solana-hash = "3.0.0"
solana-entry = "3.0.8"
solana-rpc-client-nonce-utils = "3.0.8"
solana-perf = "3.0.8"
solana-metrics = "3.0.8"
solana-nonce = "3.0.0"
solana-address-lookup-table-interface = "3.0.0"
solana-entry = "3.1.9"
solana-rpc-client-nonce-utils = "3.1.9"
solana-perf = "3.1.9"
solana-metrics = "3.1.9"
solana-nonce = "3.1.0"
solana-address-lookup-table-interface = "3.0.1"
solana-compute-budget-interface = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
solana-transaction-status-client-types = "3.0.0"
solana-tls-utils = "3.0.8"
solana-commitment-config = { version = "3.1.1", features = ["serde"] }
solana-transaction-status-client-types = "3.1.9"
solana-tls-utils = "3.1.9"
borsh = { version = "1.5.3", features = ["derive"] }
isahc = "1.7.2"
+2 -10
View File
@@ -60,14 +60,6 @@
---
## 🆕 What's new in 3.5.0
- **Performance**: Hot-path timing only when logging; reduced clones (`execute_parallel` takes `&[Arc<SwqosClient>]`); shared HTTP client constants for SWQoS.
- **Code quality**: Extracted `validate_protocol_params` for buy/sell; constants for instruction/account sizes and HTTP timeouts; prefetch and branch-hint comments.
- **Documentation**: Bilingual (English + 中文) doc comments across execution, executor, perf, and swqos modules.
---
## ✨ Features
1. **PumpFun Trading**: Support for `buy` and `sell` operations
@@ -97,14 +89,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.0" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.5" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "3.5.0"
sol-trade-sdk = "3.5.5"
```
## 🛠️ Usage Examples
+2 -10
View File
@@ -60,14 +60,6 @@
---
## 🆕 3.5.0 更新说明
- **性能**:仅在打日志时做热路径计时;减少 clone(`execute_parallel` 改为接收 `&[Arc<SwqosClient>]`);SWQoS 共用 HTTP 客户端常量。
- **代码质量**:抽取 buy/sell 共用的 `validate_protocol_params`;指令/账户大小与 HTTP 超时常量化;预取与分支提示注释完善。
- **文档**execution、executor、perf、swqos 等模块增加中英双语文档注释。
---
## ✨ 项目特性
1. **PumpFun 交易**: 支持`购买``卖出`功能
@@ -97,14 +89,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.0" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.5" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "3.5.0"
sol-trade-sdk = "3.5.5"
```
## 🛠️ 使用示例
+1 -3
View File
@@ -5,9 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
sol-parser-sdk = "0.2.2"
solana-sdk = "3.0.0"
solana-address-lookup-table-interface = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0.94"
+86 -87
View File
@@ -1,5 +1,18 @@
use std::{
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use sol_parser_sdk::grpc::{
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
TransactionFilter, YellowstoneGrpc,
};
use sol_parser_sdk::DexEvent;
use sol_trade_sdk::common::address_lookup::fetch_address_lookup_table_account;
use sol_trade_sdk::common::{gas_fee_strategy, GasFeeStrategy, TradeConfig};
use sol_trade_sdk::common::{GasFeeStrategy, TradeConfig};
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
@@ -9,95 +22,82 @@ use sol_trade_sdk::{
use solana_commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use solana_streamer_sdk::match_event;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use std::{
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
// Global static flag to ensure transaction is executed only once
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to GRPC events...");
println!("Subscribing to GRPC events (sol-parser-sdk, is_cashback_coin from event)...");
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
)?;
let callback = create_event_callback();
let protocols = vec![Protocol::PumpFun];
// Filter accounts
let account_include = vec![
PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID
];
let account_exclude = vec![];
let account_required = vec![];
// Listen to transaction data
let transaction_filter = TransactionFilter {
account_include: account_include.clone(),
account_exclude,
account_required,
let config = ClientConfig {
enable_metrics: false,
connection_timeout_ms: 10000,
request_timeout_ms: 30000,
enable_tls: true,
order_mode: OrderMode::Unordered,
..Default::default()
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
let grpc = YellowstoneGrpc::new_with_config(
grpc_endpoint,
std::env::var("GRPC_AUTH_TOKEN").ok(),
config,
)?;
// listen to specific event type
let event_type_filter =
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
let protocols = vec![Protocol::PumpFun];
let transaction_filter = TransactionFilter::for_protocols(&protocols);
let account_filter = AccountFilter::for_protocols(&protocols);
let event_filter = EventTypeFilter::include_only(vec![
EventType::PumpFunBuy,
EventType::PumpFunSell,
EventType::PumpFunBuyExactSolIn,
EventType::PumpFunTrade,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
Some(event_type_filter),
None,
callback,
)
.await?;
let queue = grpc
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
.await?;
loop {
if let Some(event) = queue.pop() {
let run = match &event {
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
DexEvent::PumpFunTrade(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
_ => None,
};
if let Some(e) = run {
tokio::spawn(async move {
if let Err(err) = pumpfun_copy_trade_with_grpc(e).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
std::process::exit(0);
});
break;
}
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
}
}
tokio::signal::ctrl_c().await?;
Ok(())
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
}
},
});
}
}
/// Create SolanaTrade client
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
@@ -110,9 +110,10 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
Ok(solana_trade)
}
/// PumpFun sniper trade
/// This function demonstrates how to snipe a new token from a PumpFun trade event
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
/// PumpFun copy trade: use is_cashback_coin from gRPC event (sol-parser-sdk)
async fn pumpfun_copy_trade_with_grpc(
trade_info: sol_parser_sdk::core::events::PumpFunTradeEvent,
) -> AnyResult<()> {
println!("Testing PumpFun trading...");
let client = create_solana_trade_client().await?;
@@ -127,15 +128,13 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
let gas_fee_strategy = GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
// Buy tokens
println!("Buying tokens from PumpFun...");
let buy_sol_amount = 100_000;
// is_cashback_coin from gRPC event (sol-parser-sdk parses it from trade event)
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpFun,
input_token_type: sol_trade_sdk::TradeTokenType::SOL,
mint: mint_pubkey,
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
input_token_amount: 100_000,
slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
trade_info.bonding_curve,
@@ -150,21 +149,21 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
None,
trade_info.fee_recipient,
trade_info.token_program,
false, // is_cashback_coin: set from event/parser when available
trade_info.is_cashback_coin,
)),
address_lookup_table_account: address_lookup_table_account,
address_lookup_table_account,
wait_transaction_confirmed: true,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
// Exit program
std::process::exit(0);
}
+2
View File
@@ -176,6 +176,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -226,6 +227,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
+2
View File
@@ -144,6 +144,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -187,6 +188,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
+10
View File
@@ -635,6 +635,7 @@ async fn handle_buy_pumpfun(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
@@ -690,6 +691,7 @@ async fn handle_buy_pumpswap(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
@@ -744,6 +746,7 @@ async fn handle_buy_bonk(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
@@ -802,6 +805,7 @@ async fn handle_buy_raydium_v4(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
@@ -860,6 +864,7 @@ async fn handle_buy_raydium_cpmm(
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
match client.buy(buy_params).await {
Ok((_, signature, _)) => {
@@ -1028,6 +1033,7 @@ async fn handle_sell_pumpfun(
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
match client.sell(sell_params).await {
@@ -1086,6 +1092,7 @@ async fn handle_sell_pumpswap(
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
@@ -1143,6 +1150,7 @@ async fn handle_sell_bonk(
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
@@ -1203,6 +1211,7 @@ async fn handle_sell_raydium_v4(
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
@@ -1263,6 +1272,7 @@ async fn handle_sell_raydium_cpmm(
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
@@ -44,6 +44,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -77,6 +78,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
fixed_output_token_amount: Some(1),
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
+7 -6
View File
@@ -1,9 +1,9 @@
use anyhow::Result;
use sol_trade_sdk::{
common::{AnyResult, TradeConfig},
swqos::{SwqosConfig, SwqosRegion},
swqos::SwqosConfig,
trading::{
core::params::{PumpSwapParams, DexParamEnum}, factory::DexType, middleware::builtin::LoggingMiddleware,
core::params::{PumpSwapParams, DexParamEnum}, factory::DexType,
InstructionMiddleware, MiddlewareManager,
},
SolanaTrade, TradeTokenType,
@@ -30,8 +30,8 @@ impl InstructionMiddleware for CustomMiddleware {
fn process_protocol_instructions(
&self,
protocol_instructions: Vec<Instruction>,
protocol_name: String,
is_buy: bool,
_protocol_name: String,
_is_buy: bool,
) -> Result<Vec<Instruction>> {
// do anything you want here
// you can modify the instructions here
@@ -41,8 +41,8 @@ impl InstructionMiddleware for CustomMiddleware {
fn process_full_instructions(
&self,
full_instructions: Vec<Instruction>,
protocol_name: String,
is_buy: bool,
_protocol_name: String,
_is_buy: bool,
) -> Result<Vec<Instruction>> {
// do anything you want here
// you can modify the instructions here
@@ -103,6 +103,7 @@ async fn test_middleware() -> AnyResult<()> {
gas_fee_strategy: gas_fee_strategy,
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
println!("tip: This transaction will not succeed because we're using a test account. You can modify the code to initialize the payer with your own private key");
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
sol-parser-sdk = "0.2.2"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
spl-associated-token-account = "7.0.0"
+77 -80
View File
@@ -6,6 +6,11 @@ use std::{
},
};
use sol_parser_sdk::grpc::{
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
TransactionFilter, YellowstoneGrpc,
};
use sol_parser_sdk::DexEvent;
use sol_trade_sdk::common::{nonce_cache::fetch_nonce_info, TradeConfig};
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
@@ -16,88 +21,82 @@ use sol_trade_sdk::{
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use solana_streamer_sdk::match_event;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::YellowstoneGrpc;
// Global static flag to ensure transaction is executed only once
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to GRPC events...");
println!("Subscribing to GRPC events (sol-parser-sdk, is_cashback_coin from event)...");
let grpc = YellowstoneGrpc::new(
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
None,
)?;
let callback = create_event_callback();
let protocols = vec![Protocol::PumpFun];
// Filter accounts
let account_include = vec![
PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID
];
let account_exclude = vec![];
let account_required = vec![];
// Listen to transaction data
let transaction_filter = TransactionFilter {
account_include: account_include.clone(),
account_exclude,
account_required,
let config = ClientConfig {
enable_metrics: false,
connection_timeout_ms: 10000,
request_timeout_ms: 30000,
enable_tls: true,
order_mode: OrderMode::Unordered,
..Default::default()
};
// Listen to account data belonging to owner programs -> account event monitoring
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
let grpc = YellowstoneGrpc::new_with_config(
grpc_endpoint,
std::env::var("GRPC_AUTH_TOKEN").ok(),
config,
)?;
// listen to specific event type
let event_type_filter =
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
let protocols = vec![Protocol::PumpFun];
let transaction_filter = TransactionFilter::for_protocols(&protocols);
let account_filter = AccountFilter::for_protocols(&protocols);
let event_filter = EventTypeFilter::include_only(vec![
EventType::PumpFunBuy,
EventType::PumpFunSell,
EventType::PumpFunBuyExactSolIn,
EventType::PumpFunTrade,
]);
grpc.subscribe_events_immediate(
protocols,
None,
vec![transaction_filter],
vec![account_filter],
Some(event_type_filter),
None,
callback,
)
.await?;
let queue = grpc
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
.await?;
loop {
if let Some(event) = queue.pop() {
let run = match &event {
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
DexEvent::PumpFunTrade(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
_ => None,
};
if let Some(e) = run {
tokio::spawn(async move {
if let Err(err) = pumpfun_copy_trade_with_grpc(e).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(1);
}
std::process::exit(0);
});
break;
}
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
}
}
tokio::signal::ctrl_c().await?;
Ok(())
}
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
}
},
});
}
}
/// Create SolanaTrade client
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
@@ -110,9 +109,10 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
Ok(solana_trade)
}
/// PumpFun sniper trade
/// This function demonstrates how to snipe a new token from a PumpFun trade event
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
/// PumpFun copy trade: use is_cashback_coin from gRPC event (sol-parser-sdk)
async fn pumpfun_copy_trade_with_grpc(
trade_info: sol_parser_sdk::core::events::PumpFunTradeEvent,
) -> AnyResult<()> {
println!("Testing PumpFun trading...");
let client = create_solana_trade_client().await?;
@@ -120,22 +120,19 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
let slippage_basis_points = Some(100);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
// Setup nonce cache
let nonce_account_str = Pubkey::from_str("use_your_nonce_account_here")?;
let durable_nonce = fetch_nonce_info(&client.infrastructure.rpc, nonce_account_str).await;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
// Buy tokens
println!("Buying tokens from PumpFun...");
let buy_sol_amount = 100_000;
// is_cashback_coin from gRPC event (sol-parser-sdk parses it from trade event)
let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpFun,
input_token_type: TradeTokenType::SOL,
mint: mint_pubkey,
input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points,
input_token_amount: 100_000,
slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
trade_info.bonding_curve,
@@ -150,21 +147,21 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
None,
trade_info.fee_recipient,
trade_info.token_program,
false, // is_cashback_coin: set from event/parser when available
trade_info.is_cashback_coin,
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
create_input_token_ata: false,
close_input_token_ata: false,
create_mint_ata: true,
durable_nonce: durable_nonce,
durable_nonce,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
// Exit program
std::process::exit(0);
}
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
sol-parser-sdk = "0.2.2"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+2 -1
View File
@@ -27,7 +27,6 @@ static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = rustls::crypto::ring::default_provider().install_default();
println!("PumpFun 跟单示例(sol-parser-sdk gRPC...");
let config = ClientConfig {
@@ -155,6 +154,7 @@ async fn pumpfun_copy_trade(
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -202,6 +202,7 @@ async fn pumpfun_copy_trade(
fixed_output_token_amount: None,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
sol-parser-sdk = "0.2.2"
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+2 -1
View File
@@ -27,7 +27,6 @@ static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = rustls::crypto::ring::default_provider().install_default();
println!("PumpFun 狙击示例(sol-parser-sdk gRPC...");
let config = ClientConfig {
@@ -147,6 +146,7 @@ async fn pumpfun_sniper_trade(
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -188,6 +188,7 @@ async fn pumpfun_sniper_trade(
fixed_output_token_amount: None,
gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
@@ -44,6 +44,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -72,6 +73,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
create_output_token_ata: true,
close_output_token_ata: true,
close_mint_token_ata: false,
grpc_recv_us: None,
durable_nonce: None,
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
+18 -7
View File
@@ -1,6 +1,7 @@
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
@@ -136,7 +137,9 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
}
async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> AnyResult<()> {
let params = PumpSwapParams::new(
let client = create_solana_trade_client().await?;
let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?;
let params = PumpSwapParams::from_trade(
trade_info.pool,
trade_info.base_mint,
trade_info.quote_mint,
@@ -149,6 +152,7 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.is_cashback_coin,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -157,12 +161,14 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
} else {
trade_info.base_mint
};
pumpswap_trade_with_grpc(mint, params).await?;
pumpswap_trade_with_grpc(&client, mint, params).await?;
Ok(())
}
async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> {
let params = PumpSwapParams::new(
let client = create_solana_trade_client().await?;
let pool_data = fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?;
let params = PumpSwapParams::from_trade(
trade_info.pool,
trade_info.base_mint,
trade_info.quote_mint,
@@ -175,6 +181,7 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
trade_info.base_token_program,
trade_info.quote_token_program,
trade_info.protocol_fee_recipient,
pool_data.is_cashback_coin,
);
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
@@ -183,14 +190,16 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
} else {
trade_info.base_mint
};
pumpswap_trade_with_grpc(mint, params).await?;
pumpswap_trade_with_grpc(&client, mint, params).await?;
Ok(())
}
async fn pumpswap_trade_with_grpc(mint_pubkey: Pubkey, params: PumpSwapParams) -> AnyResult<()> {
async fn pumpswap_trade_with_grpc(
client: &SolanaTrade,
mint_pubkey: Pubkey,
params: PumpSwapParams,
) -> AnyResult<()> {
println!("Testing PumpSwap trading...");
let client = create_solana_trade_client().await?;
let slippage_basis_points = Some(500);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
@@ -221,6 +230,7 @@ async fn pumpswap_trade_with_grpc(mint_pubkey: Pubkey, params: PumpSwapParams) -
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -255,6 +265,7 @@ async fn pumpswap_trade_with_grpc(mint_pubkey: Pubkey, params: PumpSwapParams) -
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
@@ -174,6 +174,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -212,6 +213,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
@@ -159,6 +159,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -194,6 +195,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
+2
View File
@@ -47,6 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
gas_fee_strategy: gas_fee_strategy.clone(),
simulate: false,
use_exact_sol_amount: None,
grpc_recv_us: None,
};
client.buy(buy_params).await?;
@@ -87,6 +88,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy,
simulate: false,
grpc_recv_us: None,
};
client.sell(sell_params).await?;
@@ -31,6 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your_uuid".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Bloxroute("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
];
// Step 1: Create shared infrastructure (expensive, do once)
+2
View File
@@ -49,6 +49,8 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Astralane("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
];
// Optional: Customize WSOL ATA and Seed optimization settings
+45
View File
@@ -768,6 +768,21 @@
{
"name": "fee_program",
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
},
{
"name": "bonding_curve_v2",
"pda": {
"seeds": [
{
"kind": "const",
"value": [98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101, 45, 118, 50]
},
{
"kind": "account",
"path": "mint"
}
]
}
}
],
"args": [
@@ -1164,6 +1179,21 @@
{
"name": "fee_program",
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
},
{
"name": "bonding_curve_v2",
"pda": {
"seeds": [
{
"kind": "const",
"value": [98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101, 45, 118, 50]
},
{
"kind": "account",
"path": "mint"
}
]
}
}
],
"args": [
@@ -3957,6 +3987,21 @@
{
"name": "fee_program",
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
},
{
"name": "bonding_curve_v2",
"pda": {
"seeds": [
{
"kind": "const",
"value": [98, 111, 110, 100, 105, 110, 103, 45, 99, 117, 114, 118, 101, 45, 118, 50]
},
{
"kind": "account",
"path": "mint"
}
]
}
}
],
"args": [
+46 -1
View File
@@ -688,6 +688,21 @@
{
"name": "fee_program",
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
},
{
"name": "pool_v2",
"pda": {
"seeds": [
{
"kind": "const",
"value": [112, 111, 111, 108, 45, 118, 50]
},
{
"kind": "account",
"path": "base_mint"
}
]
}
}
],
"args": [
@@ -1119,6 +1134,21 @@
{
"name": "fee_program",
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
},
{
"name": "pool_v2",
"pda": {
"seeds": [
{
"kind": "const",
"value": [112, 111, 111, 108, 45, 118, 50]
},
{
"kind": "account",
"path": "base_mint"
}
]
}
}
],
"args": [
@@ -3162,6 +3192,21 @@
{
"name": "fee_program",
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
},
{
"name": "pool_v2",
"pda": {
"seeds": [
{
"kind": "const",
"value": [112, 111, 111, 108, 45, 118, 50]
},
{
"kind": "account",
"path": "base_mint"
}
]
}
}
],
"args": [
@@ -4835,7 +4880,7 @@
},
{
"code": 6052,
"name": "CashbackEarnedDoesNotMatchTokenInVault"
"name": "TokensInVaultLessThanCashbackEarned"
}
],
"types": [
+9 -10
View File
@@ -6,20 +6,19 @@ Rust SDK for Solana DEX trading (Pump.fun, PumpSwap, Raydium, Bonk, Meteora, etc
### Performance
- **Hot-path timing**: `Instant::now()` for build/submit/total/confirm only when `log_enabled` or (for total) simulate, reducing cold-path syscalls.
- **Fewer clones**: `execute_parallel` now takes `&[Arc<SwqosClient>]` instead of `Vec<Arc<SwqosClient>>`; caller no longer clones the client list.
- **SWQoS HTTP**: Named constants for pool idle timeout, connect/request timeouts, and HTTP/2 keepalive in `swqos/common.rs`.
- **Executor hot path**: Sample `Instant::now()` only when `log_enabled` or `simulate` (total, build, submit, confirm) to reduce cold-path syscalls.
- **SWQOS**: `execute_parallel` now takes `&[Arc<SwqosClient>]` to avoid cloning the client list on each swap.
- **Constants**: Named constants for instruction/account sizes and HTTP client timeouts; no magic numbers in hot paths.
### Code quality
### Code Quality
- **Protocol params**: Single `validate_protocol_params(dex_type, params)` used by both buy and sell; removed duplicated match blocks.
- **Constants**: `BYTES_PER_ACCOUNT`, `MAX_INSTRUCTIONS_WARN` in execution; HTTP timeout constants in swqos common.
- **Comments**: Prefetch and branch-hint safety/usage documented; `SYSCALL_BYPASS` marked as reserved for future use.
- **Protocol params**: Single `validate_protocol_params()` used for both buy and sell; removed duplicate match blocks in `lib.rs`.
- **Comments**: Bilingual (English + 中文) doc and inline comments across execution, executor, perf (hardware_optimizations, syscall_bypass), and swqos/common.
- **Prefetch/safety**: Clearer docs for cache prefetch and `unsafe` usage (valid read-only ref, no concurrent write).
### Documentation
- **Bilingual docs**: English + 中文 doc comments in `trading/core/execution.rs`, `trading/core/executor.rs`, `perf/hardware_optimizations.rs`, `perf/mod.rs`, `perf/syscall_bypass.rs`, `swqos/common.rs`.
- **README**: Version references and "What's new in 3.5.0" (EN) / "3.5.0 更新说明" (CN) updated.
- README (EN/CN): Version references updated to 3.5.0 for path and crates.io usage.
---
@@ -30,7 +29,7 @@ Rust SDK for Solana DEX trading (Pump.fun, PumpSwap, Raydium, Bonk, Meteora, etc
sol-trade-sdk = { git = "https://github.com/0xfnzero/sol-trade-sdk", tag = "v3.5.0" }
```
**From crates.io** (when published):
**From crates.io:**
```toml
sol-trade-sdk = "3.5.0"
```
+2
View File
@@ -153,10 +153,12 @@ pub fn _create_associated_token_account_idempotent_fast(
pub enum PdaCacheKey {
PumpFunUserVolume(Pubkey),
PumpFunBondingCurve(Pubkey),
PumpFunBondingCurveV2(Pubkey),
PumpFunCreatorVault(Pubkey),
BonkPool(Pubkey, Pubkey),
BonkVault(Pubkey, Pubkey),
PumpSwapUserVolume(Pubkey),
PumpSwapPoolV2(Pubkey),
}
/// Global lock-free PDA cache for storing computation results
+9
View File
@@ -76,6 +76,8 @@ pub struct TradeConfig {
pub use_core_affinity: bool,
/// Whether to output all SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default true.
pub log_enabled: bool,
/// Whether to check minimum tip per SWQOS provider (filter out configs below min). Default false to save latency.
pub check_min_tip: bool,
}
impl TradeConfig {
@@ -96,6 +98,7 @@ impl TradeConfig {
use_seed_optimize: true, // default: use seed optimization
use_core_affinity: true, // default: pin parallel submit tasks to cores
log_enabled: true, // default: enable all SDK logs
check_min_tip: false, // default: skip min tip check to reduce latency
}
}
@@ -109,6 +112,12 @@ impl TradeConfig {
self.use_seed_optimize = use_seed_optimize;
self
}
/// Set whether to check minimum tip per SWQOS (filter out configs below min). Default false for lower latency.
pub fn with_check_min_tip(mut self, check_min_tip: bool) -> Self {
self.check_min_tip = check_min_tip;
self
}
}
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
+51 -16
View File
@@ -13,6 +13,20 @@ pub const JITO_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"),
];
/// Helius Sender tip accounts (fee recipient addresses).
pub const HELIUS_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("4ACfpUFoaSD9bfPdeu6DBt89gB6ENTeHBXCAi87NhDEE"),
pubkey!("D2L6yPZ2FmmmTKPgzaMKdhu6EWZcTpLy1Vhx8uvZe7NZ"),
pubkey!("9bnz4RShgq1hAnLnZbP8kbgBg1kEmcJBYQq3gQbmnSta"),
pubkey!("5VY91ws6B2hMmBFRsXkoAAdsPHBJwRfBht4DXox3xkwn"),
pubkey!("2nyhqdwKcJZR2vcqCyrYsaPVdAnFoJjiksCXJ7hfEYgD"),
pubkey!("2q5pghRs6arqVjRvT5gfgWfWcHWmw1ZuCzphgd5KfWGJ"),
pubkey!("wyvPkWjVZz1M8fHQnMMCDTQDbkManefNNhweYk5WkcF"),
pubkey!("3KCKozbAaF75qEU33jtzozcJ29yJuaLJTy2jFdzUY8bT"),
pubkey!("4vieeGHPYPG2MmyPRcYjdiDmmhN3ww7hsFNap8pVN3Ey"),
pubkey!("4TQLFNWK8AovT1gFvda5jfw2oJeRMKEmw7aH6MGBJ3or"),
];
pub const NEXTBLOCK_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
pubkey!("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
@@ -227,26 +241,30 @@ pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
"http://ny.flashblock.trade",
];
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
"http://amsterdam.solana.blockrazor.xyz:443/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
"http://tokyo.solana.blockrazor.xyz:443/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
];
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
"http://ny.gateway.astralane.io/iris",
"http://fr.gateway.astralane.io/iris",
"http://ams.gateway.astralane.io/iris",
"http://ny.gateway.astralane.io/iris",
"http://jp.gateway.astralane.io/iris",
"http://ny.gateway.astralane.io/iris",
"http://lax.gateway.astralane.io/iris",
"http://lim.gateway.astralane.io/iris",
"http://ny.gateway.astralane.io/irisb",
"http://fr.gateway.astralane.io/irisb",
"http://ams.gateway.astralane.io/irisb",
"http://ny.gateway.astralane.io/irisb",
"http://jp.gateway.astralane.io/irisb",
"http://ny.gateway.astralane.io/irisb",
"http://lax.gateway.astralane.io/irisb",
"http://lim.gateway.astralane.io/irisb",
];
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
@@ -282,6 +300,19 @@ pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [
"fra.speedlanding.trade:17778",
];
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
/// Region order: NewYork(EWR), Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles(SG), Default(Global).
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
"https://ewr-sender.helius-rpc.com/fast",
"https://fra-sender.helius-rpc.com/fast",
"https://ams-sender.helius-rpc.com/fast",
"https://slc-sender.helius-rpc.com/fast",
"https://tyo-sender.helius-rpc.com/fast",
"https://lon-sender.helius-rpc.com/fast",
"https://sg-sender.helius-rpc.com/fast",
"https://sender.helius-rpc.com/fast",
];
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001;
pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001;
@@ -296,3 +327,7 @@ pub const SWQOS_MIN_TIP_STELLIUM: f64 = 0.0001; // Stellium requires minimum 0.0
pub const SWQOS_MIN_TIP_LIGHTSPEED: f64 = 0.0001; // Lightspeed requires minimum 0.001 SOL tip
pub const SWQOS_MIN_TIP_SOYAS: f64 = 0.001; // Soyas requires minimum 0.001 SOL tip
pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires minimum 0.001 SOL tip
/// Helius Sender: 0.0002 SOL when not swqos_only; use SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY when swqos_only=true.
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
+10 -4
View File
@@ -8,8 +8,9 @@ use crate::{
};
use crate::{
instruction::utils::pumpfun::{
accounts, get_bonding_curve_pda, get_creator, get_user_volume_accumulator_pda,
global_constants::{self}, BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
get_user_volume_accumulator_pda, global_constants::{self}, BUY_DISCRIMINATOR,
BUY_EXACT_SOL_IN_DISCRIMINATOR,
},
utils::calc::{
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
@@ -143,7 +144,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
global_constants::FEE_RECIPIENT_META
};
let accounts: [AccountMeta; 16] = [
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.output_mint).unwrap();
let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
fee_recipient_meta,
AccountMeta::new_readonly(params.output_mint, false),
@@ -161,11 +163,12 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
accounts::FEE_CONFIG_META,
accounts::FEE_PROGRAM_META,
];
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // bonding_curve_v2 (readonly) at end
instructions.push(Instruction::new_with_bytes(
accounts::PUMPFUN,
&buy_data,
accounts.to_vec(),
accounts,
));
Ok(instructions)
@@ -286,6 +289,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
get_user_volume_accumulator_pda(&params.payer.pubkey()).unwrap();
accounts.push(AccountMeta::new(user_volume_accumulator, false));
}
// Program upgrade: bonding_curve_v2 (readonly) at end of account list
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.input_mint).unwrap();
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false));
instructions.push(Instruction::new_with_bytes(
accounts::PUMPFUN,
+11 -1
View File
@@ -1,7 +1,7 @@
use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::pumpswap::{
accounts, fee_recipient_ata, get_user_volume_accumulator_pda,
accounts, fee_recipient_ata, get_pool_v2_pda, get_user_volume_accumulator_pda,
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR,
BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
@@ -190,6 +190,11 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
accounts.push(AccountMeta::new(wsol_ata, false));
}
}
// Program upgrade: pool_v2 (readonly) at end of account list
accounts.push(AccountMeta::new_readonly(
get_pool_v2_pda(&base_mint).unwrap(),
false,
));
// Create instruction data
let mut data = [0u8; 24];
@@ -392,6 +397,11 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
accounts.push(AccountMeta::new(accumulator, false));
}
}
// Program upgrade: pool_v2 (readonly) at end of account list
accounts.push(AccountMeta::new_readonly(
get_pool_v2_pda(&base_mint).unwrap(),
false,
));
// Create instruction data
let mut data = [0u8; 24];
+16
View File
@@ -7,6 +7,8 @@ use std::sync::Arc;
pub mod seeds {
/// Seed for bonding curve PDAs
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
/// Seed for bonding curve v2 PDA (required by program upgrade, readonly at end of account list)
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
/// Seed for creator vault PDAs
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
@@ -178,6 +180,20 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
)
}
/// Bonding curve v2 PDA (seeds: ["bonding-curve-v2", mint]). Required at end of buy/sell/buy_exact_sol_in accounts.
#[inline]
pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
crate::common::fast_fn::get_cached_pda(
crate::common::fast_fn::PdaCacheKey::PumpFunBondingCurveV2(*mint),
|| {
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_V2_SEED, mint.as_ref()];
let program_id: &Pubkey = &accounts::PUMPFUN;
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
pda.map(|pubkey| pubkey.0)
},
)
}
#[inline]
pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
if creator_vault_pda.eq(&Pubkey::default()) {
+13
View File
@@ -26,6 +26,9 @@ pub mod seeds {
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
/// Seed for pool v2 PDA (required by program upgrade, readonly at end of account list)
pub const POOL_V2_SEED: &[u8] = b"pool-v2";
}
/// Constants related to program accounts and authorities
@@ -139,6 +142,16 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
#[inline]
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
let (pda, _) = Pubkey::find_program_address(
&[seeds::POOL_V2_SEED, base_mint.as_ref()],
&accounts::AMM_PROGRAM,
);
Some(pda)
}
// Find a pool for a specific mint
pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<Pubkey, anyhow::Error> {
let (pool_address, _) = find_by_mint(rpc, mint).await?;
+8
View File
@@ -157,6 +157,8 @@ pub struct TradingClient {
pub use_core_affinity: bool,
/// Whether to output all SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency.
pub check_min_tip: bool,
}
static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
@@ -173,6 +175,7 @@ impl Clone for TradingClient {
use_seed_optimize: self.use_seed_optimize,
use_core_affinity: self.use_core_affinity,
log_enabled: self.log_enabled,
check_min_tip: self.check_min_tip,
}
}
}
@@ -301,6 +304,7 @@ impl TradingClient {
use_seed_optimize,
use_core_affinity: true,
log_enabled: true,
check_min_tip: false,
}
}
@@ -340,6 +344,7 @@ impl TradingClient {
use_seed_optimize,
use_core_affinity: true,
log_enabled: true,
check_min_tip: false,
}
}
@@ -503,6 +508,7 @@ impl TradingClient {
use_seed_optimize: trade_config.use_seed_optimize,
use_core_affinity: trade_config.use_core_affinity,
log_enabled: trade_config.log_enabled,
check_min_tip: trade_config.check_min_tip,
};
let mut current = INSTANCE.lock();
@@ -636,6 +642,7 @@ impl TradingClient {
simulate: params.simulate,
log_enabled: self.log_enabled,
use_core_affinity: self.use_core_affinity,
check_min_tip: self.check_min_tip,
grpc_recv_us: params.grpc_recv_us,
use_exact_sol_amount: params.use_exact_sol_amount,
};
@@ -732,6 +739,7 @@ impl TradingClient {
simulate: params.simulate,
log_enabled: self.log_enabled,
use_core_affinity: self.use_core_affinity,
check_min_tip: self.check_min_tip,
grpc_recv_us: params.grpc_recv_us,
use_exact_sol_amount: None,
};
+30 -66
View File
@@ -1,13 +1,12 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use anyhow::Result;
use bincode::serialize as bincode_serialize;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
@@ -17,6 +16,9 @@ use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
/// Empty body for getHealth POST; avoid per-request allocation.
static PING_BODY: &[u8] = &[];
#[derive(Clone)]
pub struct AstralaneClient {
pub endpoint: String,
@@ -50,19 +52,7 @@ impl SwqosClientTrait for AstralaneClient {
impl AstralaneClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(300))
.pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
@@ -90,13 +80,9 @@ impl AstralaneClient {
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("Astralane ping request failed: {}", e);
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
interval.tick().await; // first tick completes immediately → one ping at start
if stop_ping.load(Ordering::Relaxed) {
break;
}
@@ -116,71 +102,49 @@ impl AstralaneClient {
}
}
/// Send ping request to /gethealth endpoint
/// Send ping request: POST endpoint?api-key=...&method=getHealth (endpoint is irisb from constants).
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
// Build ping URL by replacing /iris with /gethealth
let ping_url = if endpoint.ends_with("/iris") {
endpoint.replace("/iris", "/gethealth")
} else if endpoint.ends_with("/iris/") {
endpoint.replace("/iris/", "/gethealth")
} else if endpoint.ends_with('/') {
format!("{}gethealth", endpoint)
} else {
format!("{}/gethealth", endpoint)
};
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url)
.header("api_key", auth_token)
let response = http_client
.post(endpoint)
.query(&[("api-key", auth_token), ("method", "getHealth")])
.timeout(Duration::from_millis(1500))
.body(PING_BODY)
.send()
.await?;
let status = response.status();
let _ = response.bytes().await;
let _ = response.bytes().await; // consume body so connection returns to pool
if !status.is_success() {
eprintln!("Astralane ping request returned non-success status: {}", status);
}
Ok(())
}
/// Send transaction via /irisb binary API (no Base64; lower latency).
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let signature = transaction.get_signature();
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
content,
{ "encoding": "base64", "skipPreflight": true },
{ "mevProtect": false }
]
}))?;
let body_bytes = bincode_serialize(transaction).map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
// Send request with api_key header
let response_text = self.http_client.post(&self.endpoint)
.body(request_body)
.header("Content-Type", "application/json")
.header("api_key", &self.auth_token)
let response = self.http_client
.post(&self.endpoint)
.query(&[("api-key", self.auth_token.as_str()), ("method", "sendTransaction")])
.header("Content-Type", "application/octet-stream")
.body(body_bytes)
.send()
.await?
.text()
.await?;
// Parse JSON response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, _error);
}
let status = response.status();
let _ = response.bytes().await;
if status.is_success() {
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
} else {
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [astralane] {} submission failed: status {}", trade_type, status);
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
}
let start_time: Instant = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
+26 -62
View File
@@ -1,7 +1,6 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use rand::seq::IndexedRandom;
use reqwest::{Client, header::{HeaderMap, HeaderValue, CONTENT_TYPE}};
use serde_json::json;
use reqwest::Client;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
@@ -50,19 +49,7 @@ impl SwqosClientTrait for BlockRazorClient {
impl BlockRazorClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(300)) // 5min so ping-kept connection is not evicted early
.pool_max_idle_per_host(4) // Few connections so submit reuses same connection as ping, avoiding cold connection after ~5min server idle close
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
@@ -120,31 +107,15 @@ impl BlockRazorClient {
}
}
/// Send ping request to /health endpoint
/// Send ping request: POST /v2/health?auth=... (Keep Alive). Only required param: auth.
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
// Build health URL by replacing sendTransaction with health
let ping_url = if endpoint.ends_with("sendTransaction") {
endpoint.replace("sendTransaction", "health")
} else if endpoint.ends_with("/sendTransaction") {
endpoint.replace("/sendTransaction", "/health")
} else {
// Fallback to original logic if endpoint doesn't end with sendTransaction
if endpoint.ends_with('/') {
format!("{}health", endpoint)
} else {
format!("{}/health", endpoint)
}
};
// Prepare headers
let mut headers = HeaderMap::new();
headers.insert("apikey", HeaderValue::from_str(auth_token)?);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url)
.headers(headers)
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
let response = http_client
.post(&ping_url)
.query(&[("auth", auth_token)])
.header("Content-Type", "text/plain")
.timeout(Duration::from_millis(1500))
.body(&[] as &[u8])
.send()
.await?;
let status = response.status();
@@ -155,40 +126,33 @@ impl BlockRazorClient {
Ok(())
}
/// Send transaction via v2 API: plain Base64 body, Content-Type: text/plain. Only required URI param: auth.
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// BlockRazor fast-mode request format
let request_body = serde_json::to_string(&json!({
"transaction": content,
"mode": "fast"
}))?;
// BlockRazor uses apikey header
let response_text = self.http_client.post(&self.endpoint)
.body(request_body)
.header("Content-Type", "application/json")
.header("apikey", &self.auth_token)
let response = self.http_client
.post(&self.endpoint)
.query(&[("auth", self.auth_token.as_str())])
.header("Content-Type", "text/plain")
.body(content)
.send()
.await?
.text()
.await?;
// Parse JSON response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
let status = response.status();
let _ = response.bytes().await;
if status.is_success() {
if crate::common::sdk_log::sdk_log_enabled() {
if response_json.get("result").is_some() || response_json.get("signature").is_some() {
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, _error);
}
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, response_text);
} else {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [blockrazor] {} submission failed: status {}", trade_type, status);
}
return Err(anyhow::anyhow!("BlockRazor sendTransaction failed: {}", status));
}
let start_time: Instant = Instant::now();
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
+39 -16
View File
@@ -89,41 +89,64 @@ pub async fn poll_transaction_confirmation(
txt_sig: Signature,
wait_confirmation: bool,
) -> Result<Signature> {
// If no confirmation needed, return signature immediately
poll_any_transaction_confirmation(rpc, &[txt_sig], wait_confirmation).await
}
/// Poll multiple signatures in parallel (one RPC call per poll) and return the first one that confirms.
/// When transactions are submitted to multiple SWQOS channels, each channel produces a different
/// signature. Only one will land on-chain, so we must check all of them.
pub async fn poll_any_transaction_confirmation(
rpc: &SolanaRpcClient,
signatures: &[Signature],
wait_confirmation: bool,
) -> Result<Signature> {
if signatures.is_empty() {
return Err(anyhow::anyhow!("No signatures to confirm"));
}
// If no confirmation needed, return first signature immediately
if !wait_confirmation {
return Ok(txt_sig);
return Ok(signatures[0]);
}
let timeout: Duration = Duration::from_secs(15); // 15s to avoid timeout under network congestion
let timeout: Duration = Duration::from_secs(15);
let interval: Duration = Duration::from_millis(1000);
let start: Instant = Instant::now();
let mut poll_count = 0u32;
// Track which signature landed (confirmed or failed on-chain)
let mut landed_sig: Option<Signature> = None;
loop {
if start.elapsed() >= timeout {
return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig));
return Err(anyhow::anyhow!("Transaction confirmation timed out after {}s ({} signatures polled)", timeout.as_secs(), signatures.len()));
}
poll_count += 1;
let status = rpc.get_signature_statuses(&[txt_sig]).await?;
let first = status.value.get(0).and_then(|o| o.as_ref());
match first {
Some(s) => {
let status = rpc.get_signature_statuses(signatures).await?;
// Check all signatures for any that confirmed successfully
for (i, maybe_status) in status.value.iter().enumerate() {
if let Some(s) = maybe_status {
if s.err.is_none()
&& (s.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|| s.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
{
return Ok(txt_sig);
return Ok(signatures[i]);
}
// Track the first signature that landed on-chain (even if errored)
if landed_sig.is_none() {
landed_sig = Some(signatures[i]);
}
}
None => {
sleep(interval).await;
continue;
}
}
let should_get_transaction = first.map(|s| s.err.is_some()).unwrap_or(false) || poll_count >= 10;
// If no signature has any status yet, keep waiting
if landed_sig.is_none() {
sleep(interval).await;
continue;
}
let landed = landed_sig.unwrap();
let should_get_transaction = poll_count >= 10;
if !should_get_transaction {
sleep(interval).await;
@@ -132,7 +155,7 @@ pub async fn poll_transaction_confirmation(
let tx_details = match rpc
.get_transaction_with_config(
&txt_sig,
&landed,
RpcTransactionConfig {
encoding: Some(UiTransactionEncoding::JsonParsed),
max_supported_transaction_version: Some(0),
@@ -155,7 +178,7 @@ pub async fn poll_transaction_confirmation(
} else {
let meta = meta.unwrap();
if meta.err.is_none() {
return Ok(txt_sig);
return Ok(landed);
} else {
// Extract error message from log_messages
let mut error_msg = String::new();
+2 -15
View File
@@ -1,10 +1,9 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use anyhow::Result;
@@ -46,19 +45,7 @@ impl SwqosClientTrait for FlashBlockClient {
impl FlashBlockClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120))
.pool_max_idle_per_host(256) // Increased from 64 to 256
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
+220
View File
@@ -0,0 +1,220 @@
//! Helius Sender SWQOS client.
//!
//! Ultra-low latency transaction submission with dual routing to validators and Jito.
//! All transactions must include tips, priority fees, and skip preflight.
//! - Without swqos_only: minimum tip 0.0002 SOL.
//! - With swqos_only=true: minimum tip 0.000005 SOL (much lower, benefit of Helius).
//! API: POST {endpoint}/fast with JSON-RPC sendTransaction.
//! Optional query: api-key (custom TPS only), swqos_only (SWQOS-only routing, lower min tip).
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use anyhow::Result;
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use solana_sdk::transaction::VersionedTransaction;
use solana_transaction_status::UiTransactionEncoding;
use std::sync::Arc;
use std::time::Instant;
use crate::common::SolanaRpcClient;
use crate::constants::swqos::{HELIUS_TIP_ACCOUNTS, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY};
use crate::swqos::{SwqosClientTrait, SwqosType, TradeType};
#[derive(Clone)]
pub struct HeliusClient {
/// Cached full URL with query params (auth/swqos_only) to avoid per-request allocation.
pub submit_url: String,
pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client,
/// When true, min_tip_sol() returns 0.000005; else 0.0002.
swqos_only: bool,
}
impl HeliusClient {
pub fn new(
rpc_url: String,
endpoint: String,
api_key: Option<String>,
swqos_only: bool,
) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let submit_url = Self::build_submit_url(&endpoint, api_key.as_deref(), swqos_only);
Self {
submit_url,
rpc_client: Arc::new(rpc_client),
http_client,
swqos_only,
}
}
/// Build URL once at construction; no per-request allocation.
#[inline]
fn build_submit_url(endpoint: &str, api_key: Option<&str>, swqos_only: bool) -> String {
let mut url = endpoint.to_string();
let mut has_query = endpoint.contains('?');
if let Some(key) = api_key {
if !key.is_empty() {
url.push_str(if has_query { "&" } else { "?" });
url.push_str("api-key=");
url.push_str(key);
has_query = true;
}
}
if swqos_only {
url.push_str(if has_query { "&" } else { "?" });
url.push_str("swqos_only=true");
}
url
}
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": "1",
"method": "sendTransaction",
"params": [
content,
{
"encoding": "base64",
"skipPreflight": true,
"maxRetries": 0
}
]
}))?;
let response = self
.http_client
.post(&self.submit_url)
.body(request_body)
.header("Content-Type", "application/json")
.send()
.await?;
let status = response.status();
let response_text = response.text().await?;
if !status.is_success() {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} submission failed status={} body={}",
trade_type, status, response_text
);
}
return Err(anyhow::anyhow!(
"Helius Sender failed: status={} body={}",
status,
response_text
));
}
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("error").is_some() {
let err_msg = response_json["error"]
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [helius] {} submission error: {}", trade_type, err_msg);
}
return Err(anyhow::anyhow!("Helius Sender error: {}", err_msg));
}
if response_json.get("result").is_some() && crate::common::sdk_log::sdk_log_enabled() {
println!(
" [helius] {} submitted: {:?}",
trade_type,
start_time.elapsed()
);
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} submission failed: {:?}",
trade_type, response_text
);
}
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
}
return Err(e);
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(
" signature: {:?}",
signature
);
println!(
" [helius] {} confirmed: {:?}",
trade_type,
start_time.elapsed()
);
}
Ok(())
}
}
#[async_trait::async_trait]
impl SwqosClientTrait for HeliusClient {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
HeliusClient::send_transaction(self, trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation)
.await?;
}
Ok(())
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *HELIUS_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| HELIUS_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
fn get_swqos_type(&self) -> SwqosType {
SwqosType::Helius
}
#[inline(always)]
fn min_tip_sol(&self) -> f64 {
if self.swqos_only {
SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY
} else {
SWQOS_MIN_TIP_HELIUS
}
}
}
+2 -15
View File
@@ -1,11 +1,10 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use anyhow::Result;
@@ -49,19 +48,7 @@ impl SwqosClientTrait for JitoClient {
impl JitoClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120))
.pool_max_idle_per_host(256) // Increased from 64 to 256
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
+2 -15
View File
@@ -1,10 +1,9 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use anyhow::Result;
@@ -47,19 +46,7 @@ impl LightspeedClient {
// Lightspeed endpoint should already include /lightspeed path
// Format: https://<tier>.rpc.solanavibestation.com/lightspeed?api_key=<key>
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120))
.pool_max_idle_per_host(256)
.tcp_keepalive(Some(Duration::from_secs(60)))
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000))
.connect_timeout(Duration::from_millis(2000))
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
+60 -1
View File
@@ -14,6 +14,7 @@ pub mod stellium;
pub mod lightspeed;
pub mod soyas;
pub mod speedlanding;
pub mod helius;
use std::sync::Arc;
@@ -37,7 +38,23 @@ use crate::{
SWQOS_ENDPOINTS_ASTRALANE,
SWQOS_ENDPOINTS_STELLIUM,
SWQOS_ENDPOINTS_SOYAS,
SWQOS_ENDPOINTS_SPEEDLANDING
SWQOS_ENDPOINTS_SPEEDLANDING,
SWQOS_ENDPOINTS_HELIUS,
SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_NEXTBLOCK,
SWQOS_MIN_TIP_ZERO_SLOT,
SWQOS_MIN_TIP_TEMPORAL,
SWQOS_MIN_TIP_BLOXROUTE,
SWQOS_MIN_TIP_NODE1,
SWQOS_MIN_TIP_FLASHBLOCK,
SWQOS_MIN_TIP_BLOCKRAZOR,
SWQOS_MIN_TIP_ASTRALANE,
SWQOS_MIN_TIP_STELLIUM,
SWQOS_MIN_TIP_LIGHTSPEED,
SWQOS_MIN_TIP_SOYAS,
SWQOS_MIN_TIP_SPEEDLANDING,
SWQOS_MIN_TIP_HELIUS,
},
swqos::{
bloxroute::BloxrouteClient,
@@ -54,6 +71,7 @@ use crate::{
lightspeed::LightspeedClient,
soyas::SoyasClient,
speedlanding::SpeedlandingClient,
helius::HeliusClient,
}
};
@@ -103,6 +121,7 @@ pub enum SwqosType {
Lightspeed,
Soyas,
Speedlanding,
Helius,
Default,
}
@@ -121,6 +140,8 @@ impl SwqosType {
Self::Stellium,
Self::Lightspeed,
Self::Soyas,
Self::Speedlanding,
Self::Helius,
Self::Default,
]
}
@@ -134,6 +155,27 @@ pub trait SwqosClientTrait {
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()>;
fn get_tip_account(&self) -> Result<String>;
fn get_swqos_type(&self) -> SwqosType;
/// Minimum tip in SOL required by this provider. Helius returns lower value when swqos_only is true.
#[inline]
fn min_tip_sol(&self) -> f64 {
match self.get_swqos_type() {
SwqosType::Jito => SWQOS_MIN_TIP_JITO,
SwqosType::NextBlock => SWQOS_MIN_TIP_NEXTBLOCK,
SwqosType::ZeroSlot => SWQOS_MIN_TIP_ZERO_SLOT,
SwqosType::Temporal => SWQOS_MIN_TIP_TEMPORAL,
SwqosType::Bloxroute => SWQOS_MIN_TIP_BLOXROUTE,
SwqosType::Node1 => SWQOS_MIN_TIP_NODE1,
SwqosType::FlashBlock => SWQOS_MIN_TIP_FLASHBLOCK,
SwqosType::BlockRazor => SWQOS_MIN_TIP_BLOCKRAZOR,
SwqosType::Astralane => SWQOS_MIN_TIP_ASTRALANE,
SwqosType::Stellium => SWQOS_MIN_TIP_STELLIUM,
SwqosType::Lightspeed => SWQOS_MIN_TIP_LIGHTSPEED,
SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS,
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
SwqosType::Helius => SWQOS_MIN_TIP_HELIUS,
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -180,6 +222,9 @@ pub enum SwqosConfig {
/// To apply for an API key, please contact -> https://t.me/speedlanding_bot?start=0xzero
/// Minimum tip: 0.001 SOL
Speedlanding(String, SwqosRegion, Option<String>),
/// Helius Sender: dual routing to validators and Jito. API key optional (custom TPS only).
/// (api_key, region, custom_url, swqos_only). swqos_only: None => false (min tip 0.0002 SOL); Some(true) => SWQOS-only (min tip 0.000005 SOL, much lower).
Helius(String, SwqosRegion, Option<String>, Option<bool>),
}
impl SwqosConfig {
@@ -199,6 +244,7 @@ impl SwqosConfig {
SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed,
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
}
}
@@ -226,6 +272,7 @@ impl SwqosConfig {
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(),
SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(),
SwqosType::Default => "".to_string(),
}
}
@@ -349,6 +396,18 @@ impl SwqosConfig {
).await?;
Ok(Arc::new(speedlanding_client))
},
SwqosConfig::Helius(api_key, region, url, swqos_only) => {
let swqos_only = swqos_only.unwrap_or(false);
let endpoint = SwqosConfig::get_endpoint(SwqosType::Helius, region, url.clone());
let api_key_opt = if api_key.is_empty() { None } else { Some(api_key.clone()) };
let helius_client = HeliusClient::new(
rpc_url.clone(),
endpoint,
api_key_opt,
swqos_only,
);
Ok(Arc::new(helius_client))
},
SwqosConfig::Default(endpoint) => {
let rpc = SolanaRpcClient::new_with_commitment(
endpoint,
+2 -15
View File
@@ -1,10 +1,9 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use anyhow::Result;
@@ -51,19 +50,7 @@ impl NextBlockClient {
format!("{}/api/v2/submit", endpoint.trim_end_matches('/'))
};
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120))
.pool_max_idle_per_host(256) // Increased from 64 to 256
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
+2 -14
View File
@@ -1,5 +1,5 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -76,19 +76,7 @@ impl SwqosClientTrait for TemporalClient {
impl TemporalClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(300))
.pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
+2 -15
View File
@@ -1,10 +1,9 @@
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use anyhow::Result;
@@ -46,19 +45,7 @@ impl SwqosClientTrait for ZeroSlotClient {
impl ZeroSlotClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder()
// Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120))
.pool_max_idle_per_host(256) // Increased from 64 to 256
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10))
.http2_keep_alive_timeout(Duration::from_secs(5))
.http2_adaptive_window(true) // Enable adaptive flow control
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
.build()
.unwrap();
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
+11 -36
View File
@@ -15,22 +15,6 @@ use crate::{
common::{GasFeeStrategy, SolanaRpcClient},
swqos::{SwqosClient, SwqosType, TradeType},
trading::{common::build_transaction, MiddlewareManager},
constants::swqos::{
SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_NEXTBLOCK,
SWQOS_MIN_TIP_ZERO_SLOT,
SWQOS_MIN_TIP_TEMPORAL,
SWQOS_MIN_TIP_BLOXROUTE,
SWQOS_MIN_TIP_NODE1,
SWQOS_MIN_TIP_FLASHBLOCK,
SWQOS_MIN_TIP_BLOCKRAZOR,
SWQOS_MIN_TIP_ASTRALANE,
SWQOS_MIN_TIP_STELLIUM,
SWQOS_MIN_TIP_LIGHTSPEED,
SWQOS_MIN_TIP_SOYAS,
SWQOS_MIN_TIP_SPEEDLANDING
},
};
#[repr(align(64))]
@@ -220,6 +204,7 @@ pub async fn execute_parallel(
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
use_core_affinity: bool,
check_min_tip: bool,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
let _exec_start = Instant::now();
@@ -247,33 +232,23 @@ pub async fn execute_parallel(
with_tip || matches!(swqos_client.get_swqos_type(), SwqosType::Default)
})
.flat_map(|(i, swqos_client)| {
let swqos_type = swqos_client.get_swqos_type();
let gas_fee_strategy_configs = gas_fee_strategy.get_strategies(if is_buy {
TradeType::Buy
} else {
TradeType::Sell
});
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
};
gas_fee_strategy_configs
.into_iter()
.filter(|config| config.0.eq(&swqos_client.get_swqos_type()))
.filter(|config| {
// When tip required and not Default, filter by provider minimum tip
if with_tip && !matches!(config.0, SwqosType::Default) {
let min_tip = match config.0 {
SwqosType::Jito => SWQOS_MIN_TIP_JITO,
SwqosType::NextBlock => SWQOS_MIN_TIP_NEXTBLOCK,
SwqosType::ZeroSlot => SWQOS_MIN_TIP_ZERO_SLOT,
SwqosType::Temporal => SWQOS_MIN_TIP_TEMPORAL,
SwqosType::Bloxroute => SWQOS_MIN_TIP_BLOXROUTE,
SwqosType::Node1 => SWQOS_MIN_TIP_NODE1,
SwqosType::FlashBlock => SWQOS_MIN_TIP_FLASHBLOCK,
SwqosType::BlockRazor => SWQOS_MIN_TIP_BLOCKRAZOR,
SwqosType::Astralane => SWQOS_MIN_TIP_ASTRALANE,
SwqosType::Stellium => SWQOS_MIN_TIP_STELLIUM,
SwqosType::Lightspeed => SWQOS_MIN_TIP_LIGHTSPEED,
SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS,
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
};
.filter(move |config| config.0 == swqos_type)
.filter(move |config| {
if check_tip {
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
+8 -4
View File
@@ -11,7 +11,7 @@ use tracing::{info, trace, warn};
use crate::{
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
perf::syscall_bypass::SystemCallBypassManager,
swqos::common::poll_transaction_confirmation,
swqos::common::poll_any_transaction_confirmation,
trading::core::{
async_executor::execute_parallel,
execution::{InstructionProcessor, Prefetch},
@@ -129,6 +129,7 @@ impl TradeExecutor for GenericTradeExecutor {
if is_buy { true } else { params.with_tip },
params.gas_fee_strategy,
params.use_core_affinity,
params.check_min_tip,
)
.await;
let send_elapsed = send_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
@@ -156,10 +157,12 @@ impl TradeExecutor for GenericTradeExecutor {
),
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e))),
};
let first_sig = sigs.first().copied();
let confirm_result = if let (Some(rpc), Some(sig)) = (params.rpc.as_ref(), first_sig) {
let confirm_result = if let Some(rpc) = params.rpc.as_ref() {
if sigs.is_empty() {
(ok, sigs, err)
} else {
let confirm_start = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled()).then(Instant::now);
let poll_res = poll_transaction_confirmation(rpc, sig, true).await;
let poll_res = poll_any_transaction_confirmation(rpc, &sigs, true).await;
let confirm_elapsed = confirm_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
let dir = if is_buy { "Buy" } else { "Sell" };
@@ -171,6 +174,7 @@ impl TradeExecutor for GenericTradeExecutor {
Ok(_) => (true, sigs, None),
Err(e) => (false, sigs, Some(e)),
}
}
} else {
(ok, sigs, err)
};
+45 -1
View File
@@ -71,6 +71,8 @@ pub struct SwapParams {
pub log_enabled: bool,
/// Whether to pin parallel submit tasks to cores (from TradeConfig.use_core_affinity).
pub use_core_affinity: bool,
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
pub check_min_tip: bool,
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
pub grpc_recv_us: Option<i64>,
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
@@ -276,6 +278,7 @@ impl PumpSwapParams {
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
is_cashback_coin: bool,
) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
Self {
@@ -291,10 +294,51 @@ impl PumpSwapParams {
base_token_program,
quote_token_program,
is_mayhem_mode,
is_cashback_coin: false,
is_cashback_coin,
}
}
/// Fast-path constructor for building PumpSwap parameters directly from decoded
/// trade/event data and the accompanying instruction accounts, avoiding RPC
/// lookups and associated latency. Token program IDs should be sourced from
/// the instruction accounts themselves to respect Token Program vs Token-2022
/// differences.
///
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
/// from the event so that buy/sell instructions include the correct remaining
/// accounts for cashback.
pub fn from_trade(
pool: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
quote_token_program: Pubkey,
fee_recipient: Pubkey,
is_cashback_coin: bool,
) -> Self {
Self::new(
pool,
base_mint,
quote_mint,
pool_base_token_account,
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
quote_token_program,
fee_recipient,
is_cashback_coin,
)
}
pub async fn from_mint_by_rpc(
rpc: &SolanaRpcClient,
mint: &Pubkey,