Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9b9ddc53f | ||
|
|
d610745c7e | ||
|
|
460395e5b2 | ||
|
|
5335a4f5ff | ||
|
|
23a45e611c | ||
|
|
48b9b17ab1 | ||
|
|
0cd276d6cb | ||
|
|
82438479d3 | ||
|
|
3d062279d9 | ||
|
|
15e07e9130 | ||
|
|
6eaafde4fd |
+17
-15
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sol-trade-sdk"
|
name = "sol-trade-sdk"
|
||||||
version = "3.5.3"
|
version = "3.5.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"William <byteblock6@gmail.com>",
|
"William <byteblock6@gmail.com>",
|
||||||
@@ -20,6 +20,8 @@ members = [
|
|||||||
"examples/shared_infrastructure",
|
"examples/shared_infrastructure",
|
||||||
"examples/middleware_system",
|
"examples/middleware_system",
|
||||||
"examples/pumpswap_trading",
|
"examples/pumpswap_trading",
|
||||||
|
"examples/pumpfun_copy_trading",
|
||||||
|
"examples/pumpfun_sniper_trading",
|
||||||
"examples/bonk_sniper_trading",
|
"examples/bonk_sniper_trading",
|
||||||
"examples/bonk_copy_trading",
|
"examples/bonk_copy_trading",
|
||||||
"examples/raydium_cpmm_trading",
|
"examples/raydium_cpmm_trading",
|
||||||
@@ -43,23 +45,23 @@ perf-trace = [] # 性能追踪特性,生产环境应禁用以获得最佳性
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-client = "3.0.8"
|
solana-client = "3.1.9"
|
||||||
solana-program = "3.0.0"
|
solana-program = "3.0.0"
|
||||||
solana-rpc-client = "3.0.8"
|
solana-rpc-client = "3.1.9"
|
||||||
solana-rpc-client-api = "3.0.8"
|
solana-rpc-client-api = "3.1.9"
|
||||||
solana-transaction-status = "3.0.8"
|
solana-transaction-status = "3.1.9"
|
||||||
solana-account-decoder = "3.0.8"
|
solana-account-decoder = "3.1.9"
|
||||||
solana-hash = "3.0.0"
|
solana-hash = "3.0.0"
|
||||||
solana-entry = "3.0.8"
|
solana-entry = "3.1.9"
|
||||||
solana-rpc-client-nonce-utils = "3.0.8"
|
solana-rpc-client-nonce-utils = "3.1.9"
|
||||||
solana-perf = "3.0.8"
|
solana-perf = "3.1.9"
|
||||||
solana-metrics = "3.0.8"
|
solana-metrics = "3.1.9"
|
||||||
solana-nonce = "3.0.0"
|
solana-nonce = "3.1.0"
|
||||||
solana-address-lookup-table-interface = "3.0.0"
|
solana-address-lookup-table-interface = "3.0.1"
|
||||||
solana-compute-budget-interface = "3.0.0"
|
solana-compute-budget-interface = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.1.1", features = ["serde"] }
|
||||||
solana-transaction-status-client-types = "3.0.0"
|
solana-transaction-status-client-types = "3.1.9"
|
||||||
solana-tls-utils = "3.0.8"
|
solana-tls-utils = "3.1.9"
|
||||||
|
|
||||||
borsh = { version = "1.5.3", features = ["derive"] }
|
borsh = { version = "1.5.3", features = ["derive"] }
|
||||||
isahc = "1.7.2"
|
isahc = "1.7.2"
|
||||||
|
|||||||
@@ -60,31 +60,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🆕 What's new in 3.5.3
|
|
||||||
|
|
||||||
- **Full release**: Includes all updates (3.5.2 SWQoS perf + IDL/instruction changes). Use this version.
|
|
||||||
- **SWQoS submit latency**: Sync serialize + buffer-pool hot path; `format!` body for single/batch submit (Bloxroute); avoid status clone in confirmation polling.
|
|
||||||
- **First-submit & 5-min idle**: Immediate first ping + 30s keepalive; `pool_max_idle_per_host=4` and `pool_idle_timeout=300s` (BlockRazor, Temporal, Node1, Astralane, Stellium); ping consumes response body for connection reuse.
|
|
||||||
- **PumpFun / PumpSwap**: IDL updates (pump.json, pump_amm.json); instruction and utils updates for pumpfun and pumpswap; fast_fn and release notes.
|
|
||||||
|
|
||||||
## 🆕 What's new in 3.5.2
|
|
||||||
|
|
||||||
- **SWQoS submit latency**: Sync serialize + buffer-pool hot path; `format!` body for single/batch submit (Bloxroute); avoid status clone in confirmation polling.
|
|
||||||
- **First-submit & 5-min idle**: Immediate first ping + 30s keepalive; `pool_max_idle_per_host=4` and `pool_idle_timeout=300s` (BlockRazor, Temporal, Node1, Astralane, Stellium) so submit reuses the same connection; ping consumes response body for connection reuse.
|
|
||||||
- **Docs**: All SWQoS comments translated to English.
|
|
||||||
|
|
||||||
## 🆕 What's new in 3.5.1
|
|
||||||
|
|
||||||
- **SWQoS / executor**: Updates to common SWQoS logic and trading executor.
|
|
||||||
|
|
||||||
## 🆕 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
|
## ✨ Features
|
||||||
|
|
||||||
1. **PumpFun Trading**: Support for `buy` and `sell` operations
|
1. **PumpFun Trading**: Support for `buy` and `sell` operations
|
||||||
@@ -114,14 +89,14 @@ Add the dependency to your `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Add to your Cargo.toml
|
# Add to your Cargo.toml
|
||||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.3" }
|
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.7" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use crates.io
|
### Use crates.io
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Add to your Cargo.toml
|
# Add to your Cargo.toml
|
||||||
sol-trade-sdk = "3.5.3"
|
sol-trade-sdk = "3.5.7"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Usage Examples
|
## 🛠️ Usage Examples
|
||||||
@@ -358,6 +333,10 @@ MIT License
|
|||||||
- Telegram Group: https://t.me/fnzero_group
|
- Telegram Group: https://t.me/fnzero_group
|
||||||
- Discord: https://discord.gg/vuazbGkqQE
|
- Discord: https://discord.gg/vuazbGkqQE
|
||||||
|
|
||||||
|
## ⏱️ Timing metrics (v3.5.0+)
|
||||||
|
|
||||||
|
When `log_enabled` and SDK log are on, the executor prints `[SDK] Buy/Sell timing(...)`. **Semantics changed in v3.5.0**: `submit` is now only the send to SWQOS/RPC; `confirm` is separate; `start_to_submit` (when `grpc_recv_us` is set) is **end-to-end from gRPC event to submit**, so it is larger than in-process timings. See [docs/TIMING_METRICS.md](docs/TIMING_METRICS.md) for definitions and how to compare with older versions.
|
||||||
|
|
||||||
## ⚠️ Important Notes
|
## ⚠️ Important Notes
|
||||||
|
|
||||||
1. Test thoroughly before using on mainnet
|
1. Test thoroughly before using on mainnet
|
||||||
|
|||||||
+2
-10
@@ -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 交易**: 支持`购买`、`卖出`功能
|
1. **PumpFun 交易**: 支持`购买`、`卖出`功能
|
||||||
@@ -97,14 +89,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
# 添加到您的 Cargo.toml
|
# 添加到您的 Cargo.toml
|
||||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.3" }
|
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.7" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用 crates.io
|
### 使用 crates.io
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# 添加到您的 Cargo.toml
|
# 添加到您的 Cargo.toml
|
||||||
sol-trade-sdk = "3.5.0"
|
sol-trade-sdk = "3.5.7"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ 使用示例
|
## 🛠️ 使用示例
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
solana-streamer-sdk = "0.5.0"
|
sol-parser-sdk = "0.2.2"
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-address-lookup-table-interface = "3.0.0"
|
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
anyhow = "1.0.94"
|
|
||||||
|
|||||||
@@ -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::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::{
|
use sol_trade_sdk::{
|
||||||
common::AnyResult,
|
common::AnyResult,
|
||||||
swqos::SwqosConfig,
|
swqos::SwqosConfig,
|
||||||
@@ -9,95 +22,82 @@ use sol_trade_sdk::{
|
|||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
use solana_sdk::signature::Keypair;
|
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);
|
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
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(
|
let config = ClientConfig {
|
||||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
enable_metrics: false,
|
||||||
None,
|
connection_timeout_ms: 10000,
|
||||||
)?;
|
request_timeout_ms: 30000,
|
||||||
|
enable_tls: true,
|
||||||
let callback = create_event_callback();
|
order_mode: OrderMode::Unordered,
|
||||||
let protocols = vec![Protocol::PumpFun];
|
..Default::default()
|
||||||
// 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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to account data belonging to owner programs -> account event monitoring
|
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
.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 protocols = vec![Protocol::PumpFun];
|
||||||
let event_type_filter =
|
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||||
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
|
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(
|
let queue = grpc
|
||||||
protocols,
|
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||||
None,
|
.await?;
|
||||||
vec![transaction_filter],
|
|
||||||
vec![account_filter],
|
loop {
|
||||||
Some(event_type_filter),
|
if let Some(event) = queue.pop() {
|
||||||
None,
|
let run = match &event {
|
||||||
callback,
|
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||||
)
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
.await?;
|
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?;
|
tokio::signal::ctrl_c().await?;
|
||||||
|
|
||||||
Ok(())
|
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> {
|
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||||
println!("🚀 Initializing SolanaTrade client...");
|
println!("🚀 Initializing SolanaTrade client...");
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
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)
|
Ok(solana_trade)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PumpFun sniper trade
|
/// PumpFun copy trade: use is_cashback_coin from gRPC event (sol-parser-sdk)
|
||||||
/// This function demonstrates how to snipe a new token from a PumpFun trade event
|
async fn pumpfun_copy_trade_with_grpc(
|
||||||
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
trade_info: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||||
|
) -> AnyResult<()> {
|
||||||
println!("Testing PumpFun trading...");
|
println!("Testing PumpFun trading...");
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
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();
|
let gas_fee_strategy = GasFeeStrategy::new();
|
||||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||||
|
|
||||||
// Buy tokens
|
// is_cashback_coin from gRPC event (sol-parser-sdk parses it from trade event)
|
||||||
println!("Buying tokens from PumpFun...");
|
|
||||||
let buy_sol_amount = 100_000;
|
|
||||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
input_token_type: sol_trade_sdk::TradeTokenType::SOL,
|
input_token_type: sol_trade_sdk::TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: buy_sol_amount,
|
input_token_amount: 100_000,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
trade_info.bonding_curve,
|
trade_info.bonding_curve,
|
||||||
@@ -150,21 +149,21 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
trade_info.fee_recipient,
|
||||||
trade_info.token_program,
|
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,
|
wait_transaction_confirmed: true,
|
||||||
create_input_token_ata: false,
|
create_input_token_ata: false,
|
||||||
close_input_token_ata: false,
|
close_input_token_ata: false,
|
||||||
create_mint_ata: true,
|
create_mint_ata: true,
|
||||||
durable_nonce: None,
|
durable_nonce: None,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
// Exit program
|
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
|||||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
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,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
|||||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
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,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -635,6 +635,7 @@ async fn handle_buy_pumpfun(
|
|||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.buy(buy_params).await {
|
match client.buy(buy_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -690,6 +691,7 @@ async fn handle_buy_pumpswap(
|
|||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.buy(buy_params).await {
|
match client.buy(buy_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -744,6 +746,7 @@ async fn handle_buy_bonk(
|
|||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.buy(buy_params).await {
|
match client.buy(buy_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -802,6 +805,7 @@ async fn handle_buy_raydium_v4(
|
|||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.buy(buy_params).await {
|
match client.buy(buy_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -860,6 +864,7 @@ async fn handle_buy_raydium_cpmm(
|
|||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.buy(buy_params).await {
|
match client.buy(buy_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -1028,6 +1033,7 @@ async fn handle_sell_pumpfun(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
match client.sell(sell_params).await {
|
match client.sell(sell_params).await {
|
||||||
@@ -1086,6 +1092,7 @@ async fn handle_sell_pumpswap(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.sell(sell_params).await {
|
match client.sell(sell_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -1143,6 +1150,7 @@ async fn handle_sell_bonk(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.sell(sell_params).await {
|
match client.sell(sell_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -1203,6 +1211,7 @@ async fn handle_sell_raydium_v4(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.sell(sell_params).await {
|
match client.sell(sell_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
@@ -1263,6 +1272,7 @@ async fn handle_sell_raydium_cpmm(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
match client.sell(sell_params).await {
|
match client.sell(sell_params).await {
|
||||||
Ok((_, signature, _)) => {
|
Ok((_, signature, _)) => {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
@@ -77,6 +78,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
fixed_output_token_amount: Some(1),
|
fixed_output_token_amount: Some(1),
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use sol_trade_sdk::{
|
use sol_trade_sdk::{
|
||||||
common::{AnyResult, TradeConfig},
|
common::{AnyResult, TradeConfig},
|
||||||
swqos::{SwqosConfig, SwqosRegion},
|
swqos::SwqosConfig,
|
||||||
trading::{
|
trading::{
|
||||||
core::params::{PumpSwapParams, DexParamEnum}, factory::DexType, middleware::builtin::LoggingMiddleware,
|
core::params::{PumpSwapParams, DexParamEnum}, factory::DexType,
|
||||||
InstructionMiddleware, MiddlewareManager,
|
InstructionMiddleware, MiddlewareManager,
|
||||||
},
|
},
|
||||||
SolanaTrade, TradeTokenType,
|
SolanaTrade, TradeTokenType,
|
||||||
@@ -30,8 +30,8 @@ impl InstructionMiddleware for CustomMiddleware {
|
|||||||
fn process_protocol_instructions(
|
fn process_protocol_instructions(
|
||||||
&self,
|
&self,
|
||||||
protocol_instructions: Vec<Instruction>,
|
protocol_instructions: Vec<Instruction>,
|
||||||
protocol_name: String,
|
_protocol_name: String,
|
||||||
is_buy: bool,
|
_is_buy: bool,
|
||||||
) -> Result<Vec<Instruction>> {
|
) -> Result<Vec<Instruction>> {
|
||||||
// do anything you want here
|
// do anything you want here
|
||||||
// you can modify the instructions here
|
// you can modify the instructions here
|
||||||
@@ -41,8 +41,8 @@ impl InstructionMiddleware for CustomMiddleware {
|
|||||||
fn process_full_instructions(
|
fn process_full_instructions(
|
||||||
&self,
|
&self,
|
||||||
full_instructions: Vec<Instruction>,
|
full_instructions: Vec<Instruction>,
|
||||||
protocol_name: String,
|
_protocol_name: String,
|
||||||
is_buy: bool,
|
_is_buy: bool,
|
||||||
) -> Result<Vec<Instruction>> {
|
) -> Result<Vec<Instruction>> {
|
||||||
// do anything you want here
|
// do anything you want here
|
||||||
// you can modify the instructions here
|
// you can modify the instructions here
|
||||||
@@ -103,6 +103,7 @@ async fn test_middleware() -> AnyResult<()> {
|
|||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
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");
|
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");
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
solana-streamer-sdk = "0.5.0"
|
sol-parser-sdk = "0.2.2"
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
spl-associated-token-account = "7.0.0"
|
spl-associated-token-account = "7.0.0"
|
||||||
|
|||||||
@@ -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::common::{nonce_cache::fetch_nonce_info, TradeConfig};
|
||||||
use sol_trade_sdk::TradeTokenType;
|
use sol_trade_sdk::TradeTokenType;
|
||||||
use sol_trade_sdk::{
|
use sol_trade_sdk::{
|
||||||
@@ -16,88 +21,82 @@ use sol_trade_sdk::{
|
|||||||
};
|
};
|
||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
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);
|
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
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(
|
let config = ClientConfig {
|
||||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
enable_metrics: false,
|
||||||
None,
|
connection_timeout_ms: 10000,
|
||||||
)?;
|
request_timeout_ms: 30000,
|
||||||
|
enable_tls: true,
|
||||||
let callback = create_event_callback();
|
order_mode: OrderMode::Unordered,
|
||||||
let protocols = vec![Protocol::PumpFun];
|
..Default::default()
|
||||||
// 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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to account data belonging to owner programs -> account event monitoring
|
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
.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 protocols = vec![Protocol::PumpFun];
|
||||||
let event_type_filter =
|
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||||
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
|
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(
|
let queue = grpc
|
||||||
protocols,
|
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||||
None,
|
.await?;
|
||||||
vec![transaction_filter],
|
|
||||||
vec![account_filter],
|
loop {
|
||||||
Some(event_type_filter),
|
if let Some(event) = queue.pop() {
|
||||||
None,
|
let run = match &event {
|
||||||
callback,
|
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||||
)
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
.await?;
|
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?;
|
tokio::signal::ctrl_c().await?;
|
||||||
|
|
||||||
Ok(())
|
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> {
|
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||||
println!("🚀 Initializing SolanaTrade client...");
|
println!("🚀 Initializing SolanaTrade client...");
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
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)
|
Ok(solana_trade)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PumpFun sniper trade
|
/// PumpFun copy trade: use is_cashback_coin from gRPC event (sol-parser-sdk)
|
||||||
/// This function demonstrates how to snipe a new token from a PumpFun trade event
|
async fn pumpfun_copy_trade_with_grpc(
|
||||||
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
trade_info: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||||
|
) -> AnyResult<()> {
|
||||||
println!("Testing PumpFun trading...");
|
println!("Testing PumpFun trading...");
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
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 slippage_basis_points = Some(100);
|
||||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
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 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 durable_nonce = fetch_nonce_info(&client.infrastructure.rpc, nonce_account_str).await;
|
||||||
|
|
||||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
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);
|
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||||
|
|
||||||
// Buy tokens
|
// is_cashback_coin from gRPC event (sol-parser-sdk parses it from trade event)
|
||||||
println!("Buying tokens from PumpFun...");
|
|
||||||
let buy_sol_amount = 100_000;
|
|
||||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
input_token_type: TradeTokenType::SOL,
|
input_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: buy_sol_amount,
|
input_token_amount: 100_000,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
trade_info.bonding_curve,
|
trade_info.bonding_curve,
|
||||||
@@ -150,21 +147,21 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
trade_info.fee_recipient,
|
||||||
trade_info.token_program,
|
trade_info.token_program,
|
||||||
false, // is_cashback_coin: set from event/parser when available
|
trade_info.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
create_input_token_ata: false,
|
create_input_token_ata: false,
|
||||||
close_input_token_ata: false,
|
close_input_token_ata: false,
|
||||||
create_mint_ata: true,
|
create_mint_ata: true,
|
||||||
durable_nonce: durable_nonce,
|
durable_nonce,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
// Exit program
|
|
||||||
std::process::exit(0);
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
sol-parser-sdk = "0.2.2"
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
||||||
println!("PumpFun 跟单示例(sol-parser-sdk gRPC)...");
|
println!("PumpFun 跟单示例(sol-parser-sdk gRPC)...");
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig {
|
||||||
@@ -155,6 +154,7 @@ async fn pumpfun_copy_trade(
|
|||||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
@@ -202,6 +202,7 @@ async fn pumpfun_copy_trade(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
sol-parser-sdk = "0.2.2"
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
|
||||||
println!("PumpFun 狙击示例(sol-parser-sdk gRPC)...");
|
println!("PumpFun 狙击示例(sol-parser-sdk gRPC)...");
|
||||||
|
|
||||||
let config = ClientConfig {
|
let config = ClientConfig {
|
||||||
@@ -147,6 +146,7 @@ async fn pumpfun_sniper_trade(
|
|||||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
@@ -188,6 +188,7 @@ async fn pumpfun_sniper_trade(
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
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(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
create_output_token_ata: true,
|
create_output_token_ata: true,
|
||||||
close_output_token_ata: true,
|
close_output_token_ata: true,
|
||||||
close_mint_token_ata: false,
|
close_mint_token_ata: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
durable_nonce: None,
|
durable_nonce: None,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
|
|||||||
@@ -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::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
|
||||||
use sol_trade_sdk::common::TradeConfig;
|
use sol_trade_sdk::common::TradeConfig;
|
||||||
use sol_trade_sdk::TradeTokenType;
|
use sol_trade_sdk::TradeTokenType;
|
||||||
|
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
|
||||||
use sol_trade_sdk::{
|
use sol_trade_sdk::{
|
||||||
common::AnyResult,
|
common::AnyResult,
|
||||||
swqos::SwqosConfig,
|
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<()> {
|
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.pool,
|
||||||
trade_info.base_mint,
|
trade_info.base_mint,
|
||||||
trade_info.quote_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.base_token_program,
|
||||||
trade_info.quote_token_program,
|
trade_info.quote_token_program,
|
||||||
trade_info.protocol_fee_recipient,
|
trade_info.protocol_fee_recipient,
|
||||||
|
pool_data.is_cashback_coin,
|
||||||
);
|
);
|
||||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
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
|
|| 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 {
|
} else {
|
||||||
trade_info.base_mint
|
trade_info.base_mint
|
||||||
};
|
};
|
||||||
pumpswap_trade_with_grpc(mint, params).await?;
|
pumpswap_trade_with_grpc(&client, mint, params).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> AnyResult<()> {
|
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.pool,
|
||||||
trade_info.base_mint,
|
trade_info.base_mint,
|
||||||
trade_info.quote_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.base_token_program,
|
||||||
trade_info.quote_token_program,
|
trade_info.quote_token_program,
|
||||||
trade_info.protocol_fee_recipient,
|
trade_info.protocol_fee_recipient,
|
||||||
|
pool_data.is_cashback_coin,
|
||||||
);
|
);
|
||||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
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
|
|| 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 {
|
} else {
|
||||||
trade_info.base_mint
|
trade_info.base_mint
|
||||||
};
|
};
|
||||||
pumpswap_trade_with_grpc(mint, params).await?;
|
pumpswap_trade_with_grpc(&client, mint, params).await?;
|
||||||
Ok(())
|
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...");
|
println!("Testing PumpSwap trading...");
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
|
||||||
let slippage_basis_points = Some(500);
|
let slippage_basis_points = Some(500);
|
||||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
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(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
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,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
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(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
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,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
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(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
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,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||||
simulate: false,
|
simulate: false,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
@@ -87,6 +88,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy: gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
|
grpc_recv_us: None,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
SwqosConfig::Default(rpc_url.clone()),
|
SwqosConfig::Default(rpc_url.clone()),
|
||||||
SwqosConfig::Jito("your_uuid".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Jito("your_uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
SwqosConfig::Bloxroute("your_api_token".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)
|
// Step 1: Create shared infrastructure (expensive, do once)
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
|||||||
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
SwqosConfig::BlockRazor("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),
|
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
|
// Optional: Customize WSOL ATA and Seed optimization settings
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ pub struct TradeConfig {
|
|||||||
pub use_core_affinity: bool,
|
pub use_core_affinity: bool,
|
||||||
/// Whether to output all SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default true.
|
/// Whether to output all SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default true.
|
||||||
pub log_enabled: bool,
|
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 {
|
impl TradeConfig {
|
||||||
@@ -96,6 +98,7 @@ impl TradeConfig {
|
|||||||
use_seed_optimize: true, // default: use seed optimization
|
use_seed_optimize: true, // default: use seed optimization
|
||||||
use_core_affinity: true, // default: pin parallel submit tasks to cores
|
use_core_affinity: true, // default: pin parallel submit tasks to cores
|
||||||
log_enabled: true, // default: enable all SDK logs
|
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.use_seed_optimize = use_seed_optimize;
|
||||||
self
|
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;
|
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
|
||||||
|
|||||||
+51
-16
@@ -13,6 +13,20 @@ pub const JITO_TIP_ACCOUNTS: &[Pubkey] = &[
|
|||||||
pubkey!("3AVi9Tg9Uo68tJfuvoKvqKNWKkC5wPdSSdeBnizKZ6jT"),
|
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] = &[
|
pub const NEXTBLOCK_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||||
pubkey!("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
|
pubkey!("NextbLoCkVtMGcV47JzewQdvBpLqT9TxQFozQkN98pE"),
|
||||||
pubkey!("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
|
pubkey!("NexTbLoCkWykbLuB1NkjXgFWkX9oAtcoagQegygXXA2"),
|
||||||
@@ -227,26 +241,30 @@ pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
|
|||||||
"http://ny.flashblock.trade",
|
"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] = [
|
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
||||||
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
|
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://amsterdam.solana.blockrazor.xyz:443/sendTransaction",
|
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://tokyo.solana.blockrazor.xyz:443/sendTransaction",
|
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
|
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/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] = [
|
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
|
||||||
"http://ny.gateway.astralane.io/iris",
|
"http://ny.gateway.astralane.io/irisb",
|
||||||
"http://fr.gateway.astralane.io/iris",
|
"http://fr.gateway.astralane.io/irisb",
|
||||||
"http://ams.gateway.astralane.io/iris",
|
"http://ams.gateway.astralane.io/irisb",
|
||||||
"http://ny.gateway.astralane.io/iris",
|
"http://ny.gateway.astralane.io/irisb",
|
||||||
"http://jp.gateway.astralane.io/iris",
|
"http://jp.gateway.astralane.io/irisb",
|
||||||
"http://ny.gateway.astralane.io/iris",
|
"http://ny.gateway.astralane.io/irisb",
|
||||||
"http://lax.gateway.astralane.io/iris",
|
"http://lax.gateway.astralane.io/irisb",
|
||||||
"http://lim.gateway.astralane.io/iris",
|
"http://lim.gateway.astralane.io/irisb",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
||||||
@@ -282,6 +300,19 @@ pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [
|
|||||||
"fra.speedlanding.trade:17778",
|
"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] = [
|
||||||
|
"http://ewr-sender.helius-rpc.com/fast",
|
||||||
|
"http://fra-sender.helius-rpc.com/fast",
|
||||||
|
"http://ams-sender.helius-rpc.com/fast",
|
||||||
|
"http://slc-sender.helius-rpc.com/fast",
|
||||||
|
"http://tyo-sender.helius-rpc.com/fast",
|
||||||
|
"http://lon-sender.helius-rpc.com/fast",
|
||||||
|
"http://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_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
|
||||||
pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001;
|
pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001;
|
||||||
pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001;
|
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_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_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
|
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;
|
||||||
|
|||||||
@@ -157,6 +157,8 @@ pub struct TradingClient {
|
|||||||
pub use_core_affinity: bool,
|
pub use_core_affinity: bool,
|
||||||
/// Whether to output all SDK logs (from TradeConfig.log_enabled).
|
/// Whether to output all SDK logs (from TradeConfig.log_enabled).
|
||||||
pub log_enabled: bool,
|
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);
|
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_seed_optimize: self.use_seed_optimize,
|
||||||
use_core_affinity: self.use_core_affinity,
|
use_core_affinity: self.use_core_affinity,
|
||||||
log_enabled: self.log_enabled,
|
log_enabled: self.log_enabled,
|
||||||
|
check_min_tip: self.check_min_tip,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,6 +304,7 @@ impl TradingClient {
|
|||||||
use_seed_optimize,
|
use_seed_optimize,
|
||||||
use_core_affinity: true,
|
use_core_affinity: true,
|
||||||
log_enabled: true,
|
log_enabled: true,
|
||||||
|
check_min_tip: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,6 +344,7 @@ impl TradingClient {
|
|||||||
use_seed_optimize,
|
use_seed_optimize,
|
||||||
use_core_affinity: true,
|
use_core_affinity: true,
|
||||||
log_enabled: true,
|
log_enabled: true,
|
||||||
|
check_min_tip: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,6 +508,7 @@ impl TradingClient {
|
|||||||
use_seed_optimize: trade_config.use_seed_optimize,
|
use_seed_optimize: trade_config.use_seed_optimize,
|
||||||
use_core_affinity: trade_config.use_core_affinity,
|
use_core_affinity: trade_config.use_core_affinity,
|
||||||
log_enabled: trade_config.log_enabled,
|
log_enabled: trade_config.log_enabled,
|
||||||
|
check_min_tip: trade_config.check_min_tip,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut current = INSTANCE.lock();
|
let mut current = INSTANCE.lock();
|
||||||
@@ -636,6 +642,7 @@ impl TradingClient {
|
|||||||
simulate: params.simulate,
|
simulate: params.simulate,
|
||||||
log_enabled: self.log_enabled,
|
log_enabled: self.log_enabled,
|
||||||
use_core_affinity: self.use_core_affinity,
|
use_core_affinity: self.use_core_affinity,
|
||||||
|
check_min_tip: self.check_min_tip,
|
||||||
grpc_recv_us: params.grpc_recv_us,
|
grpc_recv_us: params.grpc_recv_us,
|
||||||
use_exact_sol_amount: params.use_exact_sol_amount,
|
use_exact_sol_amount: params.use_exact_sol_amount,
|
||||||
};
|
};
|
||||||
@@ -732,6 +739,7 @@ impl TradingClient {
|
|||||||
simulate: params.simulate,
|
simulate: params.simulate,
|
||||||
log_enabled: self.log_enabled,
|
log_enabled: self.log_enabled,
|
||||||
use_core_affinity: self.use_core_affinity,
|
use_core_affinity: self.use_core_affinity,
|
||||||
|
check_min_tip: self.check_min_tip,
|
||||||
grpc_recv_us: params.grpc_recv_us,
|
grpc_recv_us: params.grpc_recv_us,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
};
|
};
|
||||||
|
|||||||
+30
-66
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use bincode::serialize as bincode_serialize;
|
||||||
|
use solana_client::rpc_client::SerializableTransaction;
|
||||||
use solana_sdk::transaction::VersionedTransaction;
|
use solana_sdk::transaction::VersionedTransaction;
|
||||||
use crate::swqos::{SwqosType, TradeType};
|
use crate::swqos::{SwqosType, TradeType};
|
||||||
use crate::swqos::SwqosClientTrait;
|
use crate::swqos::SwqosClientTrait;
|
||||||
@@ -17,6 +16,9 @@ use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS};
|
|||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
/// Empty body for getHealth POST; avoid per-request allocation.
|
||||||
|
static PING_BODY: &[u8] = &[];
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AstralaneClient {
|
pub struct AstralaneClient {
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
@@ -50,19 +52,7 @@ impl SwqosClientTrait for AstralaneClient {
|
|||||||
impl AstralaneClient {
|
impl AstralaneClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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 client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
@@ -90,13 +80,9 @@ impl AstralaneClient {
|
|||||||
let stop_ping = self.stop_ping.clone();
|
let stop_ping = self.stop_ping.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
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));
|
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await; // first tick completes immediately → one ping at start
|
||||||
if stop_ping.load(Ordering::Relaxed) {
|
if stop_ping.load(Ordering::Relaxed) {
|
||||||
break;
|
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<()> {
|
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||||
// Build ping URL by replacing /iris with /gethealth
|
let response = http_client
|
||||||
let ping_url = if endpoint.ends_with("/iris") {
|
.post(endpoint)
|
||||||
endpoint.replace("/iris", "/gethealth")
|
.query(&[("api-key", auth_token), ("method", "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)
|
|
||||||
.timeout(Duration::from_millis(1500))
|
.timeout(Duration::from_millis(1500))
|
||||||
|
.body(PING_BODY)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
let _ = response.bytes().await;
|
let _ = response.bytes().await; // consume body so connection returns to pool
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
eprintln!("Astralane ping request returned non-success status: {}", status);
|
eprintln!("Astralane ping request returned non-success status: {}", status);
|
||||||
}
|
}
|
||||||
Ok(())
|
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<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
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!({
|
let body_bytes = bincode_serialize(transaction).map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "sendTransaction",
|
|
||||||
"params": [
|
|
||||||
content,
|
|
||||||
{ "encoding": "base64", "skipPreflight": true },
|
|
||||||
{ "mevProtect": false }
|
|
||||||
]
|
|
||||||
}))?;
|
|
||||||
|
|
||||||
// Send request with api_key header
|
let response = self.http_client
|
||||||
let response_text = self.http_client.post(&self.endpoint)
|
.post(&self.endpoint)
|
||||||
.body(request_body)
|
.query(&[("api-key", self.auth_token.as_str()), ("method", "sendTransaction")])
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/octet-stream")
|
||||||
.header("api_key", &self.auth_token)
|
.body(body_bytes)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Parse JSON response
|
let status = response.status();
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
let _ = response.bytes().await;
|
||||||
if response_json.get("result").is_some() {
|
if status.is_success() {
|
||||||
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
|
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
|
||||||
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, _error);
|
|
||||||
}
|
|
||||||
} else {
|
} 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();
|
let start_time = Instant::now();
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
|
|||||||
+26
-62
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::{Client, header::{HeaderMap, HeaderValue, CONTENT_TYPE}};
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -50,19 +49,7 @@ impl SwqosClientTrait for BlockRazorClient {
|
|||||||
impl BlockRazorClient {
|
impl BlockRazorClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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 client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
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<()> {
|
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 = endpoint.replace("/v2/sendTransaction", "/v2/health");
|
||||||
let ping_url = if endpoint.ends_with("sendTransaction") {
|
let response = http_client
|
||||||
endpoint.replace("sendTransaction", "health")
|
.post(&ping_url)
|
||||||
} else if endpoint.ends_with("/sendTransaction") {
|
.query(&[("auth", auth_token)])
|
||||||
endpoint.replace("/sendTransaction", "/health")
|
.header("Content-Type", "text/plain")
|
||||||
} 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)
|
|
||||||
.timeout(Duration::from_millis(1500))
|
.timeout(Duration::from_millis(1500))
|
||||||
|
.body(&[] as &[u8])
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
@@ -155,40 +126,33 @@ impl BlockRazorClient {
|
|||||||
Ok(())
|
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<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
// BlockRazor fast-mode request format
|
let response = self.http_client
|
||||||
let request_body = serde_json::to_string(&json!({
|
.post(&self.endpoint)
|
||||||
"transaction": content,
|
.query(&[("auth", self.auth_token.as_str())])
|
||||||
"mode": "fast"
|
.header("Content-Type", "text/plain")
|
||||||
}))?;
|
.body(content)
|
||||||
|
|
||||||
// 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)
|
|
||||||
.send()
|
.send()
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Parse JSON response
|
let status = response.status();
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
let _ = response.bytes().await;
|
||||||
|
if status.is_success() {
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
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());
|
||||||
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
|
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
|
||||||
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, _error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
} else {
|
||||||
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, response_text);
|
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 {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
+2
-15
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -46,19 +45,7 @@ impl SwqosClientTrait for FlashBlockClient {
|
|||||||
impl FlashBlockClient {
|
impl FlashBlockClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -49,19 +48,7 @@ impl SwqosClientTrait for JitoClient {
|
|||||||
impl JitoClient {
|
impl JitoClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-15
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -47,19 +46,7 @@ impl LightspeedClient {
|
|||||||
// Lightspeed endpoint should already include /lightspeed path
|
// Lightspeed endpoint should already include /lightspeed path
|
||||||
// Format: https://<tier>.rpc.solanavibestation.com/lightspeed?api_key=<key>
|
// Format: https://<tier>.rpc.solanavibestation.com/lightspeed?api_key=<key>
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+60
-1
@@ -14,6 +14,7 @@ pub mod stellium;
|
|||||||
pub mod lightspeed;
|
pub mod lightspeed;
|
||||||
pub mod soyas;
|
pub mod soyas;
|
||||||
pub mod speedlanding;
|
pub mod speedlanding;
|
||||||
|
pub mod helius;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@@ -37,7 +38,23 @@ use crate::{
|
|||||||
SWQOS_ENDPOINTS_ASTRALANE,
|
SWQOS_ENDPOINTS_ASTRALANE,
|
||||||
SWQOS_ENDPOINTS_STELLIUM,
|
SWQOS_ENDPOINTS_STELLIUM,
|
||||||
SWQOS_ENDPOINTS_SOYAS,
|
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::{
|
swqos::{
|
||||||
bloxroute::BloxrouteClient,
|
bloxroute::BloxrouteClient,
|
||||||
@@ -54,6 +71,7 @@ use crate::{
|
|||||||
lightspeed::LightspeedClient,
|
lightspeed::LightspeedClient,
|
||||||
soyas::SoyasClient,
|
soyas::SoyasClient,
|
||||||
speedlanding::SpeedlandingClient,
|
speedlanding::SpeedlandingClient,
|
||||||
|
helius::HeliusClient,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,6 +121,7 @@ pub enum SwqosType {
|
|||||||
Lightspeed,
|
Lightspeed,
|
||||||
Soyas,
|
Soyas,
|
||||||
Speedlanding,
|
Speedlanding,
|
||||||
|
Helius,
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +140,8 @@ impl SwqosType {
|
|||||||
Self::Stellium,
|
Self::Stellium,
|
||||||
Self::Lightspeed,
|
Self::Lightspeed,
|
||||||
Self::Soyas,
|
Self::Soyas,
|
||||||
|
Self::Speedlanding,
|
||||||
|
Self::Helius,
|
||||||
Self::Default,
|
Self::Default,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -134,6 +155,27 @@ pub trait SwqosClientTrait {
|
|||||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()>;
|
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()>;
|
||||||
fn get_tip_account(&self) -> Result<String>;
|
fn get_tip_account(&self) -> Result<String>;
|
||||||
fn get_swqos_type(&self) -> SwqosType;
|
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)]
|
#[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
|
/// To apply for an API key, please contact -> https://t.me/speedlanding_bot?start=0xzero
|
||||||
/// Minimum tip: 0.001 SOL
|
/// Minimum tip: 0.001 SOL
|
||||||
Speedlanding(String, SwqosRegion, Option<String>),
|
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 {
|
impl SwqosConfig {
|
||||||
@@ -199,6 +244,7 @@ impl SwqosConfig {
|
|||||||
SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed,
|
SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed,
|
||||||
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
|
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
|
||||||
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
|
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::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
|
||||||
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
||||||
SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[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(),
|
SwqosType::Default => "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -349,6 +396,18 @@ impl SwqosConfig {
|
|||||||
).await?;
|
).await?;
|
||||||
Ok(Arc::new(speedlanding_client))
|
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) => {
|
SwqosConfig::Default(endpoint) => {
|
||||||
let rpc = SolanaRpcClient::new_with_commitment(
|
let rpc = SolanaRpcClient::new_with_commitment(
|
||||||
endpoint,
|
endpoint,
|
||||||
|
|||||||
+2
-15
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -51,19 +50,7 @@ impl NextBlockClient {
|
|||||||
format!("{}/api/v2/submit", endpoint.trim_end_matches('/'))
|
format!("{}/api/v2/submit", endpoint.trim_end_matches('/'))
|
||||||
};
|
};
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-14
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -76,19 +76,7 @@ impl SwqosClientTrait for TemporalClient {
|
|||||||
impl TemporalClient {
|
impl TemporalClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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 client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
|
|||||||
+2
-15
@@ -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 rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -46,19 +45,7 @@ impl SwqosClientTrait for ZeroSlotClient {
|
|||||||
impl ZeroSlotClient {
|
impl ZeroSlotClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// 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();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,22 +15,6 @@ use crate::{
|
|||||||
common::{GasFeeStrategy, SolanaRpcClient},
|
common::{GasFeeStrategy, SolanaRpcClient},
|
||||||
swqos::{SwqosClient, SwqosType, TradeType},
|
swqos::{SwqosClient, SwqosType, TradeType},
|
||||||
trading::{common::build_transaction, MiddlewareManager},
|
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))]
|
#[repr(align(64))]
|
||||||
@@ -38,9 +22,10 @@ struct TaskResult {
|
|||||||
success: bool,
|
success: bool,
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
error: Option<anyhow::Error>,
|
error: Option<anyhow::Error>,
|
||||||
#[allow(dead_code)]
|
|
||||||
swqos_type: SwqosType,
|
swqos_type: SwqosType,
|
||||||
landed_on_chain: bool,
|
landed_on_chain: bool,
|
||||||
|
/// Microsecond timestamp when this task finished (SWQOS returned); for per-SWQOS event→submit timing.
|
||||||
|
submit_done_us: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if an error indicates the transaction landed on-chain (vs network/timeout error)
|
/// Check if an error indicates the transaction landed on-chain (vs network/timeout error)
|
||||||
@@ -103,7 +88,7 @@ impl ResultCollector {
|
|||||||
self.completed_count.fetch_add(1, Ordering::Release);
|
self.completed_count.fetch_add(1, Ordering::Release);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_success(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
async fn wait_for_success(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let timeout = std::time::Duration::from_secs(5);
|
let timeout = std::time::Duration::from_secs(5);
|
||||||
let poll_interval = std::time::Duration::from_millis(1000);
|
let poll_interval = std::time::Duration::from_millis(1000);
|
||||||
@@ -112,14 +97,16 @@ impl ResultCollector {
|
|||||||
if self.success_flag.load(Ordering::Acquire) {
|
if self.success_flag.load(Ordering::Acquire) {
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
let mut has_success = false;
|
let mut has_success = false;
|
||||||
|
let mut submit_timings = Vec::new();
|
||||||
while let Some(result) = self.results.pop() {
|
while let Some(result) = self.results.pop() {
|
||||||
signatures.push(result.signature);
|
signatures.push(result.signature);
|
||||||
|
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||||
if result.success {
|
if result.success {
|
||||||
has_success = true;
|
has_success = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if has_success && !signatures.is_empty() {
|
if has_success && !signatures.is_empty() {
|
||||||
return Some((true, signatures, None));
|
return Some((true, signatures, None, submit_timings));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,15 +115,17 @@ impl ResultCollector {
|
|||||||
if self.landed_failed_flag.load(Ordering::Acquire) {
|
if self.landed_failed_flag.load(Ordering::Acquire) {
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
let mut landed_error = None;
|
let mut landed_error = None;
|
||||||
|
let mut submit_timings = Vec::new();
|
||||||
while let Some(result) = self.results.pop() {
|
while let Some(result) = self.results.pop() {
|
||||||
signatures.push(result.signature);
|
signatures.push(result.signature);
|
||||||
|
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||||
// Prefer the error from the tx that actually landed
|
// Prefer the error from the tx that actually landed
|
||||||
if result.landed_on_chain && result.error.is_some() {
|
if result.landed_on_chain && result.error.is_some() {
|
||||||
landed_error = result.error;
|
landed_error = result.error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !signatures.is_empty() {
|
if !signatures.is_empty() {
|
||||||
return Some((false, signatures, landed_error));
|
return Some((false, signatures, landed_error, submit_timings));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,8 +134,10 @@ impl ResultCollector {
|
|||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
let mut any_success = false;
|
let mut any_success = false;
|
||||||
|
let mut submit_timings = Vec::new();
|
||||||
while let Some(result) = self.results.pop() {
|
while let Some(result) = self.results.pop() {
|
||||||
signatures.push(result.signature);
|
signatures.push(result.signature);
|
||||||
|
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||||
if result.success {
|
if result.success {
|
||||||
any_success = true;
|
any_success = true;
|
||||||
}
|
}
|
||||||
@@ -155,7 +146,7 @@ impl ResultCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !signatures.is_empty() {
|
if !signatures.is_empty() {
|
||||||
return Some((any_success, signatures, last_error));
|
return Some((any_success, signatures, last_error, submit_timings));
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -167,13 +158,15 @@ impl ResultCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_first(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
fn get_first(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
let mut has_success = false;
|
let mut has_success = false;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
|
let mut submit_timings = Vec::new();
|
||||||
|
|
||||||
while let Some(result) = self.results.pop() {
|
while let Some(result) = self.results.pop() {
|
||||||
signatures.push(result.signature);
|
signatures.push(result.signature);
|
||||||
|
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||||
if result.success {
|
if result.success {
|
||||||
has_success = true;
|
has_success = true;
|
||||||
}
|
}
|
||||||
@@ -183,17 +176,18 @@ impl ResultCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !signatures.is_empty() {
|
if !signatures.is_empty() {
|
||||||
Some((has_success, signatures, last_error))
|
Some((has_success, signatures, last_error, submit_timings))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
|
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
|
||||||
async fn wait_for_all_submitted(&self, timeout_secs: u64) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
|
||||||
|
async fn wait_for_all_submitted(&self, timeout_secs: u64) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||||
let poll_interval = std::time::Duration::from_millis(50);
|
let poll_interval = std::time::Duration::from_millis(2);
|
||||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||||
if start.elapsed() > timeout {
|
if start.elapsed() > timeout {
|
||||||
break;
|
break;
|
||||||
@@ -220,7 +214,8 @@ pub async fn execute_parallel(
|
|||||||
with_tip: bool,
|
with_tip: bool,
|
||||||
gas_fee_strategy: GasFeeStrategy,
|
gas_fee_strategy: GasFeeStrategy,
|
||||||
use_core_affinity: bool,
|
use_core_affinity: bool,
|
||||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
check_min_tip: bool,
|
||||||
|
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||||
let _exec_start = Instant::now();
|
let _exec_start = Instant::now();
|
||||||
|
|
||||||
if swqos_clients.is_empty() {
|
if swqos_clients.is_empty() {
|
||||||
@@ -247,33 +242,23 @@ pub async fn execute_parallel(
|
|||||||
with_tip || matches!(swqos_client.get_swqos_type(), SwqosType::Default)
|
with_tip || matches!(swqos_client.get_swqos_type(), SwqosType::Default)
|
||||||
})
|
})
|
||||||
.flat_map(|(i, swqos_client)| {
|
.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 {
|
let gas_fee_strategy_configs = gas_fee_strategy.get_strategies(if is_buy {
|
||||||
TradeType::Buy
|
TradeType::Buy
|
||||||
} else {
|
} else {
|
||||||
TradeType::Sell
|
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
|
gas_fee_strategy_configs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|config| config.0.eq(&swqos_client.get_swqos_type()))
|
.filter(move |config| config.0 == swqos_type)
|
||||||
.filter(|config| {
|
.filter(move |config| {
|
||||||
// When tip required and not Default, filter by provider minimum tip
|
if check_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,
|
|
||||||
};
|
|
||||||
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
|
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(
|
println!(
|
||||||
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
||||||
@@ -359,6 +344,7 @@ pub async fn execute_parallel(
|
|||||||
error: Some(e),
|
error: Some(e),
|
||||||
swqos_type,
|
swqos_type,
|
||||||
landed_on_chain: false,
|
landed_on_chain: false,
|
||||||
|
submit_done_us: crate::common::clock::now_micros(),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -400,6 +386,7 @@ pub async fn execute_parallel(
|
|||||||
error: err,
|
error: err,
|
||||||
swqos_type,
|
swqos_type,
|
||||||
landed_on_chain,
|
landed_on_chain,
|
||||||
|
submit_done_us: crate::common::clock::now_micros(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -408,15 +395,17 @@ pub async fn execute_parallel(
|
|||||||
|
|
||||||
if !wait_transaction_confirmed {
|
if !wait_transaction_confirmed {
|
||||||
const SUBMIT_TIMEOUT_SECS: u64 = 30;
|
const SUBMIT_TIMEOUT_SECS: u64 = 30;
|
||||||
let (success, signatures, last_error) = collector
|
let ret = collector
|
||||||
.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS)
|
.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS)
|
||||||
.await
|
.await
|
||||||
.unwrap_or((false, vec![], Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS))));
|
.unwrap_or((false, vec![], Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)), vec![]));
|
||||||
return Ok((success, signatures, last_error));
|
let (success, signatures, last_error, submit_timings) = ret;
|
||||||
|
return Ok((success, signatures, last_error, submit_timings));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(result) = collector.wait_for_success().await {
|
if let Some(result) = collector.wait_for_success().await {
|
||||||
Ok(result)
|
let (success, signatures, last_error, submit_timings) = result;
|
||||||
|
Ok((success, signatures, last_error, submit_timings))
|
||||||
} else {
|
} else {
|
||||||
Err(anyhow!("All transactions failed"))
|
Err(anyhow!("All transactions failed"))
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-29
@@ -69,7 +69,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
} else {
|
} else {
|
||||||
self.instruction_builder.build_sell_instructions(¶ms).await?
|
self.instruction_builder.build_sell_instructions(¶ms).await?
|
||||||
};
|
};
|
||||||
let build_elapsed = build_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
let _build_elapsed = build_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
|
|
||||||
InstructionProcessor::preprocess(&instructions)?;
|
InstructionProcessor::preprocess(&instructions)?;
|
||||||
|
|
||||||
@@ -83,7 +83,11 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
None => instructions,
|
None => instructions,
|
||||||
};
|
};
|
||||||
|
|
||||||
let before_submit_elapsed = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
let build_end_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
|
||||||
|
.then(crate::common::clock::now_micros);
|
||||||
|
let _before_submit_elapsed = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
|
let before_submit_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
|
||||||
|
.then(crate::common::clock::now_micros);
|
||||||
|
|
||||||
if params.simulate {
|
if params.simulate {
|
||||||
let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
|
let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
|
||||||
@@ -106,14 +110,20 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
|
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
println!(" [SDK] {} timing(sim) build_instructions: {:.2}ms before_submit: {:.2}ms simulate: {:.2}ms total: {:.2}ms", dir, build_elapsed.as_secs_f64() * 1000.0, before_submit_elapsed.as_secs_f64() * 1000.0, send_elapsed.as_secs_f64() * 1000.0, total_elapsed.as_secs_f64() * 1000.0);
|
if let (Some(start_us), Some(end_us)) = (timing_start_us, build_end_us) {
|
||||||
|
println!(" [SDK] {} build_instructions: {:.4} ms", dir, (end_us - start_us) as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
if let (Some(start_us), Some(end_us)) = (timing_start_us, before_submit_us) {
|
||||||
|
println!(" [SDK] {} before_submit: {:.4} ms", dir, (end_us - start_us) as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
println!(" [SDK] {} simulate (dry-run): {:.4} ms", dir, send_elapsed.as_secs_f64() * 1000.0);
|
||||||
|
println!(" [SDK] {} total: {:.4} ms", dir, total_elapsed.as_secs_f64() * 1000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
let need_confirm = params.wait_transaction_confirmed;
|
let need_confirm = params.wait_transaction_confirmed;
|
||||||
let send_start = params.log_enabled.then(Instant::now);
|
|
||||||
let result = execute_parallel(
|
let result = execute_parallel(
|
||||||
¶ms.swqos_clients,
|
¶ms.swqos_clients,
|
||||||
params.payer,
|
params.payer,
|
||||||
@@ -129,27 +139,20 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
if is_buy { true } else { params.with_tip },
|
if is_buy { true } else { params.with_tip },
|
||||||
params.gas_fee_strategy,
|
params.gas_fee_strategy,
|
||||||
params.use_core_affinity,
|
params.use_core_affinity,
|
||||||
|
params.check_min_tip,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let send_elapsed = send_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
|
||||||
|
|
||||||
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
let log_enabled = params.log_enabled && crate::common::sdk_log::sdk_log_enabled();
|
||||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
let submit_timings = if log_enabled {
|
||||||
let build_ms = build_elapsed.as_secs_f64() * 1000.0;
|
result.as_ref().ok().map(|(_, _, _, t)| t.clone()).unwrap_or_default()
|
||||||
let before_ms = before_submit_elapsed.as_secs_f64() * 1000.0;
|
} else {
|
||||||
let send_ms = send_elapsed.as_secs_f64() * 1000.0;
|
Vec::new()
|
||||||
if let Some(start_us) = timing_start_us {
|
};
|
||||||
let now_us = crate::common::clock::now_micros();
|
|
||||||
let start_to_submit_us = (now_us - start_us).max(0);
|
|
||||||
println!(" [SDK] {} timing(after_submit) build_instructions: {:.2}ms before_submit: {:.2}ms submit: {:.2}ms start_to_submit: {} μs", dir, build_ms, before_ms, send_ms, start_to_submit_us);
|
|
||||||
} else {
|
|
||||||
println!(" [SDK] {} timing(after_submit) build_instructions: {:.2}ms before_submit: {:.2}ms submit: {:.2}ms", dir, build_ms, before_ms, send_ms);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = if need_confirm {
|
let result = if need_confirm {
|
||||||
let (ok, sigs, err) = match &result {
|
let (ok, sigs, err) = match &result {
|
||||||
Ok((success, signatures, last_error)) => (
|
Ok((success, signatures, last_error, _)) => (
|
||||||
*success,
|
*success,
|
||||||
signatures.clone(),
|
signatures.clone(),
|
||||||
last_error.as_ref().map(|e| anyhow::anyhow!("{}", e)),
|
last_error.as_ref().map(|e| anyhow::anyhow!("{}", e)),
|
||||||
@@ -160,14 +163,26 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
if sigs.is_empty() {
|
if sigs.is_empty() {
|
||||||
(ok, sigs, err)
|
(ok, sigs, err)
|
||||||
} else {
|
} else {
|
||||||
let confirm_start = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled()).then(Instant::now);
|
|
||||||
let poll_res = poll_any_transaction_confirmation(rpc, &sigs, 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);
|
let confirm_done_us = log_enabled.then(crate::common::clock::now_micros);
|
||||||
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
if log_enabled {
|
||||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
let confirm_ms = confirm_elapsed.as_secs_f64() * 1000.0;
|
if let Some(start_us) = timing_start_us {
|
||||||
let total_ms = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO).as_secs_f64() * 1000.0;
|
if let Some(end_us) = build_end_us {
|
||||||
println!(" [SDK] {} timing(after_confirm) confirm: {:.2}ms total: {:.2}ms", dir, confirm_ms, total_ms);
|
println!(" [SDK] {} build_instructions: {:.4} ms", dir, (end_us - start_us) as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
if let Some(end_us) = before_submit_us {
|
||||||
|
println!(" [SDK] {} before_submit: {:.4} ms", dir, (end_us - start_us) as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
if let Some(confirm_us) = confirm_done_us {
|
||||||
|
let total_ms = (confirm_us - start_us) as f64 / 1000.0;
|
||||||
|
for (swqos_type, submit_done_us) in &submit_timings {
|
||||||
|
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||||
|
let confirmed_ms = (confirm_us - *submit_done_us).max(0) as f64 / 1000.0;
|
||||||
|
println!(" [SDK] {} {:?} submit: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms", dir, swqos_type, submit_ms, confirmed_ms, total_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
match poll_res {
|
match poll_res {
|
||||||
Ok(_) => (true, sigs, None),
|
Ok(_) => (true, sigs, None),
|
||||||
@@ -179,12 +194,22 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
};
|
};
|
||||||
Ok(confirm_result)
|
Ok(confirm_result)
|
||||||
} else {
|
} else {
|
||||||
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
if log_enabled {
|
||||||
let total_ms = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO).as_secs_f64() * 1000.0;
|
|
||||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
println!(" [SDK] {} timing total: {:.2}ms", dir, total_ms);
|
if let Some(start_us) = timing_start_us {
|
||||||
|
if let Some(end_us) = build_end_us {
|
||||||
|
println!(" [SDK] {} build_instructions: {:.4} ms", dir, (end_us - start_us) as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
if let Some(end_us) = before_submit_us {
|
||||||
|
println!(" [SDK] {} before_submit: {:.4} ms", dir, (end_us - start_us) as f64 / 1000.0);
|
||||||
|
}
|
||||||
|
for (swqos_type, submit_done_us) in &submit_timings {
|
||||||
|
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||||
|
println!(" [SDK] {} {:?} submit: {:.4} ms, confirmed: -, total: {:.4} ms", dir, swqos_type, submit_ms, submit_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result
|
result.map(|(a, b, c, _)| (a, b, c))
|
||||||
};
|
};
|
||||||
|
|
||||||
result
|
result
|
||||||
@@ -303,3 +328,49 @@ async fn simulate_transaction(
|
|||||||
|
|
||||||
Ok((true, vec![signature], None))
|
Ok((true, vec![signature], None))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::swqos::SwqosType;
|
||||||
|
|
||||||
|
/// 运行 `cargo test -p sol-trade-sdk log_timing_preview -- --nocapture` 查看日志打印效果
|
||||||
|
#[test]
|
||||||
|
fn log_timing_preview() {
|
||||||
|
let dir = "Buy";
|
||||||
|
let build_ms = 12.34;
|
||||||
|
let before_submit_ms = 15.67;
|
||||||
|
println!("\n--- 1. 构建指令耗时 / 提交前耗时(各打印一次,统一 ms,保留 4 位小数)---\n");
|
||||||
|
println!(" [SDK] {} build_instructions: {:.4} ms", dir, build_ms);
|
||||||
|
println!(" [SDK] {} before_submit: {:.4} ms", dir, before_submit_ms);
|
||||||
|
|
||||||
|
println!("\n--- 2. 每个 SWQOS 独立耗时:submit=起点→该通道返回, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
|
||||||
|
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
|
||||||
|
(SwqosType::Jito, 45.12, 83.38, 128.50),
|
||||||
|
(SwqosType::Helius, 52.30, 76.20, 128.50),
|
||||||
|
(SwqosType::ZeroSlot, 48.90, 79.60, 128.50),
|
||||||
|
] {
|
||||||
|
println!(
|
||||||
|
" [SDK] {} {:?} submit: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||||
|
dir, swqos_type, submit_ms, confirmed_ms, total_ms
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n--- 3. 不等待链上确认时:每行 total = 该通道 submit 耗时(独立)---\n");
|
||||||
|
for (swqos_type, submit_ms, total_ms) in [
|
||||||
|
(SwqosType::Jito, 44.20, 44.20),
|
||||||
|
(SwqosType::Helius, 51.80, 51.80),
|
||||||
|
] {
|
||||||
|
println!(
|
||||||
|
" [SDK] {} {:?} submit: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||||
|
dir, swqos_type, submit_ms, total_ms
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("\n--- 4. Simulate 模式(build/before_submit 仍从 grpc_recv_us 起算)---\n");
|
||||||
|
println!(" [SDK] {} build_instructions: {:.4} ms", dir, build_ms);
|
||||||
|
println!(" [SDK] {} before_submit: {:.4} ms", dir, before_submit_ms);
|
||||||
|
println!(" [SDK] {} simulate (dry-run): {:.4} ms", dir, 8.50);
|
||||||
|
println!(" [SDK] {} total: {:.4} ms", dir, 36.51);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -71,6 +71,8 @@ pub struct SwapParams {
|
|||||||
pub log_enabled: bool,
|
pub log_enabled: bool,
|
||||||
/// Whether to pin parallel submit tasks to cores (from TradeConfig.use_core_affinity).
|
/// Whether to pin parallel submit tasks to cores (from TradeConfig.use_core_affinity).
|
||||||
pub use_core_affinity: bool,
|
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.
|
/// 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>,
|
pub grpc_recv_us: Option<i64>,
|
||||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
/// 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,
|
base_token_program: Pubkey,
|
||||||
quote_token_program: Pubkey,
|
quote_token_program: Pubkey,
|
||||||
fee_recipient: Pubkey,
|
fee_recipient: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||||
Self {
|
Self {
|
||||||
@@ -291,10 +294,51 @@ impl PumpSwapParams {
|
|||||||
base_token_program,
|
base_token_program,
|
||||||
quote_token_program,
|
quote_token_program,
|
||||||
is_mayhem_mode,
|
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(
|
pub async fn from_mint_by_rpc(
|
||||||
rpc: &SolanaRpcClient,
|
rpc: &SolanaRpcClient,
|
||||||
mint: &Pubkey,
|
mint: &Pubkey,
|
||||||
|
|||||||
Reference in New Issue
Block a user