Compare commits

...

16 Commits

Author SHA1 Message Date
Wood d52503b8de Release v4.0.1: Fix BlockRazor gRPC endpoints and set gRPC as default transport
## Changes since v4.0.0

### Bug Fixes
- Fixed BlockRazor gRPC endpoints to use correct gRPC service URLs
- Set gRPC as the default transport layer for BlockRazor with HTTP fallback

This release improves the reliability and performance of BlockRazor integration by prioritizing gRPC communication while maintaining HTTP fallback compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 01:34:48 +08:00
Wood 9e53b7694c Fix BlockRazor gRPC endpoints and set gRPC as default transport
- Add SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC with correct gRPC endpoints
- Add Grpc variant to SwqosTransport enum
- Add get_endpoint_with_transport() method for transport-aware endpoint selection
- Change BlockRazor default: transport=None now uses gRPC (was HTTP)
- Require explicit Some(SwqosTransport::Http) for HTTP fallback

This fixes the issue where BlockRazor was incorrectly using HTTP endpoints
when gRPC was intended. Users can now use gRPC by default or explicitly
specify HTTP transport when needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 01:17:06 +08:00
Wood 07e45d136f Release v4.0.0: SWQoS transport improvements and Binary-Tx response handling
Major changes:
- Switch BlockRazor default transport from gRPC to HTTP to avoid FRAME_SIZE_ERROR
  - gRPC mode still available via explicit SwqosTransport configuration
- Fix ZeroSlot Binary-Tx JSON-RPC 2.0 response parsing
  - Properly handle success responses with "result" field
  - Properly handle error responses with "error" field containing code and message
- Update version to 4.0.0
- Update README files to reflect new version

Technical details:
- BlockRazor: HTTP mode is now the default (SwqosTransport::Http or None)
- ZeroSlot: Parse JSON-RPC 2.0 format responses instead of plain text
- Proto code: Pre-generated gRPC code for BlockRazor (no protoc required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 21:32:23 +08:00
Wood 20d053bab3 Add gRPC support for BlockRazor with HTTP fallback (gRPC by default)
- Add BlockRazorBackend enum to support both gRPC and HTTP transport
- Default to gRPC for better performance, HTTP available via explicit selection
- Update SwqosConfig::BlockRazor to accept optional SwqosTransport parameter
- Implement keep-alive ping task for both gRPC and HTTP backends
- Follow same backend pattern as Astralane (Quic/Http) and Node1 (Quic/Http)
- Manual gRPC client implementation to avoid proto compilation issues

Usage:
- Default: BlockRazorClient::new() uses gRPC
- HTTP: BlockRazorClient::new_http() for HTTP transport
- Config: SwqosConfig::BlockRazor(token, region, url, None) for gRPC
- Config: SwqosConfig::BlockRazor(token, region, url, Some(Http)) for HTTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 18:01:50 +08:00
Wood 5d921f23ff Improve ZeroSlot client: add HTTP keep-alive ping and switch to Binary-Tx
- Add HTTP keep-alive ping task (30s interval) using free getHealth method
- Switch from JSON-RPC to faster Binary-Tx endpoint (/txb) for transaction submission
- Send raw binary transaction bytes directly to avoid encoding/decoding overhead
- Update response handling for Binary-Tx plain text status codes (200/403/419/500)
- Follow same keep-alive pattern as BlockRazor, Stellium, and Astralane clients

These changes reduce first-submit cold start latency and overall transaction submission time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 17:29:14 +08:00
Wood a187126554 Add 9 new Astralane tip wallets for improved routing and reduced write-lock contention
Made-with: Cursor
2026-03-18 14:40:06 +08:00
Wood 6ea8c27824 docs: sync README and README_CN version to 3.6.5
Made-with: Cursor
2026-03-17 17:51:09 +08:00
Wood 8c4e3ee0c5 Release v3.6.5: SWQOS core affinity and recommended sender thread indices
- Add TradeConfig::with_swqos_cores_from_end(bool) to use last N CPU cores for SWQOS, reducing contention with main thread and default tokio workers.
- Add recommended_sender_thread_core_indices(swqos_count) to get the same last-N core indices for with_dedicated_sender_threads (recommended combo for lower latency).
- Document core affinity and latency in async_executor and with_dedicated_sender_threads.

Made-with: Cursor
2026-03-17 17:19:04 +08:00
Wood 624b1843a2 Improve SDK and SWQOS logging: alignment, errors, duration format
- Rename wait_transaction_confirmed to wait_tx_confirmed (params, lib, docs, examples)
- SDK timing: use [SDK][provider] style with fixed-width labels; add newline before block
- Move print_sdk_timing_block to common::sdk_log; unify SWQOS_LABEL_WIDTH
- SWQOS submitted/failed: aligned [Provider] labels via log_swqos_submitted/submission_failed
- Extract short error message from JSON (message/data) or quoted string; prefix with 'error: '
- Format elapsed as 'X.XXXX ms' or 'X.XXXX µs' (4 decimals, space before unit); use comma before error
- Add SwqosType::as_str() for zero-allocation log labels; only parse JSON when input starts with '{'

Made-with: Cursor
2026-03-17 13:11:33 +08:00
Wood 7d2ecd57e9 Release v3.6.4: bump version, update README (EN/CN), ensure all examples build
- Bump version to 3.6.4 in Cargo.toml
- Update version references in README.md and README_CN.md
- Add RELEASE_v3.6.4.md with English release notes
- All workspace examples verified to compile (cargo build --workspace)
- Remove RELEASE_v3.6.3.md and docs/ASYNC_EXECUTOR_REVIEW.md
- Minor updates in types, lib, transaction_builder, async_executor, executor, params

Made-with: Cursor
2026-03-17 04:52:27 +08:00
Wood 667d6d2c5b Release v3.6.3: bump version, update README and add release notes
Made-with: Cursor
2026-03-17 03:44:42 +08:00
Wood e957bc4bee Parallel multi-SWQoS submit: builder pool and executor tweaks
- transaction_pool: add PARALLEL_SENDER_COUNT (18), ensure prefill >= 18 so
  multi-channel build never serializes; prefill 64 with max(PREFILL, 18)
- async_executor: document that pool prefill must match sender thread count
- executor: do not sort submit_timings (avoids any extra work); comment that
  log order is completion order
- docs: add ASYNC_EXECUTOR_REVIEW.md
- remove RELEASE_NOTES_v3.6.2.md; minor example/types/lib/params updates

Made-with: Cursor
2026-03-17 03:32:59 +08:00
Wood 274636abc5 Merge pull request #91 from hookenful/style/rust-fmt
style/run rustfmt
2026-03-15 22:25:00 +08:00
hookenful 2abcf3839e style: run rustfmt 2026-03-14 18:16:55 +02:00
Wood 0ec826fcbd Merge pull request #90 from HelvetiCrypt/fix/swqos-worker-busy-spin
Fix SWQOS worker pool busy-spin causing high CPU at idle
2026-03-13 13:10:48 +08:00
vibes 6607d276db Fix SWQOS worker pool busy-spin causing 300%+ CPU at idle
The 32 worker tasks in swqos_worker_loop use yield_now().await when
the queue is empty, which busy-spins across all tokio threads. Replace
with tokio::sync::Notify so workers sleep until jobs are enqueued.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 00:37:14 +00:00
88 changed files with 3348 additions and 1696 deletions
+9 -27
View File
@@ -1,30 +1,12 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Proto 生成工具和生成的代码(用户不需要)
/proto/gen/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# 预生成的 proto Rust 代码(提交到仓库,但用户不应修改)
# /src/swqos/pb/serverpb.rs <- 这个文件已经提交,用户不应修改
# Build artifacts
/target/
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.cargo/
tmp_*.rs
tmp_*.log
.claude/
.serena/
# Proto sources
/proto/
+5 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "3.6.2"
version = "4.0.1"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -135,6 +135,10 @@ incremental = true # 增量编译 - 大幅加速重新编译
opt-level = 1 # 开发时适度优化
overflow-checks = true # 开发时启用溢出检查
# 🚀 构建依赖
# 注意:proto 代码已预生成在 src/swqos/pb/serverpb.rs
# 开发者如需重新生成代码,请运行 gen_proto 目录下的工具
# 🚀 性能关键依赖的特殊优化
[profile.release.package.solana-sdk]
opt-level = 3
+2 -2
View File
@@ -90,14 +90,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.6.2" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.1" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "3.6.2"
sol-trade-sdk = "4.0.1"
```
## 🛠️ Usage Examples
+2 -2
View File
@@ -90,14 +90,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.6.2" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.1" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "3.6.2"
sol-trade-sdk = "4.0.1"
```
## 🛠️ 使用示例
-16
View File
@@ -1,16 +0,0 @@
## sol-trade-sdk v3.6.2
### Changes
- **Node1 QUIC support** — Node1 SWQOS can use QUIC transport: `SwqosConfig::Node1(api_token, region, custom_url, Some(SwqosTransport::Quic))`. Uses UUID auth on first bi stream, one bi stream per transaction, bincode-serialized `VersionedTransaction`. Region endpoints: `SWQOS_ENDPOINTS_NODE1_QUIC` (ny, fra, ams, lon, tk). New dependency: `uuid`.
- **Speedlanding QUIC reliability** — Proactive connection check before send (`ensure_connected()`); 5s connect and send timeouts; reconnect uses `lock().await` so concurrent senders wait for the new connection instead of failing on `try_lock()`; TLS SNI is derived from the endpoint host (e.g. `nyc.speedlanding.trade`) with fallback to `speed-landing` for IP or unknown host. Addresses user reports of transactions failing to send.
### Crates.io
```toml
sol-trade-sdk = "3.6.2"
```
### Repository
- **Tag:** [v3.6.2](https://github.com/0xfnzero/sol-trade-sdk/releases/tag/v3.6.2)
+3 -3
View File
@@ -30,7 +30,7 @@ The `TradeBuyParams` struct contains all parameters required for executing buy o
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Address lookup table for transaction optimization |
| `wait_transaction_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `create_input_token_ata` | `bool` | ✅ | Whether to create input token Associated Token Account |
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
| `create_mint_ata` | `bool` | ✅ | Whether to create token mint ATA |
@@ -62,7 +62,7 @@ The `TradeSellParams` struct contains all parameters required for executing sell
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | Address lookup table for transaction optimization |
| `wait_transaction_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
| `create_output_token_ata` | `bool` | ✅ | Whether to create output token Associated Token Account |
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
@@ -88,7 +88,7 @@ These parameters are essential for defining the basic trading operation:
These parameters control how the transaction is processed:
- **slippage_basis_points**: Controls acceptable price slippage
- **wait_transaction_confirmed**: Controls whether to wait for confirmation
- **wait_tx_confirmed**: Controls whether to wait for confirmation
### 🔧 Account Management Parameters
+3 -3
View File
@@ -30,7 +30,7 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | 用于交易优化的地址查找表 |
| `wait_transaction_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_input_token_ata` | `bool` | ✅ | 是否创建输入代币关联代币账户 |
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
| `create_mint_ata` | `bool` | ✅ | 是否创建代币 mint ATA |
@@ -62,7 +62,7 @@
| 参数 | 类型 | 必需 | 描述 |
|------|------|------|------|
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 用于交易优化的地址查找表 |
| `wait_transaction_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
| `create_output_token_ata` | `bool` | ✅ | 是否创建输出代币关联代币账户 |
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
@@ -88,7 +88,7 @@
这些参数控制交易的处理方式:
- **slippage_basis_points**: 控制可接受的价格滑点
- **wait_transaction_confirmed**: 控制是否等待确认
- **wait_tx_confirmed**: 控制是否等待确认
### 🔧 账户管理参数
+10 -3
View File
@@ -16,7 +16,10 @@ use sol_trade_sdk::common::{GasFeeStrategy, TradeConfig};
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -63,7 +66,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
loop {
if let Some(event) = queue.pop() {
let run = match &event {
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
DexEvent::PumpFunBuy(e)
| DexEvent::PumpFunSell(e)
| DexEvent::PumpFunBuyExactSolIn(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
@@ -123,7 +128,9 @@ async fn pumpfun_copy_trade_with_grpc(
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
let address_lookup_table_account =
fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key).await.ok();
fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key)
.await
.ok();
let gas_fee_strategy = GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
+5 -9
View File
@@ -10,7 +10,10 @@ use sol_trade_sdk::common::{
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{BonkParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{BonkParams, DexParamEnum},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -127,14 +130,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
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
println!("Buying tokens from Bonk...");
+5 -9
View File
@@ -3,7 +3,10 @@ use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{BonkParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{BonkParams, DexParamEnum},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -95,14 +98,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(
150000,
150000,
500000,
500000,
0.001,
0.001,
);
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
let token_type = if trade_info.quote_token_mint == sol_trade_sdk::constants::USD1_TOKEN_ACCOUNT
{
+17 -8
View File
@@ -12,7 +12,8 @@ use sol_trade_sdk::{
swqos::SwqosConfig,
trading::{
core::params::{
BonkParams, PumpFunParams, PumpSwapParams, RaydiumAmmV4Params, RaydiumCpmmParams, DexParamEnum,
BonkParams, DexParamEnum, PumpFunParams, PumpSwapParams, RaydiumAmmV4Params,
RaydiumCpmmParams,
},
factory::DexType,
},
@@ -721,7 +722,8 @@ async fn handle_buy_bonk(
println!(" Slippage: {}%", slippage.unwrap());
}
let mint_pubkey = Pubkey::from_str(mint)?;
let param = BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
let param =
BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let sol_lamports = sol_str_to_lamports(sol_amount.to_string().as_str()).unwrap();
@@ -780,7 +782,8 @@ async fn handle_buy_raydium_v4(
let mint_pubkey = Pubkey::from_str(mint)?;
let amm_pubkey = Pubkey::from_str(amm)?;
let param = RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
let param =
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let sol_lamports = sol_str_to_lamports(sol_amount.to_string().as_str()).unwrap();
@@ -839,7 +842,9 @@ async fn handle_buy_raydium_cpmm(
let mint_pubkey = Pubkey::from_str(mint)?;
let pool_pubkey = Pubkey::from_str(pool_address)?;
let param = RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey).await?;
let param =
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey)
.await?;
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let sol_lamports = sol_str_to_lamports(sol_amount.to_string().as_str()).unwrap();
@@ -1097,7 +1102,7 @@ async fn handle_sell_pumpswap(
match client.sell(sell_params).await {
Ok((_, signature, _)) => {
println!(" ✅ Successfully sold tokens from PumpSwap!");
println!(" ✅ Transaction Signature: {:?}", signature);
println!(" ✅ Transaction Signature: {:?}", signature);
}
Err(e) => {
println!(" ❌ Failed to sell tokens from PumpSwap: {}", e);
@@ -1126,7 +1131,8 @@ async fn handle_sell_bonk(
}
let client = initialize_real_client().await?;
let mint_pubkey = Pubkey::from_str(mint)?;
let param = BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
let param =
BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
@@ -1187,7 +1193,8 @@ async fn handle_sell_raydium_v4(
let client = initialize_real_client().await?;
let amm_pubkey = Pubkey::from_str(amm)?;
let mint_pubkey = Pubkey::from_str(mint)?;
let param = RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
let param =
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
@@ -1248,7 +1255,9 @@ async fn handle_sell_raydium_cpmm(
let client = initialize_real_client().await?;
let pool_pubkey = Pubkey::from_str(pool_address)?;
let mint_pubkey = Pubkey::from_str(mint)?;
let param = RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey).await?;
let param =
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey)
.await?;
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
@@ -1,7 +1,13 @@
use sol_trade_sdk::{
SolanaTrade, TradeTokenType, common::{
AnyResult, TradeConfig, fast_fn::get_associated_token_address_with_program_id_fast_use_seed
}, swqos::SwqosConfig, trading::{core::params::{MeteoraDammV2Params, DexParamEnum}, factory::DexType}
common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
},
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, MeteoraDammV2Params},
factory::DexType,
},
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
@@ -32,7 +38,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::MeteoraDammV2(
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
@@ -54,7 +61,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM;
let account = get_associated_token_address_with_program_id_fast_use_seed(&payer, &mint_pubkey, &program_id, client.use_seed_optimize);
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
println!("Token balance: {}", amount_token);
@@ -67,7 +79,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
recent_blockhash: Some(recent_blockhash),
with_tip: false,
extension_params: DexParamEnum::MeteoraDammV2(
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
+6 -4
View File
@@ -3,7 +3,8 @@ use sol_trade_sdk::{
common::{AnyResult, TradeConfig},
swqos::SwqosConfig,
trading::{
core::params::{PumpSwapParams, DexParamEnum}, factory::DexType,
core::params::{DexParamEnum, PumpSwapParams},
factory::DexType,
InstructionMiddleware, MiddlewareManager,
},
SolanaTrade, TradeTokenType,
@@ -30,7 +31,7 @@ impl InstructionMiddleware for CustomMiddleware {
fn process_protocol_instructions(
&self,
protocol_instructions: Vec<Instruction>,
_protocol_name: String,
_protocol_name: &str,
_is_buy: bool,
) -> Result<Vec<Instruction>> {
// do anything you want here
@@ -41,7 +42,7 @@ impl InstructionMiddleware for CustomMiddleware {
fn process_full_instructions(
&self,
full_instructions: Vec<Instruction>,
_protocol_name: String,
_protocol_name: &str,
_is_buy: bool,
) -> Result<Vec<Instruction>> {
// do anything you want here
@@ -91,7 +92,8 @@ async fn test_middleware() -> AnyResult<()> {
slippage_basis_points: slippage_basis_points,
recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpSwap(
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_address).await?,
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_address)
.await?,
),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
+7 -2
View File
@@ -16,7 +16,10 @@ use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -62,7 +65,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
loop {
if let Some(event) = queue.pop() {
let run = match &event {
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
DexEvent::PumpFunBuy(e)
| DexEvent::PumpFunSell(e)
| DexEvent::PumpFunBuyExactSolIn(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
+14 -7
View File
@@ -2,7 +2,10 @@
//!
//! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use sol_parser_sdk::grpc::{
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
@@ -16,7 +19,10 @@ use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -65,7 +71,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
loop {
if let Some(event) = queue.pop() {
let run = match &event {
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
DexEvent::PumpFunBuy(e)
| DexEvent::PumpFunSell(e)
| DexEvent::PumpFunBuyExactSolIn(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
@@ -102,16 +110,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
}
async fn pumpfun_copy_trade(
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
) -> AnyResult<()> {
async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let mint_pubkey = e.mint;
let slippage_basis_points = Some(100u64);
+11 -6
View File
@@ -3,7 +3,10 @@
//! 监听创建者首次买入(Create 后同笔/首笔 Buyis_created_buy == true),
//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use sol_parser_sdk::grpc::{
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
@@ -16,7 +19,10 @@ use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, PumpFunParams},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -94,16 +100,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let rpc_url = std::env::var("RPC_URL")
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
}
async fn pumpfun_sniper_trade(
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
) -> AnyResult<()> {
async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
let client = create_solana_trade_client().await?;
let mint_pubkey = e.mint;
let slippage_basis_points = Some(300u64);
+15 -4
View File
@@ -1,7 +1,13 @@
use sol_trade_sdk::{
SolanaTrade, TradeTokenType, common::{
AnyResult, TradeConfig, fast_fn::get_associated_token_address_with_program_id_fast_use_seed
}, swqos::SwqosConfig, trading::{core::params::{PumpSwapParams, DexParamEnum}, factory::DexType}
common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
},
swqos::SwqosConfig,
trading::{
core::params::{DexParamEnum, PumpSwapParams},
factory::DexType,
},
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
@@ -54,7 +60,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey();
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM_2022;
let account = get_associated_token_address_with_program_id_fast_use_seed(&payer, &mint_pubkey, &program_id, client.use_seed_optimize);
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let sell_params = sol_trade_sdk::TradeSellParams {
+11 -3
View File
@@ -1,11 +1,14 @@
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{PumpSwapParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, PumpSwapParams},
factory::DexType,
},
SolanaTrade,
};
use solana_commitment_config::CommitmentConfig;
@@ -244,7 +247,12 @@ async fn pumpswap_trade_with_grpc(
} else {
params.quote_token_program
};
let account = get_associated_token_address_with_program_id_fast_use_seed(&payer, &mint_pubkey, &program_id, client.use_seed_optimize);
let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer,
&mint_pubkey,
&program_id,
client.use_seed_optimize,
);
let balance = rpc.get_token_account_balance(&account).await?;
let amount_token = balance.amount.parse::<u64>().unwrap();
let sell_params = sol_trade_sdk::TradeSellParams {
+14 -12
View File
@@ -2,7 +2,10 @@ use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id
use sol_trade_sdk::{
common::AnyResult,
swqos::SwqosConfig,
trading::{core::params::{RaydiumAmmV4Params, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, RaydiumAmmV4Params},
factory::DexType,
},
SolanaTrade,
};
use sol_trade_sdk::{
@@ -122,8 +125,12 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let amm_info = fetch_amm_info(&client.infrastructure.rpc, trade_info.amm).await?;
let (coin_reserve, pc_reserve) =
get_multi_token_balances(&client.infrastructure.rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
let (coin_reserve, pc_reserve) = get_multi_token_balances(
&client.infrastructure.rpc,
&amm_info.token_coin,
&amm_info.token_pc,
)
.await?;
let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
{
@@ -142,14 +149,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
);
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
println!("Buying tokens from Raydium_amm_v4...");
@@ -194,7 +194,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
let amount_token = balance.amount.parse::<u64>().unwrap();
println!("Selling {} tokens", amount_token);
let params = RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm).await?;
let params =
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm)
.await?;
let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::RaydiumAmmV4,
output_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
+13 -7
View File
@@ -1,7 +1,7 @@
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::constants::{WSOL_TOKEN_ACCOUNT, USDC_TOKEN_ACCOUNT};
use sol_trade_sdk::trading::core::params::{RaydiumCpmmParams, DexParamEnum};
use sol_trade_sdk::constants::{USDC_TOKEN_ACCOUNT, WSOL_TOKEN_ACCOUNT};
use sol_trade_sdk::trading::core::params::{DexParamEnum, RaydiumCpmmParams};
use sol_trade_sdk::trading::factory::DexType;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{common::AnyResult, swqos::SwqosConfig, SolanaTrade};
@@ -132,9 +132,12 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
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);
let buy_params =
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &trade_info.pool_state).await?;
let buy_params = RaydiumCpmmParams::from_pool_address_by_rpc(
&client.infrastructure.rpc,
&trade_info.pool_state,
)
.await?;
let is_wsol = trade_info.input_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| trade_info.output_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
@@ -173,8 +176,11 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap();
let sell_params =
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &trade_info.pool_state).await?;
let sell_params = RaydiumCpmmParams::from_pool_address_by_rpc(
&client.infrastructure.rpc,
&trade_info.pool_state,
)
.await?;
println!("Selling {} tokens", amount_token);
let sell_params = sol_trade_sdk::TradeSellParams {
+4 -1
View File
@@ -3,7 +3,10 @@ use sol_trade_sdk::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
},
swqos::SwqosConfig,
trading::{core::params::{PumpSwapParams, DexParamEnum}, factory::DexType},
trading::{
core::params::{DexParamEnum, PumpSwapParams},
factory::DexType,
},
SolanaTrade, TradeTokenType,
};
use solana_commitment_config::CommitmentConfig;
+10 -6
View File
@@ -48,17 +48,21 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
SwqosConfig::FlashBlock("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Astralane("your_api_token".to_string(), SwqosRegion::Frankfurt, None, Some(SwqosTransport::Quic)), // QUIC; use None for HTTP
SwqosConfig::Astralane(
"your_api_token".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
), // QUIC; use None for HTTP
// 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
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
.with_wsol_ata_config(
true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup
true, // use_seed_optimize: Enable seed optimization for all ATA operations
);
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment).with_wsol_ata_config(
true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup
true, // use_seed_optimize: Enable seed optimization for all ATA operations
);
// Creates new infrastructure internally
let client = TradingClient::new(Arc::new(payer), trade_config).await;
+4 -1
View File
@@ -38,7 +38,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Example 2: Unwrap half of the WSOL back to SOL using seed account
println!("\n🔄 Example 2: Unwrapping half of WSOL back to SOL using seed account");
let unwrap_amount = wrap_amount / 2; // Half of the wrapped amount
println!("Unwrapping {} lamports (0.0005 SOL) back to SOL using seed account...", unwrap_amount);
println!(
"Unwrapping {} lamports (0.0005 SOL) back to SOL using seed account...",
unwrap_amount
);
match solana_trade.wrap_wsol_to_sol(unwrap_amount).await {
Ok(signature) => {
+6 -6
View File
@@ -46,7 +46,10 @@ static INSTRUCTION_CACHE: Lazy<DashMap<InstructionCacheKey, Arc<Vec<Instruction>
/// Get cached instruction, compute and cache if not exists (lock-free)
/// 🚀 返回 Arc 避免每次调用克隆整个 Vec
#[inline]
pub fn get_cached_instructions<F>(cache_key: InstructionCacheKey, compute_fn: F) -> Arc<Vec<Instruction>>
pub fn get_cached_instructions<F>(
cache_key: InstructionCacheKey,
compute_fn: F,
) -> Arc<Vec<Instruction>>
where
F: FnOnce() -> Vec<Instruction>,
{
@@ -63,10 +66,7 @@ where
};
// Lock-free cache lookup with entry API
INSTRUCTION_CACHE
.entry(cache_key)
.or_insert_with(|| Arc::new(compute_fn()))
.clone()
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
}
// --------------------- Associated Token Account ---------------------
@@ -141,7 +141,7 @@ pub fn _create_associated_token_account_idempotent_fast(
}]
})
};
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
Arc::try_unwrap(arc_instructions).unwrap_or_else(|arc| (*arc).clone())
}
+4 -11
View File
@@ -2,9 +2,9 @@
//!
//! 使用 syscall_bypass 提供的快速时间戳避免频繁的系统调用
use std::time::{Duration, Instant};
use once_cell::sync::Lazy;
use crate::perf::syscall_bypass::SystemCallBypassManager;
use once_cell::sync::Lazy;
use std::time::{Duration, Instant};
/// 全局快速时间提供器
static FAST_TIMER: Lazy<FastTimer> = Lazy::new(|| FastTimer::new());
@@ -26,11 +26,7 @@ impl FastTimer {
let base_instant = Instant::now();
let base_nanos = bypass_manager.fast_timestamp_nanos();
Self {
bypass_manager,
_base_instant: base_instant,
_base_nanos: base_nanos,
}
Self { bypass_manager, _base_instant: base_instant, _base_nanos: base_nanos }
}
/// 🚀 获取当前时间戳(纳秒) - 使用快速系统调用绕过
@@ -107,10 +103,7 @@ impl FastStopwatch {
/// 创建并启动计时器
#[inline(always)]
pub fn start(label: &'static str) -> Self {
Self {
start_nanos: fast_now_nanos(),
label,
}
Self { start_nanos: fast_now_nanos(), label }
}
/// 获取已耗时(纳秒)
+1 -1
View File
@@ -2,11 +2,11 @@ pub mod address_lookup;
pub mod bonding_curve;
pub mod clock;
pub mod fast_fn;
pub mod sdk_log;
pub mod fast_timing;
pub mod gas_fee_strategy;
pub mod global;
pub mod nonce_cache;
pub mod sdk_log;
pub mod seed;
pub mod spl_associated_token_account;
pub mod spl_token;
+149
View File
@@ -3,10 +3,59 @@
//! Controlled by `TradeConfig::log_enabled`, set in `TradingClient::new`.
//! All SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.) should check this before output.
use std::fmt;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
/// Format duration for log: "97.9396 ms" or "13.936 µs", 4 decimal places, space before unit.
fn format_elapsed(d: Duration) -> String {
let secs = d.as_secs_f64();
if secs < 0.001 {
format!("{:.4} µs", secs * 1_000_000.0)
} else {
format!("{:.4} ms", secs * 1000.0)
}
}
/// Extract a short error message for SWQOS submission failed log.
/// Tries JSON "message"/"data" and quoted string; on any parse failure returns original (no panic).
fn extract_swqos_error_message(s: &str) -> String {
let s = s.trim();
if s.is_empty() {
return String::new();
}
// Plain double-quoted string (no inner JSON): unquote
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
let inner = &s[1..s.len() - 1];
if !inner.contains('{') {
return inner.replace("\\\"", "\"");
}
}
// Try parse as JSON only when input looks like JSON (avoid parsing long non-JSON strings)
if s.starts_with('{') {
if let Ok(v) = serde_json::from_str::<serde_json::Value>(s) {
let obj = v
.get("error")
.and_then(|e| e.as_object())
.or_else(|| v.as_object());
if let Some(o) = obj {
if let Some(m) = o.get("message").and_then(|x| x.as_str()) {
return m.to_string();
}
if let Some(d) = o.get("data").and_then(|x| x.as_str()) {
return d.to_string();
}
}
}
}
s.to_string()
}
static SDK_LOG_ENABLED: AtomicBool = AtomicBool::new(true);
/// Width of [provider] label so SWQOS submit/confirm lines align (longest: Speedlanding).
pub const SWQOS_LABEL_WIDTH: usize = 12;
/// Whether SDK logging is enabled (set from TradeConfig.log_enabled in TradingClient::new).
#[inline(always)]
pub fn sdk_log_enabled() -> bool {
@@ -17,3 +66,103 @@ pub fn sdk_log_enabled() -> bool {
pub fn set_sdk_log_enabled(enabled: bool) {
SDK_LOG_ENABLED.store(enabled, Ordering::Relaxed);
}
/// Aligned log: ` [Soyas ] Buy submitted: 13.936 µs`. Call only when sdk_log_enabled().
#[inline]
pub fn log_swqos_submitted(
provider: &str,
trade_type: impl fmt::Display,
elapsed: Duration,
) {
println!(
" [{:width$}] {} submitted: {}",
provider,
trade_type,
format_elapsed(elapsed),
width = SWQOS_LABEL_WIDTH
);
}
/// Prints one SDK timing block (build_instructions, before_submit, per-channel submit_done).
/// When confirm_us is Some, prints confirmed + total; when None, prints "confirmed: -, total: submit_ms".
/// Call only when sdk_log_enabled().
pub fn print_sdk_timing_block(
dir: &str,
start_us: Option<i64>,
build_end_us: Option<i64>,
before_submit_us: Option<i64>,
submit_timings: &[(crate::swqos::SwqosType, i64)],
confirm_us: Option<i64>,
) {
println!();
let start_us = match start_us {
Some(u) => u,
None => return,
};
if let Some(end_us) = build_end_us {
println!(
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
"-",
dir,
(end_us - start_us) as f64 / 1000.0,
width = SWQOS_LABEL_WIDTH
);
}
if let Some(end_us) = before_submit_us {
println!(
" [SDK][{:width$}] {} before_submit: {:.4} ms",
"-",
dir,
(end_us - start_us) as f64 / 1000.0,
width = SWQOS_LABEL_WIDTH
);
}
if let Some(confirm_done_us) = confirm_us {
let total_ms = (confirm_done_us - start_us) as f64 / 1000.0;
for (swqos_type, submit_done_us) in submit_timings {
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
let confirmed_ms = (confirm_done_us - *submit_done_us).max(0) as f64 / 1000.0;
println!(
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
swqos_type.as_str(),
dir,
submit_ms,
confirmed_ms,
total_ms,
width = SWQOS_LABEL_WIDTH
);
}
} else {
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][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
swqos_type.as_str(),
dir,
submit_ms,
submit_ms,
width = SWQOS_LABEL_WIDTH
);
}
}
}
/// Aligned log: ` [Stellium ] Buy submission failed after 97.9396 ms: ...`. Call only when sdk_log_enabled().
/// Error is normalized: JSON "message"/"data" or quoted string is shown; raw JSON is not.
#[inline]
pub fn log_swqos_submission_failed(
provider: &str,
trade_type: impl fmt::Display,
elapsed: Duration,
err: impl fmt::Display,
) {
let msg = extract_swqos_error_message(&format!("{}", err));
eprintln!(
" [{:width$}] {} submission failed after {}, error: {}",
provider,
trade_type,
format_elapsed(elapsed),
msg,
width = SWQOS_LABEL_WIDTH
);
}
+9 -5
View File
@@ -1,13 +1,13 @@
use crate::common::SolanaRpcClient;
use anyhow::anyhow;
use fnv::FnvHasher;
use once_cell::sync::Lazy;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
use solana_system_interface::instruction::create_account_with_seed;
use std::hash::Hasher;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use tokio::time::{sleep, Duration};
use once_cell::sync::Lazy;
// 🚀 优化:使用 AtomicU64 替代 RwLock,性能提升 5-10x
// u64::MAX 表示未初始化状态
@@ -17,7 +17,7 @@ static SPL_TOKEN_2022_RENT: Lazy<AtomicU64> = Lazy::new(|| AtomicU64::new(u64::M
/// 更新租金缓存(后台任务调用)
pub async fn update_rents(client: &SolanaRpcClient) -> Result<(), anyhow::Error> {
let rent = fetch_rent_for_token_account(client, false).await?;
SPL_TOKEN_RENT.store(rent, Ordering::Release); // Release 确保其他线程可见
SPL_TOKEN_RENT.store(rent, Ordering::Release); // Release 确保其他线程可见
let rent = fetch_rent_for_token_account(client, true).await?;
SPL_TOKEN_2022_RENT.store(rent, Ordering::Release);
@@ -62,11 +62,15 @@ pub fn create_associated_token_account_use_seed(
// Relaxed: 租金值不变,无需同步;Release/Acquire 在 update_rents 保证初始化可见性
let rent = if is_2022_token {
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
if v == u64::MAX { return Err(anyhow!("Rent not initialized")); }
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
}
v
} else {
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
if v == u64::MAX { return Err(anyhow!("Rent not initialized")); }
if v == u64::MAX {
return Err(anyhow!("Rent not initialized"));
}
v
};
+1 -5
View File
@@ -52,11 +52,7 @@ pub fn transfer(
accounts.push(AccountMeta::new_readonly(**signer, true));
}
Ok(Instruction {
program_id: *token_program_id,
accounts,
data,
})
Ok(Instruction { program_id: *token_program_id, accounts, data })
}
pub fn initialize_account3(
+19 -8
View File
@@ -9,6 +9,8 @@ pub struct InfrastructureConfig {
pub rpc_url: String,
pub swqos_configs: Vec<SwqosConfig>,
pub commitment: CommitmentConfig,
/// When true, SWQOS sender threads use the *last* N cores instead of the first N. Reduces contention with main thread / default tokio workers that often use low-numbered cores. Default false.
pub swqos_cores_from_end: bool,
}
impl InfrastructureConfig {
@@ -21,6 +23,7 @@ impl InfrastructureConfig {
rpc_url,
swqos_configs,
commitment,
swqos_cores_from_end: false,
}
}
@@ -30,6 +33,7 @@ impl InfrastructureConfig {
rpc_url: config.rpc_url.clone(),
swqos_configs: config.swqos_configs.clone(),
commitment: config.commitment.clone(),
swqos_cores_from_end: config.swqos_cores_from_end,
}
}
@@ -47,8 +51,8 @@ impl Hash for InfrastructureConfig {
fn hash<H: Hasher>(&self, state: &mut H) {
self.rpc_url.hash(state);
self.swqos_configs.hash(state);
// Hash commitment level as string since CommitmentConfig doesn't impl Hash
format!("{:?}", self.commitment).hash(state);
self.swqos_cores_from_end.hash(state);
}
}
@@ -57,6 +61,7 @@ impl PartialEq for InfrastructureConfig {
self.rpc_url == other.rpc_url
&& self.swqos_configs == other.swqos_configs
&& self.commitment == other.commitment
&& self.swqos_cores_from_end == other.swqos_cores_from_end
}
}
@@ -72,12 +77,12 @@ pub struct TradeConfig {
pub create_wsol_ata_on_startup: bool,
/// Whether to use seed optimization for all ATA operations (default: true)
pub use_seed_optimize: bool,
/// Whether to pin parallel submit tasks to CPU cores (can reduce latency; set false in containers). Default true.
pub use_core_affinity: bool,
/// Whether to output all SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default true.
pub log_enabled: bool,
/// Whether to check minimum tip per SWQOS provider (filter out configs below min). Default false to save latency.
pub check_min_tip: bool,
/// When true, SWQOS uses the *last* N cores (instead of the first N). Use when main thread / tokio use low-numbered cores to reduce CPU contention. Default false.
pub swqos_cores_from_end: bool,
}
impl TradeConfig {
@@ -94,11 +99,11 @@ impl TradeConfig {
rpc_url,
swqos_configs,
commitment,
create_wsol_ata_on_startup: true, // default: check and create on startup
use_seed_optimize: true, // default: use seed optimization
use_core_affinity: true, // default: pin parallel submit tasks to cores
log_enabled: true, // default: enable all SDK logs
check_min_tip: false, // default: skip min tip check to reduce latency
create_wsol_ata_on_startup: true, // default: check and create on startup
use_seed_optimize: true, // default: use seed optimization
log_enabled: true, // default: enable all SDK logs
check_min_tip: false, // default: skip min tip check to reduce latency
swqos_cores_from_end: false,
}
}
@@ -118,6 +123,12 @@ impl TradeConfig {
self.check_min_tip = check_min_tip;
self
}
/// Use the *last* N cores for SWQOS (instead of the first N). Call this when the main thread or tokio workers use low-numbered cores to avoid binding SWQOS to busy cores. Default false.
pub fn with_swqos_cores_from_end(mut self, from_end: bool) -> Self {
self.swqos_cores_from_end = from_end;
self
}
}
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
+1 -2
View File
@@ -43,8 +43,7 @@ pub const USD1_TOKEN_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
};
// USDC (mainnet) mint and meta
pub const USDC_TOKEN_ACCOUNT: Pubkey =
pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
pub const USDC_TOKEN_ACCOUNT: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
pub const USDC_TOKEN_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
solana_sdk::instruction::AccountMeta {
pubkey: USDC_TOKEN_ACCOUNT,
+39 -16
View File
@@ -1,7 +1,6 @@
use solana_program::pubkey;
use solana_sdk::pubkey::Pubkey;
pub const JITO_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"),
pubkey!("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"),
@@ -112,6 +111,7 @@ pub const BLOCKRAZOR_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"),
];
/// Astralane tip wallets. Extended with new addresses for improved routing and reduced write-lock contention (see portal.astralane.io/blockline).
pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF"),
pubkey!("astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm"),
@@ -121,6 +121,16 @@ pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"),
pubkey!("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"),
pubkey!("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"),
// New tip wallets (2025) for improved performance and reduced write-lock delays
pubkey!("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"),
pubkey!("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"),
pubkey!("AstrABAu8CBTyuPXpV4eSCJ5fePEPnxN8NqBaPKQ9fHR"),
pubkey!("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"),
pubkey!("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"),
pubkey!("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"),
pubkey!("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"),
pubkey!("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"),
pubkey!("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"),
];
pub const STELLIUM_TIP_ACCOUNTS: &[Pubkey] = &[
@@ -165,13 +175,13 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
// Default,
pub const SWQOS_ENDPOINTS_JITO: [&str; 8] = [
"https://ny.mainnet.block-engine.jito.wtf",
"https://ny.mainnet.block-engine.jito.wtf",
"https://frankfurt.mainnet.block-engine.jito.wtf",
"https://amsterdam.mainnet.block-engine.jito.wtf",
"https://slc.mainnet.block-engine.jito.wtf",
"https://tokyo.mainnet.block-engine.jito.wtf",
"https://london.mainnet.block-engine.jito.wtf",
"https://ny.mainnet.block-engine.jito.wtf",
"https://ny.mainnet.block-engine.jito.wtf",
"https://mainnet.block-engine.jito.wtf",
];
@@ -180,8 +190,8 @@ pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 8] = [
"http://frankfurt.nextblock.io",
"http://amsterdam.nextblock.io",
"http://slc.nextblock.io",
"http://tokyo.nextblock.io",
"http://london.nextblock.io",
"http://tokyo.nextblock.io",
"http://london.nextblock.io",
"http://singapore.nextblock.io",
"http://frankfurt.nextblock.io",
];
@@ -236,11 +246,11 @@ pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 8] = [
"ny.node1.me:16666",
"fra.node1.me:16666",
"ams.node1.me:16666",
"ny.node1.me:16666", // SLC → ny
"ny.node1.me:16666", // SLC → ny
"tk.node1.me:16666",
"lon.node1.me:16666",
"ny.node1.me:16666", // LA → ny
"ny.node1.me:16666", // Default → ny
"ny.node1.me:16666", // LA → ny
"ny.node1.me:16666", // Default → ny
];
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
@@ -267,6 +277,19 @@ pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
];
/// BlockRazor gRPC endpoints. Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
/// Port 80 for gRPC protocol. Auth: apikey metadata in gRPC headers.
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 8] = [
"http://newyork.solana-grpc.blockrazor.xyz:80",
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
"http://amsterdam.solana-grpc.blockrazor.xyz:80",
"http://newyork.solana-grpc.blockrazor.xyz:80",
"http://tokyo.solana-grpc.blockrazor.xyz:80",
"http://london.solana-grpc.blockrazor.xyz:80",
"http://newyork.solana-grpc.blockrazor.xyz:80",
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
];
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
@@ -284,14 +307,14 @@ pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
/// Astralane QUIC endpoints (port 7000). Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
/// See: https://github.com/Astralane/astralane-quic-client. We use fr, ams, la, ny, lim, sg only (avoid ams2/fr2 for lower latency).
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
"ny.gateway.astralane.io:7000", // NewYork
"fr.gateway.astralane.io:7000", // Frankfurt
"ams.gateway.astralane.io:7000", // Amsterdam
"lim.gateway.astralane.io:7000", // SLC (no slc, use lim)
"sg.gateway.astralane.io:7000", // Tokyo (Asia)
"ams.gateway.astralane.io:7000", // London (Europe, avoid ams2)
"la.gateway.astralane.io:7000", // LosAngeles
"lim.gateway.astralane.io:7000", // Default
"ny.gateway.astralane.io:7000", // NewYork
"fr.gateway.astralane.io:7000", // Frankfurt
"ams.gateway.astralane.io:7000", // Amsterdam
"lim.gateway.astralane.io:7000", // SLC (no slc, use lim)
"sg.gateway.astralane.io:7000", // Tokyo (Asia)
"ams.gateway.astralane.io:7000", // London (Europe, avoid ams2)
"la.gateway.astralane.io:7000", // LosAngeles
"lim.gateway.astralane.io:7000", // Default
];
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
+1 -1
View File
@@ -6,4 +6,4 @@ pub mod trade {
pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001;
pub const DEFAULT_RPC_UNIT_LIMIT: u32 = 150000;
pub const DEFAULT_RPC_UNIT_PRICE: u64 = 500000;
}
}
+12 -6
View File
@@ -29,8 +29,10 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
.downcast_ref::<MeteoraDammV2Params>()
.ok_or_else(|| anyhow!("Invalid protocol params for MeteoraDammV2"))?;
let is_wsol = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT || protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let is_wsol = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return Err(anyhow!("Pool must contain WSOL or USDC"));
}
@@ -38,7 +40,8 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
// ========================================
// Trade calculation and account address preparation
// ========================================
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let amount_in: u64 = params.input_amount.unwrap_or(0);
let minimum_amount_out: u64 = match params.fixed_output_amount {
Some(fixed) => fixed,
@@ -141,8 +144,10 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
return Err(anyhow!("Token amount is not set"));
}
let is_wsol = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT || protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let is_wsol = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return Err(anyhow!("Pool must contain WSOL or USDC"));
}
@@ -150,7 +155,8 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
// ========================================
// Trade calculation and account address preparation
// ========================================
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let minimum_amount_out: u64 = match params.fixed_output_amount {
Some(fixed) => fixed,
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
+4 -4
View File
@@ -1,7 +1,7 @@
pub mod bonk;
pub mod meteora_damm_v2;
pub mod pumpfun;
pub mod pumpswap;
pub mod bonk;
pub mod raydium_cpmm;
pub mod raydium_amm_v4;
pub mod meteora_damm_v2;
pub mod utils;
pub mod raydium_cpmm;
pub mod utils;
+21 -27
View File
@@ -10,7 +10,8 @@ use crate::{
instruction::utils::pumpfun::{
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
get_mayhem_fee_recipient_meta_random, get_user_volume_accumulator_pda,
global_constants::{self}, BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
global_constants::{self},
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
utils::calc::{
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
@@ -64,8 +65,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
);
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() {
get_bonding_curve_pda(&params.output_mint)
.ok_or_else(|| anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint))?
get_bonding_curve_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
})?
} else {
bonding_curve.account
};
@@ -147,8 +149,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
global_constants::FEE_RECIPIENT_META
};
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.output_mint)
.ok_or_else(|| anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint))?;
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
})?;
let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
fee_recipient_meta,
@@ -169,11 +172,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
];
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // remainingAccounts: @pump-fun/pump-sdk 要求末尾传 bondingCurveV2Pda(mint),勿删
instructions.push(Instruction::new_with_bytes(
accounts::PUMPFUN,
&buy_data,
accounts,
));
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, accounts));
Ok(instructions)
}
@@ -220,8 +219,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
};
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() {
get_bonding_curve_pda(&params.input_mint)
.ok_or_else(|| anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint))?
get_bonding_curve_pda(&params.input_mint).ok_or_else(|| {
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
})?
} else {
bonding_curve.account
};
@@ -290,20 +290,18 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
// Cashback: Bonding Curve Sell expects UserVolumeAccumulator PDA at 0th remaining account (writable)
if bonding_curve.is_cashback_coin {
let user_volume_accumulator = get_user_volume_accumulator_pda(&params.payer.pubkey())
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
let user_volume_accumulator =
get_user_volume_accumulator_pda(&params.payer.pubkey())
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
accounts.push(AccountMeta::new(user_volume_accumulator, false));
}
// remainingAccounts: @pump-fun/pump-sdk sell 要求末尾传 bondingCurveV2Pda(mint)cashback 时在 user_volume_accumulator 之后),勿删
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.input_mint)
.ok_or_else(|| anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint))?;
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.input_mint).ok_or_else(|| {
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint)
})?;
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false));
instructions.push(Instruction::new_with_bytes(
accounts::PUMPFUN,
&sell_data,
accounts,
));
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, accounts));
// Optional: Close token account
if protocol_params.close_token_account_when_sell.unwrap_or(false)
@@ -327,15 +325,11 @@ pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option<Instruction>
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
let accounts = vec![
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable, not signer)
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::PUMPFUN_META,
];
Some(Instruction::new_with_bytes(
accounts::PUMPFUN,
&CLAIM_CASHBACK_DISCRIMINATOR,
accounts,
))
Some(Instruction::new_with_bytes(accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, accounts))
}
+5 -9
View File
@@ -225,11 +225,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
buf.to_vec()
};
instructions.push(Instruction {
program_id: accounts::AMM_PROGRAM,
accounts,
data,
});
instructions.push(Instruction { program_id: accounts::AMM_PROGRAM, accounts, data });
if close_wsol_ata {
// Close wSOL ATA account, reclaim rent
instructions.extend(crate::trading::common::close_wsol(&params.payer.pubkey()));
@@ -462,12 +458,12 @@ pub fn claim_cashback_pumpswap_instruction(
// IDL order: user, user_volume_accumulator, quote_mint, quote_token_program,
// user_volume_accumulator_wsol_token_account, user_wsol_token_account, system_program, event_authority, program
let accounts = vec![
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
AccountMeta::new_readonly(quote_mint, false),
AccountMeta::new_readonly(quote_token_program, false),
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
AccountMeta::new(user_wsol_ata, false), // writable
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
AccountMeta::new(user_wsol_ata, false), // writable
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::AMM_PROGRAM_META,
+11 -3
View File
@@ -44,7 +44,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
// ========================================
// Trade calculation and account address preparation
// ========================================
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.coin_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let amount_in: u64 = params.input_amount.unwrap_or(0);
let swap_result = compute_swap_amount(
@@ -62,7 +62,11 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
let user_source_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -187,7 +191,11 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
let user_destination_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
+44 -18
View File
@@ -61,7 +61,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
// ========================================
// Trade calculation and account address preparation
// ========================================
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT;
let mint_token_program = if is_base_in {
protocol_params.quote_token_program
@@ -84,7 +84,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -97,10 +101,15 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let input_vault_account = get_vault_account(
&pool_state,
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
protocol_params,
);
let output_vault_account = get_vault_account(&pool_state, &params.output_mint, protocol_params);
let output_vault_account =
get_vault_account(&pool_state, &params.output_mint, protocol_params);
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
get_observation_state_pda(&pool_state).unwrap()
@@ -136,13 +145,17 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
accounts::AUTHORITY_META, // Authority (readonly)
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
AccountMeta::new(pool_state, false), // Pool State
AccountMeta::new(input_token_account, false), // Input Token Account
AccountMeta::new(output_token_account, false), // Output Token Account
AccountMeta::new(input_vault_account, false), // Input Vault Account
AccountMeta::new(output_vault_account, false), // Output Vault Account
AccountMeta::new(input_token_account, false), // Input Token Account
AccountMeta::new(output_token_account, false), // Output Token Account
AccountMeta::new(input_vault_account, false), // Input Vault Account
AccountMeta::new(output_vault_account, false), // Output Vault Account
crate::constants::TOKEN_PROGRAM_META, // Input Token Program (readonly)
AccountMeta::new_readonly(mint_token_program, false), // Output Token Program (readonly)
if is_wsol { crate::constants::WSOL_TOKEN_ACCOUNT_META } else { crate::constants::USDC_TOKEN_ACCOUNT_META }, // Input token mint (readonly)
if is_wsol {
crate::constants::WSOL_TOKEN_ACCOUNT_META
} else {
crate::constants::USDC_TOKEN_ACCOUNT_META
}, // Input token mint (readonly)
AccountMeta::new_readonly(params.output_mint, false), // Output token mint (readonly)
AccountMeta::new(observation_state_account, false), // Observation State Account
];
@@ -196,7 +209,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let is_usdc = protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT
|| protocol_params.quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return Err(anyhow!("Pool must contain WSOL or USDC"));
}
@@ -228,7 +241,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
&params.payer.pubkey(),
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
&crate::constants::TOKEN_PROGRAM,
params.open_seed_optimize,
);
@@ -241,10 +258,15 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
let output_vault_account = get_vault_account(
&pool_state,
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
if is_wsol {
&crate::constants::WSOL_TOKEN_ACCOUNT
} else {
&crate::constants::USDC_TOKEN_ACCOUNT
},
protocol_params,
);
let input_vault_account = get_vault_account(&pool_state, &params.input_mint, protocol_params);
let input_vault_account =
get_vault_account(&pool_state, &params.input_mint, protocol_params);
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
get_observation_state_pda(&pool_state).unwrap()
@@ -267,14 +289,18 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
accounts::AUTHORITY_META, // Authority (readonly)
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
AccountMeta::new(pool_state, false), // Pool State
AccountMeta::new(input_token_account, false), // Input Token Account
AccountMeta::new(output_token_account, false), // Output Token Account
AccountMeta::new(input_vault_account, false), // Input Vault Account
AccountMeta::new(output_vault_account, false), // Output Vault Account
AccountMeta::new(input_token_account, false), // Input Token Account
AccountMeta::new(output_token_account, false), // Output Token Account
AccountMeta::new(input_vault_account, false), // Input Vault Account
AccountMeta::new(output_vault_account, false), // Output Vault Account
AccountMeta::new_readonly(mint_token_program, false), // Input Token Program (readonly)
crate::constants::TOKEN_PROGRAM_META, // Output Token Program (readonly)
AccountMeta::new_readonly(params.input_mint, false), // Input token mint (readonly)
if is_wsol { crate::constants::WSOL_TOKEN_ACCOUNT_META } else { crate::constants::USDC_TOKEN_ACCOUNT_META }, // Output token mint (readonly)
if is_wsol {
crate::constants::WSOL_TOKEN_ACCOUNT_META
} else {
crate::constants::USDC_TOKEN_ACCOUNT_META
}, // Output token mint (readonly)
AccountMeta::new(observation_state_account, false), // Observation State Account
];
// Create instruction data
+2 -2
View File
@@ -1,13 +1,13 @@
pub mod bonk;
pub mod meteora_damm_v2;
pub mod pumpfun;
pub mod pumpswap;
pub mod raydium_amm_v4;
pub mod raydium_cpmm;
pub mod meteora_damm_v2;
// types
pub mod bonk_types;
pub mod meteora_damm_v2_types;
pub mod pumpswap_types;
pub mod raydium_amm_v4_types;
pub mod raydium_cpmm_types;
pub mod meteora_damm_v2_types;
+1 -5
View File
@@ -178,11 +178,7 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
.choose(&mut rand::rng())
.unwrap_or(&global_constants::MAYHEM_FEE_RECIPIENTS[0]);
AccountMeta {
pubkey: recipient,
is_signer: false,
is_writable: true,
}
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
}
pub struct Symbol;
+13 -9
View File
@@ -169,11 +169,7 @@ pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
let recipient = *accounts::MAYHEM_FEE_RECIPIENTS
.choose(&mut rand::rng())
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0]);
let meta = AccountMeta {
pubkey: recipient,
is_signer: false,
is_writable: false,
};
let meta = AccountMeta { pubkey: recipient, is_signer: false, is_writable: false };
(recipient, meta)
}
@@ -334,7 +330,7 @@ pub async fn find_by_base_mint(
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", base_mint));
}
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
let mut pools: Vec<_> = accounts
.into_iter()
.filter_map(|(addr, acc)| {
@@ -349,7 +345,11 @@ pub async fn find_by_base_mint(
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
if pools.is_empty() {
return Err(anyhow!("No valid pool decoded for mint {} (found {} accounts but all decode failed)", base_mint, accounts_count));
return Err(anyhow!(
"No valid pool decoded for mint {} (found {} accounts but all decode failed)",
base_mint,
accounts_count
));
}
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
@@ -386,7 +386,7 @@ pub async fn find_by_quote_mint(
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", quote_mint));
}
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
let mut pools: Vec<_> = accounts
.into_iter()
.filter_map(|(addr, acc)| {
@@ -401,7 +401,11 @@ pub async fn find_by_quote_mint(
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
if pools.is_empty() {
return Err(anyhow!("No valid pool decoded for quote_mint {} (found {} accounts but all decode failed)", quote_mint, accounts_count));
return Err(anyhow!(
"No valid pool decoded for quote_mint {} (found {} accounts but all decode failed)",
quote_mint,
accounts_count
));
}
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
+4 -2
View File
@@ -135,9 +135,11 @@ pub fn get_vault_account(
) -> Pubkey {
if protocol_params.base_mint == *token_mint && protocol_params.base_vault != Pubkey::default() {
protocol_params.base_vault
} else if protocol_params.quote_mint == *token_mint && protocol_params.quote_vault != Pubkey::default() {
} else if protocol_params.quote_mint == *token_mint
&& protocol_params.quote_vault != Pubkey::default()
{
protocol_params.quote_vault
} else {
get_vault_pda(pool_state, token_mint).unwrap()
}
}
}
+161 -49
View File
@@ -8,7 +8,7 @@ pub mod utils;
use crate::common::nonce_cache::DurableNonceInfo;
use crate::common::sdk_log;
use crate::common::GasFeeStrategy;
use crate::common::{TradeConfig, InfrastructureConfig};
use crate::common::{InfrastructureConfig, TradeConfig};
#[cfg(feature = "perf-trace")]
use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
use crate::constants::SOL_TOKEN_ACCOUNT;
@@ -22,12 +22,12 @@ use crate::swqos::TradeType;
// Re-export for SWQOS HTTP/QUIC choice in SwqosConfig (e.g. Astralane)
pub use crate::swqos::SwqosTransport;
use crate::trading::core::params::BonkParams;
use crate::trading::core::params::DexParamEnum;
use crate::trading::core::params::MeteoraDammV2Params;
use crate::trading::core::params::PumpFunParams;
use crate::trading::core::params::PumpSwapParams;
use crate::trading::core::params::RaydiumAmmV4Params;
use crate::trading::core::params::RaydiumCpmmParams;
use crate::trading::core::params::DexParamEnum;
use crate::trading::factory::DexType;
use crate::trading::MiddlewareManager;
use crate::trading::SwapParams;
@@ -65,13 +65,8 @@ pub async fn find_pool_by_mint(
dex_type: DexType,
) -> Result<Pubkey, anyhow::Error> {
match dex_type {
DexType::PumpSwap => {
crate::instruction::utils::pumpswap::find_pool(rpc, mint).await
}
_ => Err(anyhow::anyhow!(
"find_pool_by_mint not implemented for {:?}",
dex_type
)),
DexType::PumpSwap => crate::instruction::utils::pumpswap::find_pool(rpc, mint).await,
_ => Err(anyhow::anyhow!("find_pool_by_mint not implemented for {:?}", dex_type)),
}
}
@@ -91,10 +86,14 @@ pub enum TradeTokenType {
pub struct TradingInfrastructure {
/// Shared RPC client for blockchain interactions
pub rpc: Arc<SolanaRpcClient>,
/// Shared SWQOS clients for transaction priority and routing
pub swqos_clients: Vec<Arc<SwqosClient>>,
/// Shared SWQOS clients for transaction priority and routing. Arc<Vec<..>> so cloning into SwapParams is a single Arc clone.
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
/// Configuration used to create this infrastructure
pub config: InfrastructureConfig,
/// Precomputed at init: min(swqos_clients.len(), 2/3 * num_cores). Not computed on trade hot path.
pub max_sender_concurrency: usize,
/// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path.
pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
}
impl TradingInfrastructure {
@@ -180,14 +179,52 @@ impl TradingInfrastructure {
}
}
let swqos_count = swqos_clients.len();
let (max_sender_concurrency, effective_core_ids) = {
let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0);
let max_by_cores = (num_cores * 2 / 3).max(1);
let cap = swqos_count.min(max_by_cores).max(1);
let ids = core_affinity::get_core_ids()
.map(|all| {
let v: Vec<_> = all.into_iter().collect();
let len = v.len();
if config.swqos_cores_from_end && len >= cap {
v.into_iter().skip(len - cap).collect()
} else {
v.into_iter().take(cap).collect()
}
})
.unwrap_or_default();
(cap, Arc::new(ids))
};
Self {
rpc,
swqos_clients,
swqos_clients: Arc::new(swqos_clients),
config,
max_sender_concurrency,
effective_core_ids,
}
}
}
/// When using `TradeConfig::with_swqos_cores_from_end(true)`, returns the same "last N" core indices
/// that the infrastructure uses. Pass the result to `TradingClient::with_dedicated_sender_threads`
/// for 方式 C (组合使用): SWQOS on last N cores and dedicated sender threads pinned to those cores.
///
/// Returns `None` if core count cannot be determined. `swqos_count` is typically `swqos_configs.len()`.
pub fn recommended_sender_thread_core_indices(swqos_count: usize) -> Option<Vec<usize>> {
let all = core_affinity::get_core_ids()?;
let num_cores = all.len();
if num_cores == 0 {
return None;
}
let max_by_cores = (num_cores * 2 / 3).max(1);
let cap = swqos_count.min(max_by_cores).max(1).min(num_cores);
let start = num_cores.saturating_sub(cap);
Some((start..num_cores).collect())
}
/// Main trading client for Solana DeFi protocols
///
/// `SolTradingSDK` provides a unified interface for trading across multiple Solana DEXs
@@ -204,8 +241,14 @@ pub struct TradingClient {
/// Whether to use seed optimization for all ATA operations (default: true)
/// Applies to all token account creations across buy and sell operations
pub use_seed_optimize: bool,
/// Whether to pin parallel submit tasks to CPU cores (from TradeConfig.use_core_affinity). Default true.
pub use_core_affinity: bool,
/// Internal: use dedicated sender threads (default false). Set via with_dedicated_sender_threads() for advanced use.
pub use_dedicated_sender_threads: bool,
/// Internal: core indices for dedicated sender threads. Trimmed to ≤ max_sender_concurrency at set.
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
/// Internal: precomputed at infra init (min(swqos_count, 2/3*cores)). Not user-configurable.
pub max_sender_concurrency: usize,
/// Internal: precomputed at infra init for job affinity. Not user-configurable.
pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
/// Whether to output all SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency.
@@ -224,7 +267,10 @@ impl Clone for TradingClient {
infrastructure: self.infrastructure.clone(),
middleware_manager: self.middleware_manager.clone(),
use_seed_optimize: self.use_seed_optimize,
use_core_affinity: self.use_core_affinity,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
effective_core_ids: self.effective_core_ids.clone(),
log_enabled: self.log_enabled,
check_min_tip: self.check_min_tip,
}
@@ -256,7 +302,7 @@ pub struct TradeBuyParams {
/// Optional address lookup table for transaction size optimization
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Whether to wait for transaction confirmation before returning
pub wait_transaction_confirmed: bool,
pub wait_tx_confirmed: bool,
/// Whether to create input token associated token account
pub create_input_token_ata: bool,
/// Whether to close input token associated token account after trade
@@ -307,7 +353,7 @@ pub struct TradeSellParams {
/// Optional address lookup table for transaction size optimization
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
/// Whether to wait for transaction confirmation before returning
pub wait_transaction_confirmed: bool,
pub wait_tx_confirmed: bool,
/// Whether to create output token associated token account
pub create_output_token_ata: bool,
/// Whether to close output token associated token account after trade
@@ -347,13 +393,18 @@ impl TradingClient {
) -> Self {
// Initialize wallet-specific caches (fast, synchronous)
crate::common::fast_fn::fast_init(&payer.pubkey());
let max_sender_concurrency = infrastructure.max_sender_concurrency;
let effective_core_ids = infrastructure.effective_core_ids.clone();
Self {
payer,
infrastructure,
middleware_manager: None,
use_seed_optimize,
use_core_affinity: true,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency,
effective_core_ids,
log_enabled: true,
check_min_tip: false,
}
@@ -388,12 +439,18 @@ impl TradingClient {
}
}
let max_sender_concurrency = infrastructure.max_sender_concurrency;
let effective_core_ids = infrastructure.effective_core_ids.clone();
Self {
payer,
infrastructure,
middleware_manager: None,
use_seed_optimize,
use_core_affinity: true,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency,
effective_core_ids,
log_enabled: true,
check_min_tip: false,
}
@@ -408,7 +465,9 @@ impl TradingClient {
timeout_secs: u64,
) -> Result<(), String> {
use solana_sdk::transaction::Transaction;
let recent_blockhash = rpc.get_latest_blockhash().await
let recent_blockhash = rpc
.get_latest_blockhash()
.await
.map_err(|e| format!("Failed to get blockhash: {}", e))?;
let tx = Transaction::new_signed_with_payer(
create_ata_ixs,
@@ -419,7 +478,8 @@ impl TradingClient {
let send_result = tokio::time::timeout(
tokio::time::Duration::from_secs(timeout_secs),
rpc.send_and_confirm_transaction(&tx),
).await;
)
.await;
match send_result {
Ok(Ok(_signature)) => Ok(()),
Ok(Err(e)) => {
@@ -437,12 +497,11 @@ impl TradingClient {
const MAX_RETRIES: usize = 3;
const TIMEOUT_SECS: u64 = 10;
let wsol_ata =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&payer.pubkey(),
&WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
);
let wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&payer.pubkey(),
&WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
);
if rpc.get_account(&wsol_ata).await.is_ok() {
if sdk_log::sdk_log_enabled() {
@@ -451,8 +510,7 @@ impl TradingClient {
return;
}
let create_ata_ixs =
crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey());
let create_ata_ixs = crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey());
if create_ata_ixs.is_empty() {
if sdk_log::sdk_log_enabled() {
info!(target: "sol_trade_sdk", "️ WSOL ATA already exists (no need to create)");
@@ -471,7 +529,15 @@ impl TradingClient {
}
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
}
match Self::try_create_wsol_ata_once(rpc.as_ref(), payer, &wsol_ata, &create_ata_ixs, TIMEOUT_SECS).await {
match Self::try_create_wsol_ata_once(
rpc.as_ref(),
payer,
&wsol_ata,
&create_ata_ixs,
TIMEOUT_SECS,
)
.await
{
Ok(()) => {
if sdk_log::sdk_log_enabled() {
info!(target: "sol_trade_sdk", "✅ WSOL ATA created or already exists");
@@ -554,12 +620,16 @@ impl TradingClient {
}
}
// 并发/核心相关由 infrastructure 预计算,用户无需配置
let instance = Self {
payer,
infrastructure,
infrastructure: infrastructure.clone(),
middleware_manager: None,
use_seed_optimize: trade_config.use_seed_optimize,
use_core_affinity: trade_config.use_core_affinity,
use_dedicated_sender_threads: false,
sender_thread_cores: None,
max_sender_concurrency: infrastructure.max_sender_concurrency,
effective_core_ids: infrastructure.effective_core_ids.clone(),
log_enabled: trade_config.log_enabled,
check_min_tip: trade_config.check_min_tip,
};
@@ -585,6 +655,34 @@ impl TradingClient {
self
}
/// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores).
/// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs.
/// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores).
/// - `None`: keep default (shared tokio pool).
/// - `Some(vec![])`: dedicated threads with default count, no core pinning.
/// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap).
///
/// **Latency note:** If a core is busy with other work (node, bot), SWQOS submit on that core can be delayed.
/// For lowest latency, pass core indices that are *reserved* for SWQOS (do not run other CPU-heavy work on those cores).
pub fn with_dedicated_sender_threads(mut self, core_indices: Option<Vec<usize>>) -> Self {
match core_indices {
None => {
self.use_dedicated_sender_threads = false;
self.sender_thread_cores = None;
}
Some(v) if v.is_empty() => {
self.use_dedicated_sender_threads = true;
self.sender_thread_cores = None;
}
Some(v) => {
self.use_dedicated_sender_threads = true;
let cap = v.len().min(self.max_sender_concurrency);
self.sender_thread_cores = Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v }));
}
}
self
}
/// Gets the RPC client instance for direct Solana blockchain interactions
///
/// This provides access to the underlying Solana RPC client that can be used
@@ -690,7 +788,7 @@ impl TradingClient {
slippage_basis_points: params.slippage_basis_points,
address_lookup_table_account: params.address_lookup_table_account,
recent_blockhash: params.recent_blockhash,
wait_transaction_confirmed: params.wait_transaction_confirmed,
wait_tx_confirmed: params.wait_tx_confirmed,
protocol_params,
open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置
swqos_clients: self.infrastructure.swqos_clients.clone(),
@@ -705,7 +803,10 @@ impl TradingClient {
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
log_enabled: self.log_enabled,
use_core_affinity: self.use_core_affinity,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
effective_core_ids: self.effective_core_ids.clone(),
check_min_tip: self.check_min_tip,
grpc_recv_us: params.grpc_recv_us,
use_exact_sol_amount: params.use_exact_sol_amount,
@@ -794,7 +895,7 @@ impl TradingClient {
slippage_basis_points: params.slippage_basis_points,
address_lookup_table_account: params.address_lookup_table_account,
recent_blockhash: params.recent_blockhash,
wait_transaction_confirmed: params.wait_transaction_confirmed,
wait_tx_confirmed: params.wait_tx_confirmed,
protocol_params,
with_tip: params.with_tip,
open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置
@@ -809,7 +910,10 @@ impl TradingClient {
gas_fee_strategy: params.gas_fee_strategy,
simulate: params.simulate,
log_enabled: self.log_enabled,
use_core_affinity: self.use_core_affinity,
use_dedicated_sender_threads: self.use_dedicated_sender_threads,
sender_thread_cores: self.sender_thread_cores.clone(),
max_sender_concurrency: self.max_sender_concurrency,
effective_core_ids: self.effective_core_ids.clone(),
check_min_tip: self.check_min_tip,
grpc_recv_us: params.grpc_recv_us,
use_exact_sol_amount: None,
@@ -981,8 +1085,10 @@ impl TradingClient {
/// - 交易执行或确认失败
/// - 网络或 RPC 错误
pub async fn wrap_wsol_to_sol(&self, amount: u64) -> Result<String, anyhow::Error> {
use crate::trading::common::wsol_manager::{wrap_wsol_to_sol as wrap_wsol_to_sol_internal, wrap_wsol_to_sol_without_create};
use crate::common::seed::get_associated_token_address_with_program_id_use_seed;
use crate::trading::common::wsol_manager::{
wrap_wsol_to_sol as wrap_wsol_to_sol_internal, wrap_wsol_to_sol_without_create,
};
use solana_sdk::transaction::Transaction;
// 检查临时seed账户是否已存在
@@ -1003,7 +1109,8 @@ impl TradingClient {
};
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
@@ -1019,8 +1126,10 @@ impl TradingClient {
/// * `Err(anyhow::Error)` - Build or send failure (e.g. invalid PDA)
pub async fn claim_cashback_pumpfun(&self) -> Result<String, anyhow::Error> {
use solana_sdk::transaction::Transaction;
let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction(&self.payer.pubkey())
.ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?;
let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction(
&self.payer.pubkey(),
)
.ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?;
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&[ix], Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
@@ -1038,21 +1147,24 @@ impl TradingClient {
/// * `Err(anyhow::Error)` - Build or send failure
pub async fn claim_cashback_pumpswap(&self) -> Result<String, anyhow::Error> {
use solana_sdk::transaction::Transaction;
let mut instructions = crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&self.payer.pubkey(),
&self.payer.pubkey(),
&WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
self.use_seed_optimize,
);
let mut instructions =
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&self.payer.pubkey(),
&self.payer.pubkey(),
&WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
self.use_seed_optimize,
);
let ix = crate::instruction::pumpswap::claim_cashback_pumpswap_instruction(
&self.payer.pubkey(),
WSOL_TOKEN_ACCOUNT,
crate::constants::TOKEN_PROGRAM,
).ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?;
)
.ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?;
instructions.push(ix);
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
let mut transaction =
Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
+90 -75
View File
@@ -1,5 +1,5 @@
//! 🚀 编译器级性能优化 - 极致编译时优化
//!
//!
//! 实现编译时的极致性能优化,包括:
//! - 编译器标志优化配置
//! - 编译时代码生成
@@ -137,100 +137,110 @@ impl CompilerOptimizer {
stats: CompilerOptimizationStats::default(),
}
}
/// 🚀 生成超高性能编译配置
pub fn generate_ultra_performance_config(&self) -> Result<CompilerConfig> {
tracing::info!(target: "sol_trade_sdk","🚀 Generating ultra-performance compiler configuration...");
let mut rustflags = Vec::new();
// 基础优化标志
rustflags.push("-C".to_string());
rustflags.push("opt-level=3".to_string()); // 最高优化级别
// 链接时优化
if self.optimization_flags.enable_lto {
rustflags.push("-C".to_string());
rustflags.push("lto=fat".to_string()); // 胖LTO获得最佳优化
}
// 目标CPU优化
if !self.optimization_flags.target_cpu.is_empty() {
rustflags.push("-C".to_string());
rustflags.push(format!("target-cpu={}", self.optimization_flags.target_cpu));
}
// 目标特性
if !self.optimization_flags.target_features.is_empty() {
rustflags.push("-C".to_string());
rustflags.push(format!("target-feature={}", self.optimization_flags.target_features.join(",")));
rustflags.push(format!(
"target-feature={}",
self.optimization_flags.target_features.join(",")
));
}
// 代码模型
rustflags.push("-C".to_string());
rustflags.push(format!("code-model={:?}", self.optimization_flags.code_model).to_lowercase());
rustflags
.push(format!("code-model={:?}", self.optimization_flags.code_model).to_lowercase());
// 恐慌处理
if self.codegen_config.panic_abort {
rustflags.push("-C".to_string());
rustflags.push("panic=abort".to_string());
}
// 溢出检查
if !self.codegen_config.overflow_checks {
rustflags.push("-C".to_string());
rustflags.push("overflow-checks=no".to_string());
}
// 代码生成单元
if let Some(units) = self.optimization_flags.codegen_units {
rustflags.push("-C".to_string());
rustflags.push(format!("codegen-units={}", units));
}
// 内联阈值
rustflags.push("-C".to_string());
rustflags.push(format!("inline-threshold={}", self.inline_strategy.inline_threshold));
// 额外的性能优化标志
rustflags.extend([
"-C".to_string(), "embed-bitcode=no".to_string(), // 不嵌入位码以减少体积
"-C".to_string(), "debuginfo=0".to_string(), // 禁用调试信息
"-C".to_string(), "rpath=no".to_string(), // 禁用rpath
"-C".to_string(), "force-frame-pointers=no".to_string(), // 禁用帧指针
"-C".to_string(),
"embed-bitcode=no".to_string(), // 不嵌入位码以减少体积
"-C".to_string(),
"debuginfo=0".to_string(), // 禁用调试信息
"-C".to_string(),
"rpath=no".to_string(), // 禁用rpath
"-C".to_string(),
"force-frame-pointers=no".to_string(), // 禁用帧指针
]);
let config = CompilerConfig {
rustflags,
env_vars: self.generate_env_vars(),
cargo_config: self.generate_cargo_config(),
};
tracing::info!(target: "sol_trade_sdk","✅ Ultra-performance compiler configuration generated");
Ok(config)
}
/// 生成环境变量配置
fn generate_env_vars(&self) -> HashMap<String, String> {
let mut env_vars = HashMap::new();
// CPU特定优化
env_vars.insert("CARGO_CFG_TARGET_FEATURE".to_string(),
self.optimization_flags.target_features.join(","));
env_vars.insert(
"CARGO_CFG_TARGET_FEATURE".to_string(),
self.optimization_flags.target_features.join(","),
);
// 启用不稳定特性
env_vars.insert("RUSTC_BOOTSTRAP".to_string(), "1".to_string());
// 编译缓存设置
if self.optimization_flags.incremental {
env_vars.insert("CARGO_INCREMENTAL".to_string(), "1".to_string());
} else {
env_vars.insert("CARGO_INCREMENTAL".to_string(), "0".to_string());
}
env_vars
}
/// 生成Cargo配置
fn generate_cargo_config(&self) -> CargoConfig {
CargoConfig {
@@ -244,17 +254,21 @@ impl CompilerOptimizer {
debug_assertions: false,
rpath: false,
strip: true, // 去除符号表
}
},
}
}
/// 获取统计信息
pub fn get_stats(&self) -> CompilerOptimizationStats {
CompilerOptimizationStats {
inlined_functions: AtomicU64::new(self.stats.inlined_functions.load(Ordering::Relaxed)),
constant_folding: AtomicU64::new(self.stats.constant_folding.load(Ordering::Relaxed)),
dead_code_elimination: AtomicU64::new(self.stats.dead_code_elimination.load(Ordering::Relaxed)),
loop_optimizations: AtomicU64::new(self.stats.loop_optimizations.load(Ordering::Relaxed)),
dead_code_elimination: AtomicU64::new(
self.stats.dead_code_elimination.load(Ordering::Relaxed),
),
loop_optimizations: AtomicU64::new(
self.stats.loop_optimizations.load(Ordering::Relaxed),
),
}
}
}
@@ -279,12 +293,12 @@ impl OptimizationFlags {
Self {
opt_level: OptLevel::Aggressive,
enable_lto: true,
enable_pgo: false, // PGO需要多阶段构建
enable_pgo: false, // PGO需要多阶段构建
target_cpu: "native".to_string(), // 使用本机CPU特性
target_features,
code_model: CodeModel::Small,
debug_info: false,
incremental: false, // 发布版本禁用增量编译
incremental: false, // 发布版本禁用增量编译
codegen_units: Some(1), // 单个代码生成单元获得最佳优化
}
}
@@ -294,7 +308,7 @@ impl CodegenConfig {
/// 超高性能配置
pub fn ultra_performance() -> Self {
Self {
panic_abort: true, // 恐慌即中止,避免展开开销
panic_abort: true, // 恐慌即中止,避免展开开销
overflow_checks: false, // 生产环境禁用溢出检查
fat_lto: true,
enable_simd: true,
@@ -353,14 +367,14 @@ macro_rules! compile_time_optimize {
(const $expr:expr) => {
const { $expr }
};
// 强制内联热路径
(inline_hot $fn_name:ident) => {
#[inline(always)]
#[hot]
$fn_name
};
// 标记冷路径
(cold $fn_name:ident) => {
#[inline(never)]
@@ -372,10 +386,10 @@ macro_rules! compile_time_optimize {
/// 🚀 零成本抽象特征
pub trait ZeroCostAbstraction {
type Output;
/// 编译时计算
fn compute_at_compile_time(&self) -> Self::Output;
/// 内联操作
#[inline(always)]
fn inline_operation(&self) -> Self::Output {
@@ -399,35 +413,35 @@ impl CompileTimeOptimizedEventProcessor {
route_table: Self::precompute_route_table(),
}
}
/// 编译时预计算哈希表
const fn precompute_hash_table() -> [u64; 256] {
let mut table = [0u64; 256];
let mut i = 0;
while i < 256 {
// 使用编译时常量计算哈希值
table[i] = Self::const_hash(i as u8);
i += 1;
}
table
}
/// 编译时预计算路由表
const fn precompute_route_table() -> [u32; 1024] {
let mut table = [0u32; 1024];
let mut i = 0;
while i < 1024 {
// 预计算路由信息
table[i] = (i as u32) % 16; // 16个工作线程
i += 1;
}
table
}
/// 编译时常量哈希函数
const fn const_hash(input: u8) -> u64 {
// 使用简单的编译时常量哈希
@@ -437,16 +451,14 @@ impl CompileTimeOptimizedEventProcessor {
hash ^= hash << 17;
hash
}
/// 🚀 零开销事件路由
#[inline(always)]
pub fn route_event_zero_cost(&self, event_id: u8) -> u32 {
// 编译时优化:直接数组访问,无边界检查
unsafe {
*self.route_table.get_unchecked((event_id as usize) & 1023)
}
unsafe { *self.route_table.get_unchecked((event_id as usize) & 1023) }
}
/// 🚀 编译时优化的哈希查找
#[inline(always)]
pub fn hash_lookup_optimized(&self, key: u8) -> u64 {
@@ -464,28 +476,28 @@ impl SIMDCompileTimeOptimizer {
#[target_feature(enable = "avx2")]
pub unsafe fn vectorized_sum_compile_time(data: &[u64]) -> u64 {
use std::arch::x86_64::*;
if data.len() < 4 {
return data.iter().sum();
}
let chunks = data.len() / 4;
let mut sum_vec = _mm256_setzero_si256();
for i in 0..chunks {
let ptr = data.as_ptr().add(i * 4) as *const __m256i;
let vec = _mm256_loadu_si256(ptr);
sum_vec = _mm256_add_epi64(sum_vec, vec);
}
// 水平求和
let mut result = [0u64; 4];
_mm256_storeu_si256(result.as_mut_ptr() as *mut __m256i, sum_vec);
let partial_sum: u64 = result.iter().sum();
// 处理剩余元素
let remaining: u64 = data[chunks * 4..].iter().sum();
partial_sum + remaining
}
@@ -523,7 +535,8 @@ fn main() {
println!("cargo:rustc-link-arg=-fprofile-use");
}
}
"#.to_string()
"#
.to_string()
}
/// 🚀 生成.cargo/config.toml
@@ -576,56 +589,58 @@ rustflags = [
rustflags = [
"-C", "target-feature=+sse4.2,+avx,+avx2,+fma,+bmi1,+bmi2,+lzcnt,+popcnt",
]
"#.to_string()
"#
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compiler_optimizer_creation() {
let optimizer = CompilerOptimizer::new();
assert!(optimizer.optimization_flags.enable_lto);
assert_eq!(optimizer.optimization_flags.opt_level as u8, OptLevel::Aggressive as u8);
}
#[test]
fn test_compile_time_processor() {
const PROCESSOR: CompileTimeOptimizedEventProcessor = CompileTimeOptimizedEventProcessor::new();
const PROCESSOR: CompileTimeOptimizedEventProcessor =
CompileTimeOptimizedEventProcessor::new();
let route = PROCESSOR.route_event_zero_cost(42);
assert!(route < 16); // 应该路由到16个工作线程之一
let hash = PROCESSOR.hash_lookup_optimized(100);
assert!(hash > 0); // 哈希值应该非零
}
#[test]
fn test_ultra_performance_config() {
let flags = OptimizationFlags::ultra_performance();
assert!(flags.enable_lto);
assert_eq!(flags.target_cpu, "native");
assert!(!flags.target_features.is_empty());
let codegen = CodegenConfig::ultra_performance();
assert!(codegen.panic_abort);
assert!(!codegen.overflow_checks);
assert!(codegen.enable_simd);
}
#[test]
#[test]
fn test_compiler_config_generation() {
let optimizer = CompilerOptimizer::new();
let config = optimizer.generate_ultra_performance_config().unwrap();
assert!(!config.rustflags.is_empty());
assert!(config.rustflags.contains(&"-C".to_string()));
assert!(config.rustflags.contains(&"opt-level=3".to_string()));
assert!(config.env_vars.contains_key("CARGO_INCREMENTAL"));
}
#[test]
fn test_simd_compile_time_optimization() {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
@@ -642,7 +657,7 @@ mod tests {
assert_eq!(sum, 36); // 1+2+3+4+5+6+7+8 = 36
}
}
#[test]
fn test_build_script_generation() {
let build_script = generate_build_script();
@@ -650,7 +665,7 @@ mod tests {
assert!(build_script.contains("TARGET_FEATURE"));
assert!(build_script.contains("lld"));
}
#[test]
fn test_cargo_config_generation() {
let config = generate_cargo_config_toml();
@@ -659,4 +674,4 @@ mod tests {
assert!(config.contains("target-cpu=native"));
assert!(config.contains("panic = \"abort\""));
}
}
}
+82 -93
View File
@@ -1,11 +1,11 @@
//! Hardware-oriented optimizations: cache-line alignment, prefetch, SIMD, branch hints, memory barriers.
//! 硬件级优化:缓存行对齐与预取、SIMD、分支提示、内存屏障。
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::Result;
use crossbeam_utils::CachePadded;
use std::mem::size_of;
use std::ptr;
use crossbeam_utils::CachePadded;
use anyhow::Result;
use std::sync::atomic::{AtomicU64, Ordering};
/// Typical CPU cache line size in bytes. 典型 CPU 缓存行大小(字节)。
pub const CACHE_LINE_SIZE: usize = 64;
@@ -32,7 +32,7 @@ impl SIMDMemoryOps {
_ => Self::memcpy_avx512_or_fallback(dst, src, len),
}
}
/// Copy 18 bytes (scalar / small word). 小数据拷贝(18 字节)。
#[inline(always)]
unsafe fn memcpy_small(dst: *mut u8, src: *const u8, len: usize) {
@@ -53,52 +53,52 @@ impl SIMDMemoryOps {
_ => unreachable!(),
}
}
/// Copy 916 bytes using SSE (128-bit). SSE 拷贝(916 字节)。
#[inline(always)]
unsafe fn memcpy_sse(dst: *mut u8, src: *const u8, len: usize) {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{__m128i, _mm_loadu_si128, _mm_storeu_si128};
if len <= 16 {
let chunk = _mm_loadu_si128(src as *const __m128i);
_mm_storeu_si128(dst as *mut __m128i, chunk);
}
}
#[cfg(not(target_arch = "x86_64"))]
{
ptr::copy_nonoverlapping(src, dst, len);
}
}
/// Copy 1732 bytes using AVX (256-bit). AVX 拷贝(1732 字节)。
#[inline(always)]
unsafe fn memcpy_avx(dst: *mut u8, src: *const u8, len: usize) {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_storeu_si256};
if len <= 32 {
let chunk = _mm256_loadu_si256(src as *const __m256i);
_mm256_storeu_si256(dst as *mut __m256i, chunk);
}
}
#[cfg(not(target_arch = "x86_64"))]
{
ptr::copy_nonoverlapping(src, dst, len);
}
}
/// Copy 3364 bytes using AVX2 (256-bit, two chunks). AVX2 拷贝(3364 字节,两段)。
#[inline(always)]
unsafe fn memcpy_avx2(dst: *mut u8, src: *const u8, len: usize) {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_storeu_si256};
let chunk1 = _mm256_loadu_si256(src as *const __m256i);
_mm256_storeu_si256(dst as *mut __m256i, chunk1);
if len > 32 {
@@ -109,52 +109,52 @@ impl SIMDMemoryOps {
}
}
}
#[cfg(not(target_arch = "x86_64"))]
{
ptr::copy_nonoverlapping(src, dst, len);
}
}
/// Copy >64 bytes: AVX-512 64-byte chunks when available, else AVX2 32-byte chunks. >64 字节:有 AVX512 用 64 字节块,否则 AVX2 32 字节块。
#[inline(always)]
unsafe fn memcpy_avx512_or_fallback(dst: *mut u8, src: *const u8, len: usize) {
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
{
use std::arch::x86_64::{__m512i, _mm512_loadu_si512, _mm512_storeu_si512};
let chunks = len / 64;
let mut offset = 0;
for _ in 0..chunks {
let chunk = _mm512_loadu_si512(src.add(offset) as *const __m512i);
_mm512_storeu_si512(dst.add(offset) as *mut __m512i, chunk);
offset += 64;
}
let remaining = len % 64;
if remaining > 0 {
Self::memcpy_avx2(dst.add(offset), src.add(offset), remaining);
}
}
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
{
let chunks = len / 32;
let mut offset = 0;
for _ in 0..chunks {
Self::memcpy_avx2(dst.add(offset), src.add(offset), 32);
offset += 32;
}
let remaining = len % 32;
if remaining > 0 {
Self::memcpy_avx(dst.add(offset), src.add(offset), remaining);
}
}
}
/// SIMD-optimized byte equality; dispatches by length (small / SSE / AVX2 / large). SIMD 加速的内存比较,按长度分派。
#[inline(always)]
pub unsafe fn memcmp_simd_optimized(a: *const u8, b: *const u8, len: usize) -> bool {
@@ -166,109 +166,108 @@ impl SIMDMemoryOps {
_ => Self::memcmp_large(a, b, len),
}
}
/// Compare 18 bytes (scalar). 小数据比较(18 字节)。
#[inline(always)]
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
match len {
1 => *a == *b,
2 => *(a as *const u16) == *(b as *const u16),
3 => {
*(a as *const u16) == *(b as *const u16) &&
*a.add(2) == *b.add(2)
}
3 => *(a as *const u16) == *(b as *const u16) && *a.add(2) == *b.add(2),
4 => *(a as *const u32) == *(b as *const u32),
5..=8 => *(a as *const u64) == *(b as *const u64),
_ => unreachable!(),
}
}
/// Compare 916 bytes using SSE. SSE 比较(916 字节)。
#[inline(always)]
unsafe fn memcmp_sse(a: *const u8, b: *const u8, len: usize) -> bool {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{__m128i, _mm_loadu_si128, _mm_cmpeq_epi8, _mm_movemask_epi8};
use std::arch::x86_64::{__m128i, _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8};
let chunk_a = _mm_loadu_si128(a as *const __m128i);
let chunk_b = _mm_loadu_si128(b as *const __m128i);
let cmp_result = _mm_cmpeq_epi8(chunk_a, chunk_b);
let mask = _mm_movemask_epi8(cmp_result) as u32;
let valid_mask = if len >= 16 { 0xFFFF } else { (1u32 << len) - 1 };
(mask & valid_mask) == valid_mask
}
#[cfg(not(target_arch = "x86_64"))]
{
(0..len).all(|i| *a.add(i) == *b.add(i))
}
}
/// Compare 1732 bytes using AVX2. AVX2 比较(1732 字节)。
#[inline(always)]
unsafe fn memcmp_avx2(a: *const u8, b: *const u8, len: usize) -> bool {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_cmpeq_epi8, _mm256_movemask_epi8};
use std::arch::x86_64::{
__m256i, _mm256_cmpeq_epi8, _mm256_loadu_si256, _mm256_movemask_epi8,
};
let chunk_a = _mm256_loadu_si256(a as *const __m256i);
let chunk_b = _mm256_loadu_si256(b as *const __m256i);
let cmp_result = _mm256_cmpeq_epi8(chunk_a, chunk_b);
let mask = _mm256_movemask_epi8(cmp_result) as u32;
let valid_mask = if len >= 32 { 0xFFFFFFFF } else { (1u32 << len) - 1 };
(mask & valid_mask) == valid_mask
}
#[cfg(not(target_arch = "x86_64"))]
{
(0..len).all(|i| *a.add(i) == *b.add(i))
}
}
/// Compare >32 bytes in 32-byte AVX2 chunks. 大数据比较(32 字节 AVX2 分块)。
#[inline(always)]
unsafe fn memcmp_large(a: *const u8, b: *const u8, len: usize) -> bool {
let chunks = len / 32;
for i in 0..chunks {
let offset = i * 32;
if !Self::memcmp_avx2(a.add(offset), b.add(offset), 32) {
return false;
}
}
let remaining = len % 32;
if remaining > 0 {
return Self::memcmp_avx2(a.add(chunks * 32), b.add(chunks * 32), remaining);
}
true
}
/// SIMD-optimized zero memory. SIMD 加速的内存清零。
#[inline(always)]
pub unsafe fn memzero_simd_optimized(ptr: *mut u8, len: usize) {
#[cfg(target_arch = "x86_64")]
{
use std::arch::x86_64::{__m256i, _mm256_setzero_si256, _mm256_storeu_si256};
let zero = _mm256_setzero_si256();
let chunks = len / 32;
let mut offset = 0;
for _ in 0..chunks {
_mm256_storeu_si256(ptr.add(offset) as *mut __m256i, zero);
offset += 32;
}
let remaining = len % 32;
for i in 0..remaining {
*ptr.add(offset + i) = 0;
}
}
#[cfg(not(target_arch = "x86_64"))]
{
ptr::write_bytes(ptr, 0, len);
@@ -291,17 +290,17 @@ impl CacheAlignedCounter {
_padding: [0; CACHE_LINE_SIZE - size_of::<AtomicU64>()],
}
}
#[inline(always)]
pub fn increment(&self) -> u64 {
self.value.fetch_add(1, Ordering::Relaxed)
}
#[inline(always)]
pub fn load(&self) -> u64 {
self.value.load(Ordering::Relaxed)
}
#[inline(always)]
pub fn store(&self, val: u64) {
self.value.store(val, Ordering::Relaxed)
@@ -312,7 +311,7 @@ impl CacheLineAligned for CacheAlignedCounter {
fn ensure_cache_aligned(&self) -> bool {
(self as *const Self as usize) % CACHE_LINE_SIZE == 0
}
fn prefetch_data(&self) {
#[cfg(target_arch = "x86_64")]
unsafe {
@@ -339,10 +338,10 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
if !capacity.is_power_of_two() {
return Err(anyhow::anyhow!("Capacity must be a power of 2"));
}
let mut buffer = Vec::with_capacity(capacity);
buffer.resize_with(capacity, Default::default);
Ok(Self {
buffer,
producer_head: CachePadded::new(AtomicU64::new(0)),
@@ -351,7 +350,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
mask: capacity - 1,
})
}
/// Lock-free push; returns false if full. 无锁写入,满则返回 false。
#[inline(always)]
pub fn try_push(&self, item: T) -> bool {
@@ -368,7 +367,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
self.producer_head.store(current_head + 1, Ordering::Release);
true
}
/// Lock-free pop; returns None if empty. 无锁读取,空则返回 None。
#[inline(always)]
pub fn try_pop(&self) -> Option<T> {
@@ -385,7 +384,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
self.consumer_tail.store(current_tail + 1, Ordering::Release);
Some(item)
}
/// Current number of elements. 当前元素个数。
#[inline(always)]
pub fn len(&self) -> usize {
@@ -393,12 +392,11 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
let tail = self.consumer_tail.load(Ordering::Relaxed);
((head + self.capacity as u64 - tail) & self.mask as u64) as usize
}
/// True if no elements. 是否为空。
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.producer_head.load(Ordering::Relaxed) ==
self.consumer_tail.load(Ordering::Relaxed)
self.producer_head.load(Ordering::Relaxed) == self.consumer_tail.load(Ordering::Relaxed)
}
}
@@ -406,7 +404,7 @@ impl<T> CacheLineAligned for CacheOptimizedRingBuffer<T> {
fn ensure_cache_aligned(&self) -> bool {
(self as *const Self as usize) % CACHE_LINE_SIZE == 0
}
fn prefetch_data(&self) {
#[cfg(target_arch = "x86_64")]
unsafe {
@@ -428,25 +426,25 @@ impl BranchOptimizer {
pub fn likely(condition: bool) -> bool {
#[cold]
fn cold() {}
if !condition {
cold();
}
condition
}
/// Hint: condition is usually false. 提示编译器条件大概率为假。
#[inline(always)]
pub fn unlikely(condition: bool) -> bool {
#[cold]
fn cold() {}
if condition {
cold();
}
condition
}
/// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。
#[inline(always)]
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
@@ -457,7 +455,7 @@ impl BranchOptimizer {
_mm_prefetch(ptr as *const i8, _MM_HINT_T0);
}
}
/// Prefetch for write (T1 hint). 写预取(T1 提示)。
#[inline(always)]
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
@@ -479,25 +477,25 @@ impl MemoryBarriers {
pub fn compiler_barrier() {
std::sync::atomic::compiler_fence(Ordering::SeqCst);
}
/// Light barrier (Acquire). 轻量级屏障(Acquire)。
#[inline(always)]
pub fn memory_barrier_light() {
std::sync::atomic::fence(Ordering::Acquire);
}
/// Full sequential consistency barrier. 全序一致性屏障。
#[inline(always)]
pub fn memory_barrier_heavy() {
std::sync::atomic::fence(Ordering::SeqCst);
}
/// Store/release barrier. 存储屏障,保证写入可见性。
#[inline(always)]
pub fn store_barrier() {
std::sync::atomic::fence(Ordering::Release);
}
/// Load/acquire barrier. 加载屏障,保证读取顺序。
#[inline(always)]
pub fn load_barrier() {
@@ -508,63 +506,54 @@ impl MemoryBarriers {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_aligned_counter() {
let counter = CacheAlignedCounter::new(0);
assert!(counter.ensure_cache_aligned());
assert_eq!(counter.load(), 0);
counter.increment();
assert_eq!(counter.load(), 1);
}
#[test]
fn test_simd_memcpy() {
let src = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let mut dst = [0u8; 10];
unsafe {
SIMDMemoryOps::memcpy_simd_optimized(
dst.as_mut_ptr(),
src.as_ptr(),
src.len()
);
SIMDMemoryOps::memcpy_simd_optimized(dst.as_mut_ptr(), src.as_ptr(), src.len());
}
assert_eq!(src, dst);
}
#[test]
fn test_cache_optimized_ring_buffer() {
let buffer: CacheOptimizedRingBuffer<u64> =
CacheOptimizedRingBuffer::new(16).unwrap();
let buffer: CacheOptimizedRingBuffer<u64> = CacheOptimizedRingBuffer::new(16).unwrap();
assert!(buffer.is_empty());
// 测试推入
assert!(buffer.try_push(42));
assert_eq!(buffer.len(), 1);
// 测试弹出
assert_eq!(buffer.try_pop(), Some(42));
assert!(buffer.is_empty());
}
#[test]
fn test_simd_memcmp() {
let a = [1u8, 2, 3, 4, 5];
let b = [1u8, 2, 3, 4, 5];
let c = [1u8, 2, 3, 4, 6];
unsafe {
assert!(SIMDMemoryOps::memcmp_simd_optimized(
a.as_ptr(), b.as_ptr(), a.len()
));
assert!(!SIMDMemoryOps::memcmp_simd_optimized(
a.as_ptr(), c.as_ptr(), a.len()
));
assert!(SIMDMemoryOps::memcmp_simd_optimized(a.as_ptr(), b.as_ptr(), a.len()));
assert!(!SIMDMemoryOps::memcmp_simd_optimized(a.as_ptr(), c.as_ptr(), a.len()));
}
}
}
}
+8 -8
View File
@@ -1,14 +1,14 @@
//! Performance: SIMD, cache prefetch, branch hints, zero-copy I/O, syscall bypass, compiler hints.
//! 性能优化:SIMD、缓存预取、分支提示、零拷贝 I/O、系统调用绕过、编译器提示。
pub mod simd;
pub mod hardware_optimizations;
pub mod zero_copy_io;
pub mod syscall_bypass;
pub mod compiler_optimization;
pub mod hardware_optimizations;
pub mod simd;
pub mod syscall_bypass;
pub mod zero_copy_io;
pub use simd::*;
pub use hardware_optimizations::*;
pub use zero_copy_io::*;
pub use syscall_bypass::*;
pub use compiler_optimization::*;
pub use hardware_optimizations::*;
pub use simd::*;
pub use syscall_bypass::*;
pub use zero_copy_io::*;
+1 -1
View File
@@ -235,7 +235,7 @@ impl SIMDHash {
/// 批量计算 SHA256 哈希
#[inline(always)]
pub fn hash_batch_sha256(data: &[&[u8]]) -> Vec<[u8; 32]> {
use sha2::{Sha256, Digest};
use sha2::{Digest, Sha256};
data.iter()
.map(|item| {
+154 -154
View File
@@ -1,11 +1,11 @@
//! Syscall bypass: batching, vDSO fast time, io_uring, mmap, userspace impl.
//! 系统调用绕过:批处理、vDSO 快速时间、io_uring、mmap、用户态实现。
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH, Duration, Instant};
#[allow(unused_imports)]
use std::fs::OpenOptions;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use anyhow::Result;
use crossbeam_utils::CachePadded;
@@ -55,14 +55,30 @@ pub struct SyscallBatchProcessor {
#[derive(Debug, Clone)]
pub enum SyscallRequest {
Write { fd: i32, data: Vec<u8> },
Read { fd: i32, size: usize },
Send { socket: i32, data: Vec<u8> },
Recv { socket: i32, size: usize },
Write {
fd: i32,
data: Vec<u8>,
},
Read {
fd: i32,
size: usize,
},
Send {
socket: i32,
data: Vec<u8>,
},
Recv {
socket: i32,
size: usize,
},
GetTime,
MemAlloc { size: usize },
MemAlloc {
size: usize,
},
/// 内存释放
MemFree { ptr: usize },
MemFree {
ptr: usize,
},
}
/// 🚀 快速时间提供器 - 绕过系统调用获取时间
@@ -86,24 +102,22 @@ impl FastTimeProvider {
pub fn new(enable_vdso: bool) -> Result<Self> {
let now = SystemTime::now();
let instant_now = Instant::now();
let provider = Self {
_base_time: now,
monotonic_start: instant_now,
time_cache: CachePadded::new(AtomicU64::new(
now.duration_since(UNIX_EPOCH)?.as_nanos() as u64
now.duration_since(UNIX_EPOCH)?.as_nanos() as u64,
)),
cache_update_interval_ns: 1_000_000, // 1ms
last_update: CachePadded::new(AtomicU64::new(
instant_now.elapsed().as_nanos() as u64
)),
last_update: CachePadded::new(AtomicU64::new(instant_now.elapsed().as_nanos() as u64)),
vdso_enabled: enable_vdso,
};
tracing::info!(target: "sol_trade_sdk","🚀 Fast time provider initialized with vDSO: {}", enable_vdso);
Ok(provider)
}
/// 🚀 超快速获取当前时间 - 绕过系统调用
#[inline(always)]
pub fn fast_now_nanos(&self) -> u64 {
@@ -111,19 +125,19 @@ impl FastTimeProvider {
// 使用vDSO快速获取时间
return self.vdso_time_nanos();
}
// 使用缓存的时间
let now_mono = self.monotonic_start.elapsed().as_nanos() as u64;
let last_update = self.last_update.load(Ordering::Relaxed);
if now_mono.saturating_sub(last_update) > self.cache_update_interval_ns {
// 需要更新缓存
self.update_time_cache();
}
self.time_cache.load(Ordering::Relaxed)
}
/// vDSO时间获取
#[inline(always)]
fn vdso_time_nanos(&self) -> u64 {
@@ -132,36 +146,34 @@ impl FastTimeProvider {
// 在Linux上使用vDSO获取时间,避免系统调用
unsafe {
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
// CLOCK_MONOTONIC_RAW不受NTP调整影响,更适合性能测量
if libc::clock_gettime(libc::CLOCK_MONOTONIC_RAW, &mut ts) == 0 {
return (ts.tv_sec as u64) * 1_000_000_000 + (ts.tv_nsec as u64);
}
}
}
// 回退到缓存时间
self.time_cache.load(Ordering::Relaxed)
}
/// 更新时间缓存
fn update_time_cache(&self) {
if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) {
let nanos = now.as_nanos() as u64;
self.time_cache.store(nanos, Ordering::Relaxed);
self.last_update.store(
self.monotonic_start.elapsed().as_nanos() as u64,
Ordering::Relaxed
);
self.last_update
.store(self.monotonic_start.elapsed().as_nanos() as u64, Ordering::Relaxed);
}
}
/// 🚀 快速获取微秒时间戳
#[inline(always)]
pub fn fast_now_micros(&self) -> u64 {
self.fast_now_nanos() / 1000
}
/// 🚀 快速获取毫秒时间戳
#[inline(always)]
pub fn fast_now_millis(&self) -> u64 {
@@ -200,16 +212,16 @@ impl IOOptimizer {
/// 创建I/O优化器
pub fn new(_config: &SyscallBypassConfig) -> Result<Self> {
let io_uring_available = Self::check_io_uring_support();
tracing::info!(target: "sol_trade_sdk","🚀 I/O Optimizer initialized - io_uring: {}", io_uring_available);
Ok(Self {
io_uring_available,
async_io_stats: Arc::new(AsyncIOStats::default()),
mmap_regions: Vec::new(),
})
}
/// 检查io_uring支持
fn check_io_uring_support() -> bool {
#[cfg(target_os = "linux")]
@@ -218,7 +230,7 @@ impl IOOptimizer {
if let Ok(uname) = std::process::Command::new("uname").arg("-r").output() {
let kernel_version = String::from_utf8_lossy(&uname.stdout);
tracing::info!(target: "sol_trade_sdk","Kernel version: {}", kernel_version.trim());
// 简单检查:内核版本 >= 5.1 支持io_uring
if let Some(version_str) = kernel_version.split('.').next() {
if let Ok(major_version) = version_str.parse::<u32>() {
@@ -227,60 +239,62 @@ impl IOOptimizer {
}
}
}
false
}
/// 🚀 批量异步写入 - 绕过多次系统调用
#[inline(always)]
pub async fn batch_async_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
if self.io_uring_available && requests.len() > 1 {
return self.io_uring_batch_write(requests).await;
}
// 回退到标准批量写入
self.standard_batch_write(requests).await
}
/// 使用io_uring进行批量写入
async fn io_uring_batch_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
// 这里是伪代码 - 实际实现需要io_uring库
tracing::trace!(target: "sol_trade_sdk","Using io_uring for {} write operations", requests.len());
let mut results = Vec::with_capacity(requests.len());
// 模拟批量提交到io_uring
for (_fd, data) in requests {
self.async_io_stats.operations_queued.fetch_add(1, Ordering::Relaxed);
// 实际的io_uring实现会在这里提交所有操作
// 然后等待完成,避免多次系统调用
results.push(data.len()); // 模拟写入成功
self.async_io_stats.bytes_transferred.fetch_add(data.len() as u64, Ordering::Relaxed);
self.async_io_stats.operations_completed.fetch_add(1, Ordering::Relaxed);
}
// 这是一个系统调用而不是N个
self.async_io_stats.syscalls_avoided.fetch_add(requests.len() as u64 - 1, Ordering::Relaxed);
self.async_io_stats
.syscalls_avoided
.fetch_add(requests.len() as u64 - 1, Ordering::Relaxed);
Ok(results)
}
/// 标准批量写入
async fn standard_batch_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
let mut results = Vec::with_capacity(requests.len());
// 将所有写入打包成一个写操作
for (_fd, data) in requests {
// 模拟写入操作
results.push(data.len());
self.async_io_stats.bytes_transferred.fetch_add(data.len() as u64, Ordering::Relaxed);
}
Ok(results)
}
/// 🚀 内存映射文件I/O - 避免read/write系统调用
pub fn create_memory_mapped_io(&mut self, file_path: &str, size: usize) -> Result<usize> {
#[cfg(unix)]
@@ -298,16 +312,12 @@ impl IOOptimizer {
.custom_flags(libc::O_DIRECT) // 直接I/O,绕过页面缓存
.open(file_path)?
};
#[cfg(not(target_os = "linux"))]
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(file_path)?;
let file = OpenOptions::new().read(true).write(true).create(true).open(file_path)?;
let fd = file.as_raw_fd();
unsafe {
let addr = libc::mmap(
std::ptr::null_mut(),
@@ -317,37 +327,42 @@ impl IOOptimizer {
fd,
0,
);
if addr == libc::MAP_FAILED {
return Err(anyhow::anyhow!("Memory mapping failed"));
}
let region = MemoryMappedRegion {
address: addr as usize,
size,
file_descriptor: fd,
};
let region =
MemoryMappedRegion { address: addr as usize, size, file_descriptor: fd };
self.mmap_regions.push(region);
tracing::info!(target: "sol_trade_sdk","✅ Memory mapped I/O created: {} bytes at {:p}", size, addr);
Ok(addr as usize)
}
}
#[cfg(not(unix))]
{
Err(anyhow::anyhow!("Memory mapped I/O not supported on this platform"))
}
}
/// 获取I/O统计
pub fn get_stats(&self) -> AsyncIOStats {
AsyncIOStats {
operations_queued: AtomicU64::new(self.async_io_stats.operations_queued.load(Ordering::Relaxed)),
operations_completed: AtomicU64::new(self.async_io_stats.operations_completed.load(Ordering::Relaxed)),
bytes_transferred: AtomicU64::new(self.async_io_stats.bytes_transferred.load(Ordering::Relaxed)),
syscalls_avoided: AtomicU64::new(self.async_io_stats.syscalls_avoided.load(Ordering::Relaxed)),
operations_queued: AtomicU64::new(
self.async_io_stats.operations_queued.load(Ordering::Relaxed),
),
operations_completed: AtomicU64::new(
self.async_io_stats.operations_completed.load(Ordering::Relaxed),
),
bytes_transferred: AtomicU64::new(
self.async_io_stats.bytes_transferred.load(Ordering::Relaxed),
),
syscalls_avoided: AtomicU64::new(
self.async_io_stats.syscalls_avoided.load(Ordering::Relaxed),
),
}
}
}
@@ -357,47 +372,46 @@ impl SyscallBatchProcessor {
pub fn new(batch_size: usize) -> Result<Self> {
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
let executor = tokio::runtime::Handle::current();
tracing::info!(target: "sol_trade_sdk","🚀 Syscall batch processor created with batch size: {}", batch_size);
Ok(Self {
pending_calls,
_executor: executor,
batch_stats: CachePadded::new(AtomicU64::new(0)),
})
}
/// 🚀 提交系统调用请求到批处理队列
#[inline(always)]
pub fn submit_request(&self, request: SyscallRequest) -> Result<()> {
self.pending_calls.push(request)
.map_err(|_| anyhow::anyhow!("Batch queue full"))?;
self.pending_calls.push(request).map_err(|_| anyhow::anyhow!("Batch queue full"))?;
Ok(())
}
/// 🚀 执行批量系统调用
pub async fn execute_batch(&self) -> Result<usize> {
let mut batch = Vec::new();
// 收集批量请求
while batch.len() < 100 && !self.pending_calls.is_empty() {
if let Some(request) = self.pending_calls.pop() {
batch.push(request);
}
}
if batch.is_empty() {
return Ok(0);
}
let batch_size = batch.len();
// 按类型分组批量执行
let mut write_requests = Vec::new();
let mut read_requests = Vec::new();
let mut network_requests = Vec::new();
for request in batch {
match request {
SyscallRequest::Write { fd, data } => {
@@ -414,28 +428,28 @@ impl SyscallBatchProcessor {
}
}
}
// 批量执行写入
if !write_requests.is_empty() {
self.batch_write_operations(write_requests).await?;
}
// 批量执行读取
if !read_requests.is_empty() {
self.batch_read_operations(read_requests).await?;
}
// 批量执行网络操作
if !network_requests.is_empty() {
self.batch_network_operations(network_requests).await?;
}
self.batch_stats.fetch_add(1, Ordering::Relaxed);
tracing::trace!(target: "sol_trade_sdk","Executed batch of {} syscalls", batch_size);
Ok(batch_size)
}
/// 批量写入操作
async fn batch_write_operations(&self, requests: Vec<(i32, Vec<u8>)>) -> Result<()> {
// 使用writev系统调用进行批量写入
@@ -445,7 +459,7 @@ impl SyscallBatchProcessor {
}
Ok(())
}
/// 批量读取操作
async fn batch_read_operations(&self, requests: Vec<(i32, usize)>) -> Result<()> {
// 使用readv系统调用进行批量读取
@@ -454,7 +468,7 @@ impl SyscallBatchProcessor {
}
Ok(())
}
/// 批量网络操作
async fn batch_network_operations(&self, requests: Vec<(i32, Vec<u8>)>) -> Result<()> {
// 使用sendmsg/recvmsg进行批量网络操作
@@ -482,22 +496,16 @@ impl SystemCallBypassManager {
let fast_time_provider = Arc::new(FastTimeProvider::new(config.enable_vdso)?);
let io_optimizer = Arc::new(IOOptimizer::new(&config)?);
let stats = Arc::new(SyscallBypassStats::default());
tracing::info!(target: "sol_trade_sdk","🚀 System Call Bypass Manager initialized");
tracing::info!(target: "sol_trade_sdk"," 📦 Batch Processing: {}", config.enable_batch_processing);
tracing::info!(target: "sol_trade_sdk"," ⏰ Fast Time: {}", config.enable_fast_time);
tracing::info!(target: "sol_trade_sdk"," 🚀 vDSO: {}", config.enable_vdso);
tracing::info!(target: "sol_trade_sdk"," 📁 io_uring: {}", config.enable_io_uring);
Ok(Self {
config,
batch_processor,
fast_time_provider,
_io_optimizer: io_optimizer,
stats,
})
Ok(Self { config, batch_processor, fast_time_provider, _io_optimizer: io_optimizer, stats })
}
/// 🚀 快速获取当前时间戳 - 绕过系统调用
#[inline(always)]
pub fn fast_timestamp_nanos(&self) -> u64 {
@@ -505,28 +513,25 @@ impl SystemCallBypassManager {
self.stats.time_calls_cached.fetch_add(1, Ordering::Relaxed);
return self.fast_time_provider.fast_now_nanos();
}
// 回退到标准时间获取
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos() as u64
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64
}
/// 🚀 提交批量I/O操作
pub async fn submit_batch_io(&self, operations: Vec<SyscallRequest>) -> Result<()> {
if !self.config.enable_batch_processing {
return Err(anyhow::anyhow!("Batch processing disabled"));
}
for op in operations {
self.batch_processor.submit_request(op)?;
}
self.stats.syscalls_batched.fetch_add(1, Ordering::Relaxed);
Ok(())
}
/// 🚀 执行优化的内存分配 - 绕过malloc系统调用
#[inline(always)]
pub fn fast_allocate(&self, size: usize) -> Result<*mut u8> {
@@ -534,34 +539,30 @@ impl SystemCallBypassManager {
self.stats.memory_operations_avoided.fetch_add(1, Ordering::Relaxed);
return self.userspace_allocate(size);
}
// 回退到标准分配
let layout = std::alloc::Layout::from_size_align(size, 8)?;
let ptr = unsafe { std::alloc::alloc(layout) };
if ptr.is_null() {
Err(anyhow::anyhow!("Allocation failed"))
} else {
Ok(ptr)
}
}
/// 用户空间内存分配
fn userspace_allocate(&self, size: usize) -> Result<*mut u8> {
use std::sync::Mutex;
use once_cell::sync::Lazy;
use std::sync::Mutex;
struct MemoryPool {
pool: Box<[u8; 1024 * 1024]>,
offset: usize,
}
static MEMORY_POOL: Lazy<Mutex<MemoryPool>> = Lazy::new(|| {
Mutex::new(MemoryPool {
pool: Box::new([0; 1024 * 1024]),
offset: 0,
})
});
static MEMORY_POOL: Lazy<Mutex<MemoryPool>> =
Lazy::new(|| Mutex::new(MemoryPool { pool: Box::new([0; 1024 * 1024]), offset: 0 }));
let mut pool = MEMORY_POOL.lock().unwrap();
@@ -574,18 +575,18 @@ impl SystemCallBypassManager {
Ok(ptr)
}
/// 启动批处理工作线程
pub async fn start_batch_processing(&self) -> Result<()> {
let processor = Arc::clone(&self.batch_processor);
let stats = Arc::clone(&self.stats);
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_micros(100)); // 100μs间隔
loop {
interval.tick().await;
if let Ok(processed) = processor.execute_batch().await {
if processed > 0 {
stats.syscalls_bypassed.fetch_add(processed as u64, Ordering::Relaxed);
@@ -593,11 +594,11 @@ impl SystemCallBypassManager {
}
}
});
tracing::info!(target: "sol_trade_sdk","✅ Batch processing worker started");
Ok(())
}
/// 获取绕过统计
pub fn get_bypass_stats(&self) -> SyscallBypassStatsSnapshot {
SyscallBypassStatsSnapshot {
@@ -608,7 +609,7 @@ impl SystemCallBypassManager {
memory_operations_avoided: self.stats.memory_operations_avoided.load(Ordering::Relaxed),
}
}
/// 🚀 极致优化配置
pub fn extreme_bypass_config() -> SyscallBypassConfig {
SyscallBypassConfig {
@@ -643,9 +644,11 @@ impl SyscallBypassStatsSnapshot {
tracing::info!(target: "sol_trade_sdk"," ⏰ Time Calls Cached: {}", self.time_calls_cached);
tracing::info!(target: "sol_trade_sdk"," 📁 I/O Operations Optimized: {}", self.io_operations_optimized);
tracing::info!(target: "sol_trade_sdk"," 💾 Memory Operations Avoided: {}", self.memory_operations_avoided);
let total_optimizations = self.syscalls_bypassed + self.time_calls_cached +
self.io_operations_optimized + self.memory_operations_avoided;
let total_optimizations = self.syscalls_bypassed
+ self.time_calls_cached
+ self.io_operations_optimized
+ self.memory_operations_avoided;
tracing::info!(target: "sol_trade_sdk"," 🏆 Total Optimizations: {}", total_optimizations);
}
}
@@ -657,7 +660,7 @@ macro_rules! bypass_syscall {
// 使用快速时间而不是系统调用
crate::performance::syscall_bypass::GLOBAL_TIME_PROVIDER.fast_now_nanos()
};
(batch_io $ops:expr) => {
// 批量提交I/O操作
crate::performance::syscall_bypass::GLOBAL_BYPASS_MANAGER.submit_batch_io($ops).await
@@ -667,60 +670,57 @@ macro_rules! bypass_syscall {
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_fast_time_provider() {
let provider = FastTimeProvider::new(false).unwrap();
let time1 = provider.fast_now_nanos();
tokio::time::sleep(Duration::from_millis(1)).await;
let time2 = provider.fast_now_nanos();
assert!(time2 > time1);
assert!(time2 - time1 >= 1_000_000); // 至少1ms差异
}
#[tokio::test]
#[tokio::test]
async fn test_syscall_batch_processor() {
let processor = SyscallBatchProcessor::new(10).unwrap();
let request = SyscallRequest::Write {
fd: 1,
data: vec![1, 2, 3, 4, 5],
};
let request = SyscallRequest::Write { fd: 1, data: vec![1, 2, 3, 4, 5] };
processor.submit_request(request).unwrap();
let processed = processor.execute_batch().await.unwrap();
assert_eq!(processed, 1);
}
#[tokio::test]
async fn test_io_optimizer() {
let config = SyscallBypassConfig::default();
let optimizer = IOOptimizer::new(&config).unwrap();
let requests = vec![(1, b"test data".as_ref())];
let results = optimizer.batch_async_write(&requests).await.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0], 9); // "test data".len()
}
#[tokio::test]
async fn test_system_call_bypass_manager() {
let config = SyscallBypassConfig::default();
let manager = SystemCallBypassManager::new(config).unwrap();
// 测试快速时间戳
let timestamp = manager.fast_timestamp_nanos();
assert!(timestamp > 0);
// 测试统计
let stats = manager.get_bypass_stats();
assert_eq!(stats.time_calls_cached, 1);
}
#[test]
fn test_extreme_bypass_config() {
let config = SystemCallBypassManager::extreme_bypass_config();
@@ -731,16 +731,16 @@ mod tests {
assert_eq!(config.batch_size, 1000);
assert_eq!(config.syscall_cache_size, 10000);
}
#[test]
fn test_userspace_allocation() {
let config = SyscallBypassConfig::default();
let manager = SystemCallBypassManager::new(config).unwrap();
let ptr = manager.fast_allocate(64).unwrap();
assert!(!ptr.is_null());
let stats = manager.get_bypass_stats();
assert_eq!(stats.memory_operations_avoided, 1);
}
}
}
+133 -126
View File
@@ -1,5 +1,5 @@
//! 🚀 零拷贝内存映射IO - 完全消除数据拷贝开销
//!
//!
//! 实现极致的零拷贝策略,包括:
//! - 内存映射文件IO
//! - 共享内存环形缓冲区
@@ -10,11 +10,11 @@
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
// use std::mem::{size_of, MaybeUninit};
use anyhow::{Context, Result};
use crossbeam_utils::CachePadded;
use memmap2::{MmapMut, MmapOptions};
use std::ptr::NonNull;
use std::slice;
use memmap2::{MmapMut, MmapOptions};
use anyhow::{Result, Context};
use crossbeam_utils::CachePadded;
/// 🚀 零拷贝内存管理器
pub struct ZeroCopyMemoryManager {
@@ -50,17 +50,17 @@ impl SharedMemoryPool {
// 确保块大小是64字节对齐(缓存行对齐)
let aligned_block_size = (block_size + 63) & !63;
let total_blocks = total_size / aligned_block_size;
// 创建内存映射文件
let memory_region = MmapOptions::new()
.len(total_blocks * aligned_block_size)
.map_anon()
.context("Failed to create memory mapped region")?;
// 初始化空闲块位图 (每个u64可以管理64个块)
let bitmap_size = (total_blocks + 63) / 64;
let mut free_blocks = Vec::with_capacity(bitmap_size);
// 将所有块标记为空闲(全1)
for i in 0..bitmap_size {
let bits = if i == bitmap_size - 1 && total_blocks % 64 != 0 {
@@ -72,10 +72,10 @@ impl SharedMemoryPool {
};
free_blocks.push(AtomicU64::new(bits));
}
tracing::info!(target: "sol_trade_sdk","🚀 Created shared memory pool {} with {} blocks of {} bytes each",
pool_id, total_blocks, aligned_block_size);
Ok(Self {
memory_region,
free_blocks,
@@ -85,31 +85,31 @@ impl SharedMemoryPool {
pool_id,
})
}
/// 🚀 零拷贝分配内存块
#[inline(always)]
pub fn allocate_block(&self) -> Option<ZeroCopyBlock> {
// 快速路径:尝试从预期位置分配
let start_index = self.allocator_head.load(Ordering::Relaxed) / 64;
// 遍历所有位图寻找空闲块
for attempt in 0..self.free_blocks.len() {
let bitmap_index = (start_index + attempt) % self.free_blocks.len();
let bitmap = &self.free_blocks[bitmap_index];
let mut current = bitmap.load(Ordering::Acquire);
while current != 0 {
// 找到最低位的1(最小的空闲块)
let bit_pos = current.trailing_zeros() as usize;
let mask = 1u64 << bit_pos;
// 尝试原子地清除这一位(标记为已分配)
match bitmap.compare_exchange_weak(
current,
current,
current & !mask,
Ordering::AcqRel,
Ordering::Relaxed
Ordering::Relaxed,
) {
Ok(_) => {
// 成功分配
@@ -119,20 +119,17 @@ impl SharedMemoryPool {
bitmap.fetch_or(mask, Ordering::Relaxed);
break;
}
let offset = block_index * self.block_size;
let ptr = unsafe {
NonNull::new_unchecked(
self.memory_region.as_ptr().add(offset) as *mut u8
)
};
// 更新分配器头指针
self.allocator_head.store(
(block_index + 1) * 64,
Ordering::Relaxed
);
self.allocator_head.store((block_index + 1) * 64, Ordering::Relaxed);
return Some(ZeroCopyBlock {
ptr,
size: self.block_size,
@@ -147,10 +144,10 @@ impl SharedMemoryPool {
}
}
}
None // 没有可用块
}
/// 🚀 零拷贝释放内存块
#[inline(always)]
pub fn deallocate_block(&self, block: ZeroCopyBlock) {
@@ -158,20 +155,21 @@ impl SharedMemoryPool {
tracing::error!(target: "sol_trade_sdk", "Attempting to deallocate block from wrong pool");
return;
}
let bitmap_index = block.block_index / 64;
let bit_pos = block.block_index % 64;
let mask = 1u64 << bit_pos;
if bitmap_index < self.free_blocks.len() {
// 原子地设置位为1(标记为空闲)
self.free_blocks[bitmap_index].fetch_or(mask, Ordering::Release);
}
}
/// 获取可用块数量
pub fn available_blocks(&self) -> usize {
self.free_blocks.iter()
self.free_blocks
.iter()
.map(|bitmap| bitmap.load(Ordering::Relaxed).count_ones() as usize)
.sum()
}
@@ -195,49 +193,49 @@ impl ZeroCopyBlock {
pub fn as_ptr(&self) -> *mut u8 {
self.ptr.as_ptr()
}
/// 获取只读切片
#[inline(always)]
pub unsafe fn as_slice(&self) -> &[u8] {
slice::from_raw_parts(self.ptr.as_ptr(), self.size)
}
/// 获取可变切片
#[inline(always)]
pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
slice::from_raw_parts_mut(self.ptr.as_ptr(), self.size)
}
/// 获取块大小
#[inline(always)]
pub fn size(&self) -> usize {
self.size
}
/// 零拷贝写入数据
#[inline(always)]
pub unsafe fn write_bytes(&mut self, data: &[u8]) -> Result<()> {
if data.len() > self.size {
return Err(anyhow::anyhow!("Data too large for block"));
}
// 使用硬件优化的内存拷贝
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
self.ptr.as_ptr(),
data.as_ptr(),
data.len()
data.len(),
);
Ok(())
}
/// 零拷贝读取数据
#[inline(always)]
pub unsafe fn read_bytes(&self, len: usize) -> Result<&[u8]> {
if len > self.size {
return Err(anyhow::anyhow!("Read length exceeds block size"));
}
Ok(slice::from_raw_parts(self.ptr.as_ptr(), len))
}
}
@@ -266,9 +264,9 @@ impl MemoryMappedBuffer {
.len(size)
.map_anon()
.context("Failed to create memory mapped buffer")?;
tracing::info!(target: "sol_trade_sdk","🚀 Created memory mapped buffer {} with size {} bytes", buffer_id, size);
Ok(Self {
mmap,
read_pos: CachePadded::new(AtomicUsize::new(0)),
@@ -277,128 +275,136 @@ impl MemoryMappedBuffer {
_buffer_id: buffer_id,
})
}
/// 🚀 零拷贝写入数据
#[inline(always)]
pub fn write_data(&self, data: &[u8]) -> Result<usize> {
let data_len = data.len();
let current_write = self.write_pos.load(Ordering::Relaxed);
let current_read = self.read_pos.load(Ordering::Acquire);
// 计算可用空间
let available_space = if current_write >= current_read {
self.size - (current_write - current_read) - 1
} else {
current_read - current_write - 1
};
if data_len > available_space {
return Err(anyhow::anyhow!("Insufficient buffer space"));
}
// 零拷贝写入
unsafe {
let write_ptr = self.mmap.as_ptr().add(current_write) as *mut u8;
if current_write + data_len <= self.size {
// 数据不跨越缓冲区边界
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
write_ptr, data.as_ptr(), data_len
write_ptr,
data.as_ptr(),
data_len,
);
} else {
// 数据跨越缓冲区边界,分两段写入
let first_part = self.size - current_write;
let second_part = data_len - first_part;
// 写入第一部分
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
write_ptr, data.as_ptr(), first_part
write_ptr,
data.as_ptr(),
first_part,
);
// 写入第二部分(从缓冲区开头)
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
self.mmap.as_ptr() as *mut u8,
data.as_ptr().add(first_part),
second_part
self.mmap.as_ptr() as *mut u8,
data.as_ptr().add(first_part),
second_part,
);
}
}
// 更新写指针
let new_write_pos = (current_write + data_len) % self.size;
self.write_pos.store(new_write_pos, Ordering::Release);
Ok(data_len)
}
/// 🚀 零拷贝读取数据
#[inline(always)]
pub fn read_data(&self, buffer: &mut [u8]) -> Result<usize> {
let buffer_len = buffer.len();
let current_read = self.read_pos.load(Ordering::Relaxed);
let current_write = self.write_pos.load(Ordering::Acquire);
// 计算可读数据量
let available_data = if current_write >= current_read {
current_write - current_read
} else {
self.size - (current_read - current_write)
};
if available_data == 0 {
return Ok(0); // 无数据可读
}
let read_len = buffer_len.min(available_data);
// 零拷贝读取
unsafe {
let read_ptr = self.mmap.as_ptr().add(current_read);
if current_read + read_len <= self.size {
// 数据不跨越缓冲区边界
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
buffer.as_mut_ptr(), read_ptr, read_len
buffer.as_mut_ptr(),
read_ptr,
read_len,
);
} else {
// 数据跨越缓冲区边界,分两段读取
let first_part = self.size - current_read;
let second_part = read_len - first_part;
// 读取第一部分
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
buffer.as_mut_ptr(), read_ptr, first_part
buffer.as_mut_ptr(),
read_ptr,
first_part,
);
// 读取第二部分(从缓冲区开头)
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
buffer.as_mut_ptr().add(first_part),
self.mmap.as_ptr(),
second_part
self.mmap.as_ptr(),
second_part,
);
}
}
// 更新读指针
let new_read_pos = (current_read + read_len) % self.size;
self.read_pos.store(new_read_pos, Ordering::Release);
Ok(read_len)
}
/// 获取可读数据量
#[inline(always)]
pub fn available_data(&self) -> usize {
let current_read = self.read_pos.load(Ordering::Relaxed);
let current_write = self.write_pos.load(Ordering::Relaxed);
if current_write >= current_read {
current_write - current_read
} else {
self.size - (current_read - current_write)
}
}
/// 获取可用空间
#[inline(always)]
pub fn available_space(&self) -> usize {
@@ -420,38 +426,39 @@ impl DirectMemoryAccessManager {
/// 创建DMA管理器
pub fn new(num_channels: usize) -> Result<Self> {
let mut dma_channels = Vec::with_capacity(num_channels);
for i in 0..num_channels {
dma_channels.push(Arc::new(DMAChannel::new(i)?));
}
tracing::info!(target: "sol_trade_sdk","🚀 Created DMA manager with {} channels", num_channels);
Ok(Self {
dma_channels,
channel_allocator: AtomicUsize::new(0),
dma_stats: Arc::new(DMAStats::new()),
})
}
/// 🚀 执行零拷贝DMA传输
#[inline(always)]
pub async fn dma_transfer(&self, src: &[u8], dst: &mut [u8]) -> Result<usize> {
if src.len() != dst.len() {
return Err(anyhow::anyhow!("Source and destination sizes don't match"));
}
// 选择DMA通道(轮询分配)
let channel_index = self.channel_allocator.fetch_add(1, Ordering::Relaxed) % self.dma_channels.len();
let channel_index =
self.channel_allocator.fetch_add(1, Ordering::Relaxed) % self.dma_channels.len();
let channel = &self.dma_channels[channel_index];
// 执行DMA传输
let transferred = channel.transfer(src, dst).await?;
// 更新统计
self.dma_stats.bytes_transferred.fetch_add(transferred as u64, Ordering::Relaxed);
self.dma_stats.transfers_completed.fetch_add(1, Ordering::Relaxed);
Ok(transferred)
}
}
@@ -475,21 +482,21 @@ impl DMAChannel {
_status: AtomicU64::new(0),
})
}
/// 🚀 执行零拷贝传输
#[inline(always)]
pub async fn transfer(&self, src: &[u8], dst: &mut [u8]) -> Result<usize> {
let transfer_size = src.len();
// 使用硬件优化的SIMD内存拷贝
unsafe {
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
dst.as_mut_ptr(),
src.as_ptr(),
transfer_size
transfer_size,
);
}
Ok(transfer_size)
}
}
@@ -541,14 +548,14 @@ impl ZeroCopyStats {
mmap_buffer_usage: AtomicU64::new(0),
}
}
/// 打印统计信息
pub fn print_stats(&self) {
let allocated = self.blocks_allocated.load(Ordering::Relaxed);
let freed = self.blocks_freed.load(Ordering::Relaxed);
let bytes = self.bytes_transferred.load(Ordering::Relaxed);
let mmap_usage = self.mmap_buffer_usage.load(Ordering::Relaxed);
tracing::info!(target: "sol_trade_sdk","🚀 Zero-Copy Stats:");
tracing::info!(target: "sol_trade_sdk"," 📦 Blocks: Allocated={}, Freed={}, Active={}",
allocated, freed, allocated.saturating_sub(freed));
@@ -564,36 +571,36 @@ impl ZeroCopyMemoryManager {
pub fn new() -> Result<Self> {
let mut shared_pools = Vec::new();
let mut mmap_buffers = Vec::new();
// 创建不同大小的内存池
// 小块池: 64KB blocks, 1GB total
shared_pools.push(Arc::new(SharedMemoryPool::new(0, 1024 * 1024 * 1024, 64 * 1024)?));
// 中块池: 1MB blocks, 4GB total
// 中块池: 1MB blocks, 4GB total
shared_pools.push(Arc::new(SharedMemoryPool::new(1, 4 * 1024 * 1024 * 1024, 1024 * 1024)?));
// 大块池: 16MB blocks, 8GB total
shared_pools.push(Arc::new(SharedMemoryPool::new(2, 8 * 1024 * 1024 * 1024, 16 * 1024 * 1024)?));
shared_pools.push(Arc::new(SharedMemoryPool::new(
2,
8 * 1024 * 1024 * 1024,
16 * 1024 * 1024,
)?));
// 创建内存映射缓冲区
for i in 0..8 {
mmap_buffers.push(Arc::new(MemoryMappedBuffer::new(i, 256 * 1024 * 1024)?)); // 256MB each
mmap_buffers.push(Arc::new(MemoryMappedBuffer::new(i, 256 * 1024 * 1024)?));
// 256MB each
}
let dma_manager = Arc::new(DirectMemoryAccessManager::new(16)?); // 16 DMA channels
let stats = Arc::new(ZeroCopyStats::new());
tracing::info!(target: "sol_trade_sdk","🚀 Zero-Copy Memory Manager initialized");
tracing::info!(target: "sol_trade_sdk"," 📦 Memory Pools: {}", shared_pools.len());
tracing::info!(target: "sol_trade_sdk"," 💾 Mapped Buffers: {}", mmap_buffers.len());
tracing::info!(target: "sol_trade_sdk"," 🔄 DMA Channels: 16");
Ok(Self {
shared_pools,
mmap_buffers,
dma_manager,
stats,
})
Ok(Self { shared_pools, mmap_buffers, dma_manager, stats })
}
/// 🚀 分配零拷贝内存块
#[inline(always)]
pub fn allocate(&self, size: usize) -> Option<ZeroCopyBlock> {
@@ -605,7 +612,7 @@ impl ZeroCopyMemoryManager {
} else {
&self.shared_pools[2] // 大块池
};
if let Some(block) = pool.allocate_block() {
self.stats.blocks_allocated.fetch_add(1, Ordering::Relaxed);
Some(block)
@@ -613,7 +620,7 @@ impl ZeroCopyMemoryManager {
None
}
}
/// 🚀 释放零拷贝内存块
#[inline(always)]
pub fn deallocate(&self, block: ZeroCopyBlock) {
@@ -623,19 +630,19 @@ impl ZeroCopyMemoryManager {
self.stats.blocks_freed.fetch_add(1, Ordering::Relaxed);
}
}
/// 获取内存映射缓冲区
#[inline(always)]
pub fn get_mmap_buffer(&self, buffer_id: usize) -> Option<Arc<MemoryMappedBuffer>> {
self.mmap_buffers.get(buffer_id).cloned()
}
/// 获取DMA管理器
#[inline(always)]
pub fn get_dma_manager(&self) -> Arc<DirectMemoryAccessManager> {
self.dma_manager.clone()
}
/// 获取统计信息
pub fn get_stats(&self) -> Arc<ZeroCopyStats> {
self.stats.clone()
@@ -645,73 +652,73 @@ impl ZeroCopyMemoryManager {
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_shared_memory_pool() -> Result<()> {
let pool = SharedMemoryPool::new(0, 1024 * 1024, 4096)?;
// 测试分配
let block1 = pool.allocate_block().expect("Should allocate block");
assert_eq!(block1.size(), 4096);
let block2 = pool.allocate_block().expect("Should allocate another block");
assert_eq!(block2.size(), 4096);
// 测试释放
pool.deallocate_block(block1);
pool.deallocate_block(block2);
Ok(())
}
#[tokio::test]
async fn test_memory_mapped_buffer() -> Result<()> {
let buffer = MemoryMappedBuffer::new(0, 1024 * 1024)?;
let test_data = b"Hello, Zero-Copy World!";
// 测试写入
let written = buffer.write_data(test_data)?;
assert_eq!(written, test_data.len());
// 测试读取
let mut read_buffer = vec![0u8; test_data.len()];
let read = buffer.read_data(&mut read_buffer)?;
assert_eq!(read, test_data.len());
assert_eq!(&read_buffer, test_data);
Ok(())
}
#[tokio::test]
async fn test_dma_transfer() -> Result<()> {
let dma_manager = DirectMemoryAccessManager::new(4)?;
let src = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
let mut dst = vec![0u8; 8];
let transferred = dma_manager.dma_transfer(&src, &mut dst).await?;
assert_eq!(transferred, 8);
assert_eq!(src, dst);
Ok(())
}
#[tokio::test]
async fn test_zero_copy_manager() -> Result<()> {
let manager = ZeroCopyMemoryManager::new()?;
// 测试小块分配
let small_block = manager.allocate(1024).expect("Should allocate small block");
assert_eq!(small_block.size(), 65536); // 小块池的块大小
// 测试大块分配
let large_block = manager.allocate(5 * 1024 * 1024).expect("Should allocate large block");
assert_eq!(large_block.size(), 16 * 1024 * 1024); // 大块池的块大小
manager.deallocate(small_block);
manager.deallocate(large_block);
Ok(())
}
}
}
+85 -41
View File
@@ -2,20 +2,20 @@ use crate::swqos::common::{default_http_client_builder, poll_transaction_confirm
use rand::seq::IndexedRandom;
use reqwest::Client;
use std::{sync::Arc, time::Instant};
use tracing::{error, info, warn};
use tracing::warn;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use bincode::serialize as bincode_serialize;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use std::time::Duration;
use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
/// Empty body for getHealth POST; avoid per-request allocation.
static PING_BODY: &[u8] = &[];
@@ -42,11 +42,21 @@ pub struct AstralaneClient {
#[async_trait::async_trait]
impl SwqosClientTrait for AstralaneClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
}
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<()> {
for transaction in transactions {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
}
@@ -54,7 +64,10 @@ impl SwqosClientTrait for AstralaneClient {
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *ASTRALANE_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ASTRALANE_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *ASTRALANE_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| ASTRALANE_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -100,36 +113,48 @@ impl AstralaneClient {
async fn start_ping_task(&self) {
match &self.backend {
AstralaneBackend::Http { endpoint, auth_token, http_client, ping_handle, stop_ping } => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let http_client = http_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
warn!(target: "sol_trade_sdk", "Astralane ping request failed: {}", e);
AstralaneBackend::Http {
endpoint,
auth_token,
http_client,
ping_handle,
stop_ping,
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let http_client = http_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) =
Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
warn!(target: "sol_trade_sdk", "Astralane ping request failed: {}", e);
}
}
});
let mut guard = ping_handle.lock().await;
if let Some(old) = guard.as_ref() {
old.abort();
}
});
let mut guard = ping_handle.lock().await;
if let Some(old) = guard.as_ref() {
old.abort();
}
*guard = Some(handle);
*guard = Some(handle);
}
AstralaneBackend::Quic(_) => {}
}
}
/// Send ping request: POST endpoint?api-key=...&method=getHealth
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<()> {
let response = http_client
.post(endpoint)
.query(&[("api-key", auth_token), ("method", "getHealth")])
@@ -145,10 +170,16 @@ impl AstralaneClient {
Ok(())
}
async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction_impl(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let signature = transaction.get_signature();
let body_bytes = bincode_serialize(transaction).map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
let body_bytes = bincode_serialize(transaction)
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
match &self.backend {
AstralaneBackend::Http { endpoint, auth_token, http_client, .. } => {
@@ -162,15 +193,26 @@ impl AstralaneClient {
let status = response.status();
let _ = response.bytes().await;
if status.is_success() {
info!(target: "sol_trade_sdk", "[astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("Astralane", trade_type, start_time.elapsed());
}
} else {
error!(target: "sol_trade_sdk", "[astralane] {} submission failed: status {}", trade_type, status);
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Astralane", trade_type, start_time.elapsed(), format!("status {}", status));
}
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
}
}
AstralaneBackend::Quic(quic) => {
quic.send_transaction(&body_bytes).await?;
info!(target: "sol_trade_sdk", "[astralane-quic] {} submitted: {:?}", trade_type, start_time.elapsed());
if let Err(e) = quic.send_transaction(&body_bytes).await {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Astralane", trade_type, start_time.elapsed(), &e);
}
return Err(e);
}
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("Astralane", trade_type, start_time.elapsed());
}
}
}
@@ -178,14 +220,16 @@ impl AstralaneClient {
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
info!(target: "sol_trade_sdk", "signature: {:?}", signature);
error!(target: "sol_trade_sdk", "[astralane] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [{:width$}] {} confirmation failed: {:?}", "Astralane", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
return Err(e);
}
}
if wait_confirmation {
info!(target: "sol_trade_sdk", "signature: {:?}", signature);
info!(target: "sol_trade_sdk", "[astralane] {} confirmed: {:?}", trade_type, start_time.elapsed());
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [{:width$}] {} confirmed: {:?}", "Astralane", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
+18 -26
View File
@@ -49,14 +49,16 @@ impl AstralaneQuicClient {
/// Generates a self-signed TLS certificate with the API key as the Common Name (CN).
pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
let _ = rustls::crypto::ring::default_provider().install_default();
let addr = SocketAddr::from_str(server_addr).or_else(|_| {
use std::net::ToSocketAddrs;
server_addr
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.ok_or_else(|| anyhow::anyhow!("Cannot resolve address: {}", server_addr))
}).context("Invalid server address")?;
let addr = SocketAddr::from_str(server_addr)
.or_else(|_| {
use std::net::ToSocketAddrs;
server_addr
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.ok_or_else(|| anyhow::anyhow!("Cannot resolve address: {}", server_addr))
})
.context("Invalid server address")?;
info!("[astralane-quic] Building TLS config (CN = api_key)");
let client_config = Self::build_client_config(api_key)?;
@@ -116,10 +118,8 @@ impl AstralaneQuicClient {
guard.clone()
};
let mut send_stream = conn
.open_uni()
.await
.context("Failed to open unidirectional stream")?;
let mut send_stream =
conn.open_uni().await.context("Failed to open unidirectional stream")?;
send_stream
.write_all(transaction_bytes)
@@ -154,19 +154,15 @@ impl AstralaneQuicClient {
/// Close the connection gracefully.
pub async fn close(&self) {
self.connection
.lock()
.await
.close(error_code::OK.into(), b"client closing");
self.connection.lock().await.close(error_code::OK.into(), b"client closing");
}
fn build_client_config(api_key: &str) -> Result<ClientConfig> {
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
let mut cert_params = CertificateParams::new(vec![])?;
cert_params.distinguished_name.push(
rcgen::DnType::CommonName,
rcgen::DnValue::Utf8String(api_key.to_string()),
);
cert_params
.distinguished_name
.push(rcgen::DnType::CommonName, rcgen::DnValue::Utf8String(api_key.to_string()));
let cert = cert_params.self_signed(&key_pair)?;
let cert_der = CertificateDer::from(cert.der().to_vec());
@@ -181,9 +177,7 @@ impl AstralaneQuicClient {
crypto.alpn_protocols = vec![ALPN_ASTRALANE_TPU.to_vec()];
let mut transport = TransportConfig::default();
transport.max_idle_timeout(Some(
IdleTimeout::try_from(Duration::from_secs(30)).unwrap(),
));
transport.max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(30)).unwrap()));
transport.keep_alive_interval(Some(Duration::from_secs(25)));
let mut client_config =
@@ -196,9 +190,7 @@ impl AstralaneQuicClient {
impl Drop for AstralaneQuicClient {
fn drop(&mut self) {
self.connection
.get_mut()
.close(error_code::OK.into(), b"client closing");
self.connection.get_mut().close(error_code::OK.into(), b"client closing");
}
}
+339 -112
View File
@@ -1,43 +1,137 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::BLOCKRAZOR_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
use tonic::transport::Channel;
use tonic::metadata::AsciiMetadataValue;
// Include pre-generated gRPC code
pub mod serverpb {
include!("pb/serverpb.rs");
}
// gRPC client wrapper
#[derive(Clone)]
pub struct BlockRazorGrpcClient {
channel: Channel,
auth_token: String,
}
impl BlockRazorGrpcClient {
pub fn new(channel: Channel, auth_token: String) -> Self {
Self { channel, auth_token }
}
pub async fn get_health(&self) -> Result<String> {
let mut client = serverpb::server_client::ServerClient::new(self.channel.clone());
let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str())
.map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?;
let mut request = tonic::Request::new(serverpb::HealthRequest {});
request.metadata_mut().insert("apikey", apikey);
let response = client.get_health(request).await
.map_err(|e| anyhow::anyhow!("gRPC health check failed: {}", e))?;
Ok(response.into_inner().status)
}
pub async fn send_transaction(
&self,
transaction: String,
mode: String,
safe_window: Option<i32>,
revert_protection: bool,
) -> Result<String> {
// 检查交易数据大小
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor transaction size: {} bytes", transaction.len());
}
let mut client = serverpb::server_client::ServerClient::new(self.channel.clone());
let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str())
.map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?;
let mut request = tonic::Request::new(serverpb::SendRequest {
transaction,
mode: String::from(mode),
safe_window,
revert_protection,
});
request.metadata_mut().insert("apikey", apikey);
let response = client.send_transaction(request).await
.map_err(|e| anyhow::anyhow!("gRPC send transaction failed: {}", e))?;
Ok(response.into_inner().signature)
}
}
#[derive(Clone)]
pub enum BlockRazorBackend {
Grpc {
endpoint: String,
auth_token: String,
grpc_client: Arc<BlockRazorGrpcClient>,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
},
Http {
endpoint: String,
auth_token: String,
http_client: Client,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
},
}
#[derive(Clone)]
pub struct BlockRazorClient {
pub endpoint: String,
pub auth_token: String,
pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client,
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
pub stop_ping: Arc<AtomicBool>,
backend: BlockRazorBackend,
}
#[async_trait::async_trait]
impl SwqosClientTrait for BlockRazorClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
self.send_transactions(trade_type, transactions, 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_impl(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *BLOCKRAZOR_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *BLOCKRAZOR_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -47,72 +141,155 @@ impl SwqosClientTrait for BlockRazorClient {
}
impl BlockRazorClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
// 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR
Ok(Self::new_http(rpc_url, endpoint, auth_token))
}
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
// 官方文档:請求中唯一允許的 header 是 Content-Type: text/plain;避免默认 User-Agent 等导致 500
let http_client = default_http_client_builder()
.user_agent("")
.build()
.unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
// 配置 Channel,增加连接超时
let channel = tonic::transport::Channel::from_shared(endpoint.clone())
.map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))?
.timeout(Duration::from_secs(30))
.connect()
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
let grpc_client = Arc::new(BlockRazorGrpcClient::new(channel, auth_token.clone()));
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
let stop_ping = Arc::new(AtomicBool::new(false));
let client = Self {
rpc_client: Arc::new(rpc_client),
backend: BlockRazorBackend::Grpc {
endpoint,
auth_token,
grpc_client,
ping_handle,
stop_ping,
},
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
Ok(client)
}
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().user_agent("").build().unwrap();
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
let stop_ping = Arc::new(AtomicBool::new(false));
let client = Self {
rpc_client: Arc::new(rpc_client),
backend: BlockRazorBackend::Http {
endpoint,
auth_token,
http_client,
ping_handle,
stop_ping,
},
};
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
/// Start periodic ping task to keep connections active
async fn start_ping_task(&self) {
let endpoint = self.endpoint.clone();
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor ping request failed: {}", e);
match &self.backend {
BlockRazorBackend::Grpc {
grpc_client,
ping_handle,
stop_ping,
..
} => {
let grpc_client = grpc_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let handle = tokio::spawn(async move {
if let Err(e) = grpc_client.get_health().await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = grpc_client.get_health().await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC ping request failed: {}", e);
}
}
}
});
let mut ping_guard = ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
}
*ping_guard = Some(handle);
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
BlockRazorBackend::Http {
endpoint,
auth_token,
http_client,
ping_handle,
stop_ping,
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let http_client = http_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let handle = tokio::spawn(async move {
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor HTTP ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor HTTP ping request failed: {}", e);
}
}
}
});
let mut ping_guard = ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
}
*ping_guard = Some(handle);
}
*ping_guard = Some(handle);
}
}
/// 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_http_ping(
http_client: &Client,
endpoint: &str,
auth_token: &str,
) -> Result<()> {
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
let response = http_client
.post(&ping_url)
@@ -125,84 +302,134 @@ impl BlockRazorClient {
let status = response.status();
let _ = response.bytes().await;
if !status.is_success() {
eprintln!("BlockRazor ping request failed with status: {}", status);
eprintln!("BlockRazor HTTP ping request failed with status: {}", status);
}
Ok(())
}
/// Send transaction via v2 API: plain Base64 body, Content-Type: text/plain. Only required URI param: auth.
/// 文档要求:auth 以 URI 参数传入;body 为纯 Base64 编码交易;唯一允许的 header 为 Content-Type: text/plain。
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction_impl(
&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 response = self.http_client
.post(&self.endpoint)
.query(&[("auth", self.auth_token.as_str())])
.header("Content-Type", "text/plain")
.body(content)
.send()
.await?;
match &self.backend {
BlockRazorBackend::Grpc {
grpc_client,
..
} => {
let (content, _signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let status = response.status();
if status.is_success() {
let _ = response.bytes().await;
if crate::common::sdk_log::sdk_log_enabled() {
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
let signature = grpc_client.send_transaction(
content,
"fast".to_string(),
None,
false,
).await;
match signature {
Ok(sig) => {
if !sig.is_empty() {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("BlockRazor", trade_type, start_time.elapsed());
}
} else {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("BlockRazor", trade_type, start_time.elapsed(), "empty signature".to_string());
}
return Err(anyhow::anyhow!("BlockRazor gRPC returned empty signature"));
}
}
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("BlockRazor", trade_type, start_time.elapsed(), format!("gRPC error: {}", e));
}
return Err(anyhow::anyhow!("BlockRazor gRPC sendTransaction failed: {}", e));
}
}
}
} else {
let body = response.text().await.unwrap_or_default();
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [blockrazor] {} submission failed: status {} body: {}", trade_type, status, body);
BlockRazorBackend::Http {
endpoint,
auth_token,
http_client,
..
} => {
let (content, _signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let response = http_client
.post(endpoint)
.query(&[("auth", auth_token.as_str())])
.header("Content-Type", "text/plain")
.body(content)
.send()
.await?;
let status = response.status();
if status.is_success() {
let _ = response.bytes().await;
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("blockrazor", trade_type, start_time.elapsed());
}
} else {
let body = response.text().await.unwrap_or_default();
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("blockrazor", trade_type, start_time.elapsed(), format!("status {} body: {}", status, body));
}
return Err(anyhow::anyhow!(
"BlockRazor HTTP sendTransaction failed: status {} body: {}",
status,
body
));
}
}
return Err(anyhow::anyhow!(
"BlockRazor sendTransaction failed: status {} body: {}",
status,
body
));
}
let start_time = Instant::now();
let signature = transaction.signatures[0];
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [blockrazor] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"blockrazor",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [blockrazor] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "blockrazor", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub 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(())
}
}
impl Drop for BlockRazorClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
tokio::spawn(async move {
let mut ping_guard = ping_handle.lock().await;
if let Some(handle) = ping_guard.as_ref() {
handle.abort();
match &self.backend {
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. } | BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
stop_ping.store(true, Ordering::Relaxed);
let ping_handle = ping_handle.clone();
tokio::spawn(async move {
let mut ping_guard = ping_handle.lock().await;
if let Some(handle) = ping_guard.as_ref() {
handle.abort();
}
*ping_guard = None;
});
}
*ping_guard = None;
});
}
}
}
+53 -20
View File
@@ -6,17 +6,16 @@ use rand::seq::IndexedRandom;
use reqwest::Client;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::BLOX_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct BloxrouteClient {
pub endpoint: String,
@@ -27,16 +26,29 @@ pub struct BloxrouteClient {
#[async_trait::async_trait]
impl SwqosClientTrait for BloxrouteClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *BLOX_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOX_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *BLOX_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| BLOX_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -56,9 +68,15 @@ impl BloxrouteClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Single format! for body to avoid json! + to_string() double allocation
let body = format!(
@@ -67,7 +85,9 @@ impl BloxrouteClient {
);
let endpoint = format!("{}/api/v2/submit", self.endpoint);
let response_text = self.http_client.post(&endpoint)
let response_text = self
.http_client
.post(&endpoint)
.body(body)
.header("Content-Type", "application/json")
.header("Authorization", self.auth_token.as_str())
@@ -80,13 +100,13 @@ impl BloxrouteClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if crate::common::sdk_log::sdk_log_enabled() {
if response_json.get("result").is_some() {
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("bloxroute", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, _error);
eprintln!(" [bloxroute] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
}
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("bloxroute", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -95,20 +115,31 @@ impl BloxrouteClient {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [bloxroute] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"bloxroute",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [bloxroute] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "bloxroute", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
_wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let contents = serialization::serialize_transactions_batch_sync(
@@ -123,7 +154,9 @@ impl BloxrouteClient {
let body = format!(r#"{{"entries":[{}]}}"#, entries);
let endpoint = format!("{}/api/v2/submit-batch", self.endpoint);
let response_text = self.http_client.post(&endpoint)
let response_text = self
.http_client
.post(&endpoint)
.body(body)
.header("Content-Type", "application/json")
.header("Authorization", self.auth_token.as_str())
@@ -137,11 +170,11 @@ impl BloxrouteClient {
if response_json.get("result").is_some() {
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" bloxroute {} submission failed: {:?}", trade_type, _error);
eprintln!(" bloxroute {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
}
}
}
Ok(())
}
}
}
+23 -11
View File
@@ -1,9 +1,9 @@
use crate::common::types::SolanaRpcClient;
use crate::swqos::serialization;
use anyhow::Result;
use base64::engine::general_purpose::{self, STANDARD};
use base64::Engine;
use bincode::serialize;
use crate::swqos::serialization;
use reqwest::Client;
use serde_json;
use serde_json::json;
@@ -117,7 +117,11 @@ pub async fn poll_any_transaction_confirmation(
loop {
if start.elapsed() >= timeout {
return Err(anyhow::anyhow!("Transaction confirmation timed out after {}s ({} signatures polled)", timeout.as_secs(), signatures.len()));
return Err(anyhow::anyhow!(
"Transaction confirmation timed out after {}s ({} signatures polled)",
timeout.as_secs(),
signatures.len()
));
}
poll_count += 1;
@@ -205,7 +209,7 @@ pub async fn poll_any_transaction_confirmation(
let ui_err = meta.err.unwrap();
let tx_err: TransactionError =
serde_json::from_value(serde_json::to_value(&ui_err)?)?;
// Use Solana InstructionError codes directly
let mut code = 0u32;
let mut index = None;
@@ -230,7 +234,7 @@ pub async fn poll_any_transaction_confirmation(
}
_ => {}
}
return Err(anyhow::Error::new(TradeError {
code: code,
message: format!("{} {:?}", tx_err, error_msg),
@@ -241,11 +245,16 @@ pub async fn poll_any_transaction_confirmation(
}
}
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
pub async fn send_nb_transaction(
client: Client,
endpoint: &str,
auth_token: &str,
transaction: &Transaction,
) -> Result<Signature, anyhow::Error> {
// Serialize transaction
let serialized = bincode::serialize(transaction)
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
// Base64 encode
let encoded = STANDARD.encode(serialized);
@@ -266,18 +275,21 @@ pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &st
.await
.map_err(|e| anyhow::anyhow!("Request failed: {}", e))?;
let resp = response.json::<serde_json::Value>().await
let resp = response
.json::<serde_json::Value>()
.await
.map_err(|e| anyhow::anyhow!("Response parsing failed: {}", e))?;
if let Some(reason) = resp["reason"].as_str() {
return Err(anyhow::anyhow!(reason.to_string()));
}
let signature = resp["signature"].as_str()
let signature = resp["signature"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing signature field in response"))?;
let signature = Signature::from_str(signature)
.map_err(|e| anyhow::anyhow!("Invalid signature: {}", e))?;
let signature =
Signature::from_str(signature).map_err(|e| anyhow::anyhow!("Invalid signature: {}", e))?;
Ok(signature)
}
@@ -314,4 +326,4 @@ pub async fn serialize_smart_transaction_and_encode(
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
};
Ok((serialized, *signature))
}
}
+50 -17
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,14 +8,13 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::FLASHBLOCK_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct FlashBlockClient {
pub endpoint: String,
@@ -24,16 +25,29 @@ pub struct FlashBlockClient {
#[async_trait::async_trait]
impl SwqosClientTrait for FlashBlockClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *FLASHBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| FLASHBLOCK_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *FLASHBLOCK_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| FLASHBLOCK_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -49,9 +63,15 @@ impl FlashBlockClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// FlashBlock API format
let request_body = serde_json::to_string(&json!({
@@ -61,7 +81,9 @@ impl FlashBlockClient {
let url = format!("{}/api/v2/submit-batch", self.endpoint);
// Send request to FlashBlock
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body)
.header("Authorization", &self.auth_token)
.header("Content-Type", "application/json")
@@ -75,12 +97,12 @@ impl FlashBlockClient {
// Parse response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("success").is_some() || response_json.get("result").is_some() {
println!(" [FlashBlock] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("FlashBlock", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [FlashBlock] {} submission failed: {:?}", trade_type, _error);
eprintln!(" [FlashBlock] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
}
} else {
eprintln!(" [FlashBlock] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("FlashBlock", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -88,19 +110,30 @@ impl FlashBlockClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [FlashBlock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"FlashBlock",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [FlashBlock] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "FlashBlock", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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?;
}
+16 -32
View File
@@ -20,7 +20,9 @@ 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::constants::swqos::{
HELIUS_TIP_ACCOUNTS, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY,
};
use crate::swqos::{SwqosClientTrait, SwqosType, TradeType};
#[derive(Clone)]
@@ -43,12 +45,7 @@ impl HeliusClient {
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,
}
Self { submit_url, rpc_client: Arc::new(rpc_client), http_client, swqos_only }
}
/// Build URL once at construction; no per-request allocation.
@@ -109,8 +106,8 @@ impl HeliusClient {
if !status.is_success() {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} submission failed status={} body={}",
trade_type, status, response_text
" [helius] {} submission failed after {:?} status={} body={}",
trade_type, start_time.elapsed(), status, response_text
);
}
return Err(anyhow::anyhow!(
@@ -127,22 +124,15 @@ impl HeliusClient {
.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);
crate::common::sdk_log::log_swqos_submission_failed("helius", trade_type, start_time.elapsed(), 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()
);
crate::common::sdk_log::log_swqos_submitted("helius", trade_type, start_time.elapsed());
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} submission failed: {:?}",
trade_type, response_text
);
crate::common::sdk_log::log_swqos_submission_failed("helius", trade_type, start_time.elapsed(), response_text);
}
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
@@ -150,24 +140,19 @@ impl HeliusClient {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} confirmation failed: {:?}",
" [{:width$}] {} confirmation failed: {:?}",
"helius",
trade_type,
start_time.elapsed()
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(
" signature: {:?}",
signature
);
println!(
" [helius] {} confirmed: {:?}",
trade_type,
start_time.elapsed()
);
println!(" signature: {:?}", signature);
println!(" [{:width$}] {} confirmed: {:?}", "helius", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
@@ -191,8 +176,7 @@ impl SwqosClientTrait for HeliusClient {
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation)
.await?;
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
+44 -23
View File
@@ -1,5 +1,7 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
FormatBase64VersionedTransaction,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -7,14 +9,13 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::JITO_TIP_ACCOUNTS};
pub struct JitoClient {
pub endpoint: String,
pub auth_token: String,
@@ -24,11 +25,21 @@ pub struct JitoClient {
#[async_trait::async_trait]
impl SwqosClientTrait for JitoClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions_impl(trade_type, transactions, wait_confirmation).await
}
@@ -52,9 +63,15 @@ impl JitoClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction_impl(
&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 (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"id": 1,
@@ -76,8 +93,7 @@ impl JitoClient {
let response = if self.auth_token.is_empty() {
self.http_client.post(&endpoint)
} else {
self.http_client.post(&endpoint)
.header("x-jito-auth", &self.auth_token)
self.http_client.post(&endpoint).header("x-jito-auth", &self.auth_token)
};
let response_text = response
.body(request_body)
@@ -89,12 +105,12 @@ impl JitoClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" [jito] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("jito", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [jito] {} submission failed: {:?}", trade_type, _error);
eprintln!(" [jito] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
}
} else {
eprintln!(" [jito] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("jito", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -102,21 +118,27 @@ impl JitoClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [jito] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmation failed: {:?}", "jito", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [jito] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "jito", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions_impl(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions_impl(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
_wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let txs_base64 = transactions.iter().map(|tx| tx.to_base64_string()).collect::<Vec<String>>();
let txs_base64 =
transactions.iter().map(|tx| tx.to_base64_string()).collect::<Vec<String>>();
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "sendBundle",
@@ -135,8 +157,7 @@ impl JitoClient {
let response = if self.auth_token.is_empty() {
self.http_client.post(&endpoint)
} else {
self.http_client.post(&endpoint)
.header("x-jito-auth", &self.auth_token)
self.http_client.post(&endpoint).header("x-jito-auth", &self.auth_token)
};
let response_text = response
.body(body.to_string())
@@ -150,10 +171,10 @@ impl JitoClient {
if response_json.get("result").is_some() {
println!(" jito {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" jito {} submission failed: {:?}", trade_type, _error);
eprintln!(" jito {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
}
}
Ok(())
}
}
}
+50 -16
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,10 +8,10 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::LIGHTSPEED_TIP_ACCOUNTS};
@@ -23,16 +25,29 @@ pub struct LightspeedClient {
#[async_trait::async_trait]
impl SwqosClientTrait for LightspeedClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *LIGHTSPEED_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| LIGHTSPEED_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *LIGHTSPEED_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| LIGHTSPEED_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -50,9 +65,15 @@ impl LightspeedClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction
let request_body = serde_json::to_string(&json!({
@@ -70,7 +91,9 @@ impl LightspeedClient {
]
}))?;
let response_text = self.http_client.post(&self.endpoint)
let response_text = self
.http_client
.post(&self.endpoint)
.body(request_body)
.header("Content-Type", "application/json")
.send()
@@ -80,12 +103,12 @@ impl LightspeedClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" [lightspeed] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("lightspeed", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [lightspeed] {} submission failed: {:?}", trade_type, _error);
crate::common::sdk_log::log_swqos_submission_failed("lightspeed", trade_type, start_time.elapsed(), _error);
}
} else {
eprintln!(" [lightspeed] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("lightspeed", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -93,19 +116,30 @@ impl LightspeedClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [lightspeed] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"lightspeed",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [lightspeed] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "lightspeed", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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?;
}
+182 -170
View File
@@ -1,22 +1,22 @@
pub mod astralane;
pub mod astralane_quic;
pub mod common;
pub mod serialization;
pub mod solana_rpc;
pub mod jito;
pub mod nextblock;
pub mod zeroslot;
pub mod temporal;
pub mod blockrazor;
pub mod bloxroute;
pub mod common;
pub mod flashblock;
pub mod helius;
pub mod jito;
pub mod lightspeed;
pub mod nextblock;
pub mod node1;
pub mod node1_quic;
pub mod flashblock;
pub mod blockrazor;
pub mod astralane;
pub mod stellium;
pub mod lightspeed;
pub mod serialization;
pub mod solana_rpc;
pub mod soyas;
pub mod speedlanding;
pub mod helius;
pub mod stellium;
pub mod temporal;
pub mod zeroslot;
use std::sync::Arc;
@@ -29,55 +29,26 @@ use anyhow::Result;
use crate::{
common::SolanaRpcClient,
constants::swqos::{
SWQOS_ENDPOINTS_BLOX,
SWQOS_ENDPOINTS_JITO,
SWQOS_ENDPOINTS_NEXTBLOCK,
SWQOS_ENDPOINTS_TEMPORAL,
SWQOS_ENDPOINTS_ZERO_SLOT,
SWQOS_ENDPOINTS_NODE1,
SWQOS_ENDPOINTS_NODE1_QUIC,
SWQOS_ENDPOINTS_FLASHBLOCK,
SWQOS_ENDPOINTS_BLOCKRAZOR,
SWQOS_ENDPOINTS_ASTRALANE,
SWQOS_ENDPOINTS_ASTRALANE_QUIC,
SWQOS_ENDPOINTS_STELLIUM,
SWQOS_ENDPOINTS_SOYAS,
SWQOS_ENDPOINTS_SPEEDLANDING,
SWQOS_ENDPOINTS_HELIUS,
SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_NEXTBLOCK,
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR,
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL,
SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR,
SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK,
SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED,
SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_SOYAS,
SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL,
SWQOS_MIN_TIP_ZERO_SLOT,
SWQOS_MIN_TIP_TEMPORAL,
SWQOS_MIN_TIP_BLOXROUTE,
SWQOS_MIN_TIP_NODE1,
SWQOS_MIN_TIP_FLASHBLOCK,
SWQOS_MIN_TIP_BLOCKRAZOR,
SWQOS_MIN_TIP_ASTRALANE,
SWQOS_MIN_TIP_STELLIUM,
SWQOS_MIN_TIP_LIGHTSPEED,
SWQOS_MIN_TIP_SOYAS,
SWQOS_MIN_TIP_SPEEDLANDING,
SWQOS_MIN_TIP_HELIUS,
},
swqos::{
bloxroute::BloxrouteClient,
jito::JitoClient,
nextblock::NextBlockClient,
solana_rpc::SolRpcClient,
temporal::TemporalClient,
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
flashblock::FlashBlockClient, helius::HeliusClient, jito::JitoClient,
lightspeed::LightspeedClient, nextblock::NextBlockClient, node1::Node1Client,
node1_quic::Node1QuicClient, solana_rpc::SolRpcClient, soyas::SoyasClient,
speedlanding::SpeedlandingClient, stellium::StelliumClient, temporal::TemporalClient,
zeroslot::ZeroSlotClient,
node1::Node1Client,
node1_quic::Node1QuicClient,
flashblock::FlashBlockClient,
blockrazor::BlockRazorClient,
astralane::AstralaneClient,
stellium::StelliumClient,
lightspeed::LightspeedClient,
soyas::SoyasClient,
speedlanding::SpeedlandingClient,
helius::HeliusClient,
}
},
};
lazy_static::lazy_static! {
@@ -90,14 +61,17 @@ lazy_static::lazy_static! {
/// Providers added here will be disabled even if configured by user
/// To enable a provider, remove it from this list
pub const SWQOS_BLACKLIST: &[SwqosType] = &[
SwqosType::NextBlock, // NextBlock is disabled by default
SwqosType::NextBlock, // NextBlock is disabled by default
];
/// SWQOS 提交通道:HTTP 或 QUIC(低延迟)。部分提供商(如 Astralane)支持 QUIC。
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
/// BlockRazor 支持 gRPC 和 HTTP。
/// Astralane 和 Node1 支持 QUIC。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SwqosTransport {
#[default]
Http,
Grpc,
Quic,
}
@@ -141,6 +115,28 @@ pub enum SwqosType {
}
impl SwqosType {
/// Label for log alignment; same as Debug output (e.g. "Soyas", "Speedlanding").
#[inline]
pub fn as_str(self) -> &'static str {
match self {
Self::Jito => "Jito",
Self::NextBlock => "NextBlock",
Self::ZeroSlot => "ZeroSlot",
Self::Temporal => "Temporal",
Self::Bloxroute => "Bloxroute",
Self::Node1 => "Node1",
Self::FlashBlock => "FlashBlock",
Self::BlockRazor => "BlockRazor",
Self::Astralane => "Astralane",
Self::Stellium => "Stellium",
Self::Lightspeed => "Lightspeed",
Self::Soyas => "Soyas",
Self::Speedlanding => "Speedlanding",
Self::Helius => "Helius",
Self::Default => "Default",
}
}
pub fn values() -> Vec<Self> {
vec![
Self::Jito,
@@ -166,8 +162,18 @@ pub type SwqosClient = dyn SwqosClientTrait + Send + Sync + 'static;
#[async_trait::async_trait]
pub trait SwqosClientTrait {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()>;
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()>;
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &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_swqos_type(&self) -> SwqosType;
/// Minimum tip in SOL required by this provider. Helius returns lower value when swqos_only is true.
@@ -222,8 +228,8 @@ pub enum SwqosConfig {
Node1(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// FlashBlock(api_token, region, custom_url)
FlashBlock(String, SwqosRegion, Option<String>),
/// BlockRazor(api_token, region, custom_url)
BlockRazor(String, SwqosRegion, Option<String>),
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// Astralane(api_token, region, custom_url, transport). transport=None 表示 Http。
Astralane(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// Stellium(api_token, region, custom_url)
@@ -243,7 +249,7 @@ pub enum SwqosConfig {
}
impl SwqosConfig {
pub fn swqos_type(&self) -> SwqosType{
pub fn swqos_type(&self) -> SwqosType {
match self {
SwqosConfig::Default(_) => SwqosType::Default,
SwqosConfig::Jito(_, _, _) => SwqosType::Jito,
@@ -253,7 +259,7 @@ impl SwqosConfig {
SwqosConfig::ZeroSlot(_, _, _) => SwqosType::ZeroSlot,
SwqosConfig::Node1(_, _, _, _) => SwqosType::Node1,
SwqosConfig::FlashBlock(_, _, _) => SwqosType::FlashBlock,
SwqosConfig::BlockRazor(_, _, _) => SwqosType::BlockRazor,
SwqosConfig::BlockRazor(_, _, _, _) => SwqosType::BlockRazor,
SwqosConfig::Astralane(_, _, _, _) => SwqosType::Astralane,
SwqosConfig::Stellium(_, _, _) => SwqosType::Stellium,
SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed,
@@ -292,167 +298,173 @@ impl SwqosConfig {
}
}
pub async fn get_swqos_client(rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig) -> Result<Arc<SwqosClient>> {
pub fn get_endpoint_with_transport(
swqos_type: SwqosType,
region: SwqosRegion,
url: Option<String>,
transport: Option<SwqosTransport>,
) -> String {
if let Some(custom_url) = url {
return custom_url;
}
match swqos_type {
SwqosType::BlockRazor => {
// transport=None 或 transport=Grpc => gRPC; transport=Http => HTTP
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
if use_http {
SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC[region as usize].to_string()
}
}
SwqosType::Node1 => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
SWQOS_ENDPOINTS_NODE1_QUIC[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
}
}
SwqosType::Astralane => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
}
}
_ => Self::get_endpoint(swqos_type, region, url),
}
}
pub async fn get_swqos_client(
rpc_url: String,
commitment: CommitmentConfig,
swqos_config: SwqosConfig,
) -> Result<Arc<SwqosClient>> {
match swqos_config {
SwqosConfig::Jito(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Jito, region, url);
let jito_client = JitoClient::new(
rpc_url.clone(),
endpoint,
auth_token
);
let jito_client = JitoClient::new(rpc_url.clone(), endpoint, auth_token);
Ok(Arc::new(jito_client))
}
SwqosConfig::NextBlock(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::NextBlock, region, url);
let nextblock_client = NextBlockClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let nextblock_client =
NextBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(nextblock_client))
},
}
SwqosConfig::ZeroSlot(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::ZeroSlot, region, url);
let zeroslot_client = ZeroSlotClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let zeroslot_client =
ZeroSlotClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(zeroslot_client))
},
SwqosConfig::Temporal(auth_token, region, url) => {
}
SwqosConfig::Temporal(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Temporal, region, url);
let temporal_client = TemporalClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let temporal_client =
TemporalClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(temporal_client))
},
SwqosConfig::Bloxroute(auth_token, region, url) => {
}
SwqosConfig::Bloxroute(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Bloxroute, region, url);
let bloxroute_client = BloxrouteClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let bloxroute_client =
BloxrouteClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(bloxroute_client))
},
}
SwqosConfig::Node1(auth_token, region, url, transport) => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
let quic_endpoint = url
.unwrap_or_else(|| SWQOS_ENDPOINTS_NODE1_QUIC[region as usize].to_string());
let node1_quic = Node1QuicClient::connect(
&quic_endpoint,
&auth_token,
rpc_url.clone(),
)
.await?;
let node1_quic =
Node1QuicClient::connect(&quic_endpoint, &auth_token, rpc_url.clone())
.await?;
Ok(Arc::new(node1_quic))
} else {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Node1, region, url);
let node1_client = Node1Client::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token,
);
let node1_client =
Node1Client::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(node1_client))
}
},
}
SwqosConfig::FlashBlock(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::FlashBlock, region, url);
let flashblock_client = FlashBlockClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let flashblock_client =
FlashBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(flashblock_client))
},
SwqosConfig::BlockRazor(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url);
let blockrazor_client = BlockRazorClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
Ok(Arc::new(blockrazor_client))
},
}
SwqosConfig::BlockRazor(auth_token, region, url, transport) => {
// BlockRazor: transport=None 或 transport=Grpc 时使用 gRPCtransport=Http 时使用 HTTP
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport);
if use_http {
let blockrazor_client =
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(blockrazor_client))
} else {
// 使用 gRPC 模式(默认或用户明确指定了 gRPC)
let blockrazor_client =
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
Ok(Arc::new(blockrazor_client))
}
}
SwqosConfig::Astralane(auth_token, region, url, transport) => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
let quic_endpoint = url
.unwrap_or_else(|| SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string());
let quic_endpoint = url.unwrap_or_else(|| {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
});
let astralane_client =
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token).await?;
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
.await?;
Ok(Arc::new(astralane_client))
} else {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
let astralane_client = AstralaneClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token,
);
let astralane_client =
AstralaneClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(astralane_client))
}
},
}
SwqosConfig::Stellium(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Stellium, region, url);
let stellium_client = StelliumClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let stellium_client =
StelliumClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(stellium_client))
},
}
SwqosConfig::Lightspeed(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Lightspeed, region, url);
let lightspeed_client = LightspeedClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let lightspeed_client =
LightspeedClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(lightspeed_client))
},
}
SwqosConfig::Soyas(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Soyas, region, url);
let soyas_client = SoyasClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
).await?;
let soyas_client =
SoyasClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
Ok(Arc::new(soyas_client))
},
}
SwqosConfig::Speedlanding(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Speedlanding, region, url);
let speedlanding_client = SpeedlandingClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
).await?;
let speedlanding_client =
SpeedlandingClient::new(rpc_url.clone(), endpoint.to_string(), auth_token)
.await?;
Ok(Arc::new(speedlanding_client))
},
}
SwqosConfig::Helius(api_key, region, url, swqos_only) => {
let swqos_only = swqos_only.unwrap_or(false);
let endpoint = SwqosConfig::get_endpoint(SwqosType::Helius, region, url.clone());
let api_key_opt = if api_key.is_empty() { None } else { Some(api_key.clone()) };
let helius_client = HeliusClient::new(
rpc_url.clone(),
endpoint,
api_key_opt,
swqos_only,
);
let helius_client =
HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only);
Ok(Arc::new(helius_client))
},
}
SwqosConfig::Default(endpoint) => {
let rpc = SolanaRpcClient::new_with_commitment(
endpoint,
commitment
);
let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment);
let rpc_client = SolRpcClient::new(Arc::new(rpc));
Ok(Arc::new(rpc_client))
}
}
}
}
}
+51 -17
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,10 +8,10 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::NEXTBLOCK_TIP_ACCOUNTS};
@@ -23,16 +25,29 @@ pub struct NextBlockClient {
#[async_trait::async_trait]
impl SwqosClientTrait for NextBlockClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -54,9 +69,15 @@ impl NextBlockClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"transaction": {
@@ -65,7 +86,9 @@ impl NextBlockClient {
"frontRunningProtection": false
}))?;
let response_text = self.http_client.post(&self.endpoint)
let response_text = self
.http_client
.post(&self.endpoint)
.body(request_body)
.header("Authorization", &self.auth_token)
.header("Content-Type", "application/json")
@@ -76,12 +99,12 @@ impl NextBlockClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" [nextblock] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("nextblock", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [nextblock] {} submission failed: {:?}", trade_type, _error);
crate::common::sdk_log::log_swqos_submission_failed("nextblock", trade_type, start_time.elapsed(), _error);
}
} else {
eprintln!(" [nextblock] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("nextblock", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -89,22 +112,33 @@ impl NextBlockClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [nextblock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"nextblock",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [nextblock] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "nextblock", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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(())
}
}
}
+71 -34
View File
@@ -1,21 +1,23 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::NODE1_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
#[derive(Clone)]
pub struct Node1Client {
@@ -29,16 +31,29 @@ pub struct Node1Client {
#[async_trait::async_trait]
impl SwqosClientTrait for Node1Client {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *NODE1_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NODE1_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *NODE1_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| NODE1_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -51,22 +66,22 @@ impl Node1Client {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
@@ -76,7 +91,7 @@ impl Node1Client {
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
@@ -90,14 +105,15 @@ impl Node1Client {
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("Node1 ping request failed: {}", e);
}
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
@@ -109,7 +125,11 @@ impl Node1Client {
}
/// Send ping request to /ping endpoint
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
let ping_url = if endpoint.ends_with('/') {
format!("{}ping", endpoint)
@@ -118,10 +138,8 @@ impl Node1Client {
};
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url)
.timeout(Duration::from_millis(1500))
.send()
.await?;
let response =
http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await?;
let status = response.status();
let _ = response.bytes().await;
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
@@ -130,9 +148,15 @@ impl Node1Client {
Ok(())
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
@@ -145,7 +169,9 @@ impl Node1Client {
}))?;
// Node1 uses api-key header instead of URL parameter
let response_text = self.http_client.post(&self.endpoint)
let response_text = self
.http_client
.post(&self.endpoint)
.body(request_body)
.header("Content-Type", "application/json")
.header("api-key", &self.auth_token)
@@ -158,13 +184,13 @@ impl Node1Client {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if crate::common::sdk_log::sdk_log_enabled() {
if response_json.get("result").is_some() {
println!(" [node1] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("node1", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [node1] {} submission failed: {:?}", trade_type, _error);
eprintln!(" [node1] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
}
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [node1] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("node1", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -173,20 +199,31 @@ impl Node1Client {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [node1] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"node1",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [node1] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "node1", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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?;
}
@@ -198,7 +235,7 @@ impl Drop for Node1Client {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
+28 -38
View File
@@ -10,8 +10,8 @@ use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, RecvStream, Transpo
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use tokio::sync::Mutex;
use tokio::time::timeout;
use uuid::Uuid;
use crate::common::SolanaRpcClient;
@@ -41,44 +41,34 @@ pub struct Node1QuicClient {
impl Node1QuicClient {
/// Connect and authenticate. Reuse the returned client for all subsequent sends.
pub async fn connect(
server_addr: &str,
api_key: &str,
rpc_url: String,
) -> Result<Self> {
pub async fn connect(server_addr: &str, api_key: &str, rpc_url: String) -> Result<Self> {
let socket_addr = server_addr
.to_socket_addrs()
.context("resolve Node1 QUIC server address")?
.next()
.context("no socket address for Node1 QUIC")?;
let api_key_uuid = Uuid::parse_str(api_key).context("Node1 API key must be a valid UUID")?;
let api_key_uuid =
Uuid::parse_str(api_key).context("Node1 API key must be a valid UUID")?;
let api_key_bytes: [u8; 16] = *api_key_uuid.as_bytes();
let server_name = server_addr
.split(':')
.next()
.unwrap_or(server_addr);
let server_name = server_addr.split(':').next().unwrap_or(server_addr);
let client_config = Self::build_client_config()?;
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)
.context("create QUIC endpoint")?;
let mut endpoint =
Endpoint::client("0.0.0.0:0".parse()?).context("create QUIC endpoint")?;
endpoint.set_default_client_config(client_config);
let connecting = endpoint
.connect(socket_addr, server_name)
.context("Node1 QUIC connect failed")?;
let connecting =
endpoint.connect(socket_addr, server_name).context("Node1 QUIC connect failed")?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Node1 QUIC connect timeout")?
.context("Node1 QUIC handshake failed")?;
timeout(
AUTH_TIMEOUT,
Self::authenticate(&connection, &api_key_bytes),
)
.await
.context("Node1 QUIC auth timeout")??;
timeout(AUTH_TIMEOUT, Self::authenticate(&connection, &api_key_bytes))
.await
.context("Node1 QUIC auth timeout")??;
Ok(Self {
endpoint,
@@ -96,8 +86,7 @@ impl Node1QuicClient {
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth();
let client_crypto = QuicClientConfig::try_from(crypto)
.context("build QUIC TLS config")?;
let client_crypto = QuicClientConfig::try_from(crypto).context("build QUIC TLS config")?;
let mut client_config = ClientConfig::new(Arc::new(client_crypto));
let mut transport = TransportConfig::default();
@@ -140,12 +129,9 @@ impl Node1QuicClient {
.context("Node1 QUIC reconnect timeout")?
.context("Node1 QUIC re-handshake failed")?;
timeout(
AUTH_TIMEOUT,
Self::authenticate(&connection, &self.api_key_uuid),
)
.await
.context("Node1 QUIC re-auth timeout")??;
timeout(AUTH_TIMEOUT, Self::authenticate(&connection, &self.api_key_uuid))
.await
.context("Node1 QUIC re-auth timeout")??;
let mut g = self.connection.lock().await;
*g = connection.clone();
@@ -201,16 +187,16 @@ impl SwqosClientTrait for Node1QuicClient {
let signature = transaction.signatures.first().copied().unwrap_or_default();
let tx_bytes = bincode::serialize(transaction).context("Node1 QUIC: bincode serialize")?;
let (status, msg) = timeout(
SEND_TIMEOUT,
self.send_transaction_bytes(&tx_bytes),
)
.await
.context("Node1 QUIC send timeout")??;
let (status, msg) = timeout(SEND_TIMEOUT, self.send_transaction_bytes(&tx_bytes))
.await
.context("Node1 QUIC send timeout")??;
if status != 200 {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [node1-quic] {} submit failed: status={} msg={}", trade_type, status, msg);
eprintln!(
" [node1-quic] {} submit failed: status={} msg={}",
trade_type, status, msg
);
}
anyhow::bail!("Node1 QUIC submit failed: status={} msg={}", status, msg);
}
@@ -229,7 +215,11 @@ impl SwqosClientTrait for Node1QuicClient {
}
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [node1-quic] {} confirmation failed: {:?}", trade_type, start.elapsed());
eprintln!(
" [node1-quic] {} confirmation failed: {:?}",
trade_type,
start.elapsed()
);
}
Err(e)
}
+42
View File
@@ -0,0 +1,42 @@
# Proto 生成的代码说明
## 概述
这个目录包含了从 `.proto` 文件预生成的 Rust 代码。
## 文件说明
- `serverpb.rs` - 从 `blockrazor.proto` 生成的 gRPC 代码
- 消息类型: `SendRequest`, `SendResponse`, `HealthRequest`, `HealthResponse`
- gRPC 客户端: `server_client::ServerClient`
- gRPC 服务端: `server_server::Server`
## 用户使用
用户**不需要**安装 `protoc` 或编译 proto 文件。这些代码已经预生成好了,可以直接使用。
`blockrazor.rs` 中使用:
```rust
pub mod serverpb {
include!("pb/serverpb.rs");
}
```
## 开发者如何重新生成代码
如果你修改了 `.proto` 文件并需要重新生成代码:
```bash
cd sol-trade-sdk/proto/gen
cargo run
```
这会在 `src/swqos/pb/serverpb.rs` 生成新的代码。
## 技术细节
生成工具使用 `tonic-prost-build` crate
- 输出目录: `src/swqos/pb`
- Proto 文件: `proto/blockrazor.proto`
- 生成工具: `proto/gen/`
- 包含完整的客户端和服务端代码
+383
View File
@@ -0,0 +1,383 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SendRequest {
#[prost(string, tag = "1")]
pub transaction: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub mode: ::prost::alloc::string::String,
/// only take effect in sandwichMitigation mode
#[prost(int32, optional, tag = "3")]
pub safe_window: ::core::option::Option<i32>,
#[prost(bool, tag = "4")]
pub revert_protection: bool,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SendResponse {
#[prost(string, tag = "1")]
pub signature: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct HealthRequest {}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct HealthResponse {
#[prost(string, tag = "1")]
pub status: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod server_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct ServerClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ServerClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> ServerClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> ServerClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
ServerClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn send_transaction(
&mut self,
request: impl tonic::IntoRequest<super::SendRequest>,
) -> std::result::Result<tonic::Response<super::SendResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/serverpb.Server/SendTransaction",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("serverpb.Server", "SendTransaction"));
self.inner.unary(req, path, codec).await
}
pub async fn get_health(
&mut self,
request: impl tonic::IntoRequest<super::HealthRequest>,
) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/serverpb.Server/GetHealth",
);
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("serverpb.Server", "GetHealth"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod server_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with ServerServer.
#[async_trait]
pub trait Server: std::marker::Send + std::marker::Sync + 'static {
async fn send_transaction(
&self,
request: tonic::Request<super::SendRequest>,
) -> std::result::Result<tonic::Response<super::SendResponse>, tonic::Status>;
async fn get_health(
&self,
request: tonic::Request<super::HealthRequest>,
) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct ServerServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> ServerServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for ServerServer<T>
where
T: Server,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/serverpb.Server/SendTransaction" => {
#[allow(non_camel_case_types)]
struct SendTransactionSvc<T: Server>(pub Arc<T>);
impl<T: Server> tonic::server::UnaryService<super::SendRequest>
for SendTransactionSvc<T> {
type Response = super::SendResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::SendRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Server>::send_transaction(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = SendTransactionSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/serverpb.Server/GetHealth" => {
#[allow(non_camel_case_types)]
struct GetHealthSvc<T: Server>(pub Arc<T>);
impl<T: Server> tonic::server::UnaryService<super::HealthRequest>
for GetHealthSvc<T> {
type Response = super::HealthResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::HealthRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Server>::get_health(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetHealthSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for ServerServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "serverpb.Server";
impl<T> tonic::server::NamedService for ServerServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}
+16 -6
View File
@@ -109,25 +109,35 @@ impl SwqosClientTrait for SoyasClient {
let serialized_tx = bincode::serialize(transaction)?;
let connection = self.connection.load_full();
if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() {
eprintln!(" [soyas] {} submission failed, reconnecting", trade_type);
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Soyas", trade_type, start_time.elapsed(), "reconnecting");
}
self.reconnect().await?;
let connection = self.connection.load_full();
if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await {
eprintln!(" [soyas] {} submission failed: {:?}", trade_type, e);
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Soyas", trade_type, start_time.elapsed(), &e);
}
return Err(e.into());
}
}
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("Soyas", trade_type, start_time.elapsed());
}
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [soyas] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [{:width$}] {} confirmation failed: {:?}", "Soyas", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
return Err(e);
}
}
if wait_confirmation {
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [soyas] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "Soyas", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
+37 -10
View File
@@ -119,9 +119,11 @@ impl SpeedlandingClient {
let _guard = self.reconnect.lock().await;
let current = self.connection.load_full();
if current.close_reason().is_some() {
let connecting = self
.endpoint
.connect_with(self.client_config.clone(), self.addr, self.server_name.as_str())?;
let connecting = self.endpoint.connect_with(
self.client_config.clone(),
self.addr,
self.server_name.as_str(),
)?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Speedlanding QUIC reconnect timeout")?
@@ -151,33 +153,58 @@ impl SwqosClientTrait for SpeedlandingClient {
let start_time = Instant::now();
let (buf_guard, signature) = serialize_transaction_bincode_sync(transaction)?;
let connection = self.ensure_connected().await?;
let mut send_result = timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
let mut send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
let need_retry = match &send_result {
Ok(Ok(())) => false,
Ok(Err(_)) | Err(_) => true,
};
if need_retry {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [speedlanding] {} send failed or timeout, reconnecting", trade_type);
eprintln!(" [Speedlanding] {} submission failed after {:?}, reconnecting", trade_type, start_time.elapsed());
}
let connection = self.ensure_connected().await?;
send_result = timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
}
match send_result.context("Speedlanding QUIC send timeout") {
Ok(Ok(())) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
}
}
Ok(Err(e)) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), &e);
}
return Err(e.into());
}
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), "timeout");
}
return Err(e.into());
}
}
send_result
.context("Speedlanding QUIC send timeout")??;
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [speedlanding] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"Speedlanding",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [speedlanding] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "Speedlanding", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
+55 -20
View File
@@ -1,21 +1,22 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::sync::atomic::{AtomicBool, Ordering};
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::STELLIUM_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct StelliumClient {
pub endpoint: String,
@@ -27,16 +28,29 @@ pub struct StelliumClient {
#[async_trait::async_trait]
impl SwqosClientTrait for StelliumClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *STELLIUM_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| STELLIUM_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *STELLIUM_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| STELLIUM_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -79,7 +93,9 @@ impl StelliumClient {
tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
let url = format!("{}/{}", endpoint, auth_token);
if let Ok(resp) = http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
if let Ok(resp) =
http_client.get(&url).timeout(Duration::from_millis(1500)).send().await
{
let status = resp.status();
let _ = resp.bytes().await;
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
@@ -111,9 +127,15 @@ impl StelliumClient {
});
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Stellium uses standard Solana sendTransaction format
let request_body = serde_json::to_string(&json!({
@@ -130,7 +152,9 @@ impl StelliumClient {
let url = format!("{}/{}", self.endpoint, self.auth_token);
// Send request to Stellium
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body)
.header("Content-Type", "application/json")
.header("Connection", "keep-alive")
@@ -144,13 +168,13 @@ impl StelliumClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if crate::common::sdk_log::sdk_log_enabled() {
if response_json.get("result").is_some() {
println!(" [Stellium] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("Stellium", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, _error);
crate::common::sdk_log::log_swqos_submission_failed("Stellium", trade_type, start_time.elapsed(), _error);
}
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("Stellium", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -159,20 +183,31 @@ impl StelliumClient {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [Stellium] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [{:width$}] {} confirmation failed: {:?}",
"Stellium",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [Stellium] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "Stellium", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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?;
}
+77 -41
View File
@@ -1,27 +1,29 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use sha2::{Digest, Sha256};
use solana_transaction_status::UiTransactionEncoding;
use sha2::{Sha256, Digest};
use std::time::Duration;
use std::{sync::Arc, time::Instant};
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::NOZOMI_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
const SPECIAL_API_KEY_PREFIX: &str = "298b5025";
const SPECIAL_API_KEY_SUFFIX: &str = "a055323";
const SPECIAL_API_KEY_HASH: &str = "e7be933c8058aebcb4d08a6120fb4dfd2ead568d42527a3fc2b60a703f25e48d";
const SPECIAL_API_KEY_HASH: &str =
"e7be933c8058aebcb4d08a6120fb4dfd2ead568d42527a3fc2b60a703f25e48d";
const TEMPORAL_COMMUNITY_TIP_ADDRESS: &str = "mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj";
#[inline]
@@ -31,7 +33,6 @@ fn fast_sha256_hex(input: &str) -> String {
format!("{:x}", hasher.finalize())
}
#[derive(Clone)]
pub struct TemporalClient {
pub rpc_client: Arc<SolanaRpcClient>,
@@ -44,18 +45,30 @@ pub struct TemporalClient {
#[async_trait::async_trait]
impl SwqosClientTrait for TemporalClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let api_key = &self.auth_token;
if api_key.len() >= SPECIAL_API_KEY_PREFIX.len() + SPECIAL_API_KEY_SUFFIX.len() {
if api_key.starts_with(SPECIAL_API_KEY_PREFIX) && api_key.ends_with(SPECIAL_API_KEY_SUFFIX) {
if api_key.starts_with(SPECIAL_API_KEY_PREFIX)
&& api_key.ends_with(SPECIAL_API_KEY_SUFFIX)
{
let current_api_key_hash = fast_sha256_hex(api_key);
if current_api_key_hash == SPECIAL_API_KEY_HASH {
@@ -64,7 +77,10 @@ impl SwqosClientTrait for TemporalClient {
}
}
let tip_account = *NOZOMI_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NOZOMI_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *NOZOMI_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| NOZOMI_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -77,22 +93,22 @@ impl TemporalClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
@@ -102,7 +118,7 @@ impl TemporalClient {
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
@@ -114,12 +130,13 @@ impl TemporalClient {
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
eprintln!("Temporal ping request failed: {}", e);
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
@@ -131,7 +148,11 @@ impl TemporalClient {
}
/// Send ping request to /ping endpoint
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 (no auth token required for ping endpoint)
let ping_url = if endpoint.ends_with('/') {
format!("{}ping", endpoint)
@@ -140,10 +161,8 @@ impl TemporalClient {
};
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url)
.timeout(Duration::from_millis(1500))
.send()
.await?;
let response =
http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await?;
let status = response.status();
let _ = response.bytes().await;
if !status.is_success() {
@@ -152,9 +171,15 @@ impl TemporalClient {
Ok(())
}
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 (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Build request body according to Nozomi documentation requirements
let request_body = serde_json::to_string(&json!({
@@ -172,7 +197,9 @@ impl TemporalClient {
url.push_str("/?c=");
url.push_str(&self.auth_token);
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body)
.header("Content-Type", "application/json")
.send()
@@ -182,12 +209,12 @@ impl TemporalClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" [nozomi] {} submitted: {:?}", trade_type, start_time.elapsed());
crate::common::sdk_log::log_swqos_submitted("nozomi", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
// eprintln!("nozomi transaction submission failed: {:?}", _error);
crate::common::sdk_log::log_swqos_submission_failed("nozomi", trade_type, start_time.elapsed(), _error);
}
} else {
eprintln!(" [nozomi] {} submission failed: {:?}", trade_type, response_text);
crate::common::sdk_log::log_swqos_submission_failed("nozomi", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
@@ -195,19 +222,28 @@ impl TemporalClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [nozomi] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [nozomi] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [nozomi] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "nozomi", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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?;
}
@@ -219,7 +255,7 @@ impl Drop for TemporalClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
@@ -231,4 +267,4 @@ impl Drop for TemporalClient {
*ping_guard = None;
});
}
}
}
+196 -44
View File
@@ -1,39 +1,55 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use std::{sync::Arc, time::Instant, time::Duration};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
use bincode;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::ZEROSLOT_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct ZeroSlotClient {
pub endpoint: String,
pub auth_token: String,
pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client,
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
pub stop_ping: Arc<AtomicBool>,
}
#[async_trait::async_trait]
impl SwqosClientTrait for ZeroSlotClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
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<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ZEROSLOT_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *ZEROSLOT_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| ZEROSLOT_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -46,69 +62,205 @@ impl ZeroSlotClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
/// Start periodic ping task to keep connections active
async fn start_ping_task(&self) {
let endpoint = self.endpoint.clone();
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("0slot ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive under 65s server timeout
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("0slot ping request failed: {}", e);
}
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
}
*ping_guard = Some(handle);
}
}
/// Send ping request: POST with getHealth method (Keep Alive). Free operation, not counted toward TPS.
async fn send_ping_request(
http_client: &Client,
endpoint: &str,
auth_token: &str,
) -> Result<()> {
let url = format!("{}/?api-key={}", endpoint, auth_token);
let response = http_client
.post(&url)
.header("Content-Type", "application/json")
.timeout(Duration::from_millis(1500))
.body(r#"{"jsonrpc":"2.0","id":1,"method":"getHealth"}"#)
.send()
.await?;
let _ = response.bytes().await;
Ok(())
}
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 }
]
}))?;
// Binary-Tx: Send raw binary transaction bytes directly
// This is faster than JSON-RPC as it avoids unnecessary encoding/decoding
let tx_bytes = bincode::serialize(transaction)?;
// Build URL for Binary-Tx endpoint: {endpoint}/txb?api-key={auth_token}
let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20);
url.push_str(&self.endpoint);
url.push_str("/?api-key=");
url.push_str("/txb?api-key=");
url.push_str(&self.auth_token);
// 4. Use `text().await?` directly, avoiding async JSON parsing from `json().await?`
let response_text = self.http_client.post(&url)
.body(request_body) // Pass string directly, avoiding `json()` overhead
.header("Content-Type", "application/json") // Explicitly specify JSON header
// Send binary transaction directly
let response = self
.http_client
.post(&url)
.header("User-Agent", "") // Optional: 0slot recommends empty User-Agent
.body(tx_bytes)
.send()
.await?
.text()
.await?;
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" [0slot] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [0slot] {} submission failed: {:?}", trade_type, _error);
let status = response.status();
let response_text = response.text().await?;
// Binary-Tx returns JSON-RPC 2.0 format responses
// 200: success with result field containing signature, or error field with code/message
// 403: api-key error (null, doesn't exist, or expired)
// 419: rate limit exceeded
// 500: submission failed
match status.as_u16() {
200 => {
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
if json_value.get("result").is_some() {
crate::common::sdk_log::log_swqos_submitted("0slot", trade_type, start_time.elapsed());
} else if let Some(error) = json_value.get("error") {
let code = error.get("code")
.and_then(|c| c.as_i64())
.map(|c| c.to_string())
.unwrap_or_else(|| "unknown".to_string());
let message = error.get("message")
.and_then(|m| m.as_str())
.unwrap_or("unknown error");
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("code {}: {}", code, message));
return Err(anyhow::anyhow!("0slot Binary-Tx error: {}", message));
} else {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("unexpected JSON: {}", response_text));
return Err(anyhow::anyhow!("0slot Binary-Tx unexpected JSON: {}", response_text));
}
} else {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("invalid JSON: {}", response_text));
return Err(anyhow::anyhow!("0slot Binary-Tx invalid JSON: {}", response_text));
}
}
403 => {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text.clone());
return Err(anyhow::anyhow!("0slot API key error: {}", response_text));
}
419 => {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text.clone());
return Err(anyhow::anyhow!("0slot rate limit exceeded"));
}
500 => {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), "submission failed".to_string());
return Err(anyhow::anyhow!("0slot transaction submission failed"));
}
_ => {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("status {} body: {}", status, response_text));
return Err(anyhow::anyhow!("0slot Binary-Tx failed with status {}: {}", status, response_text));
}
} else {
eprintln!(" [0slot] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
// Get transaction signature from the transaction for confirmation polling
let signature = transaction.signatures[0];
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [0slot] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmation failed: {:?}", "0slot", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [0slot] {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [{:width$}] {} confirmed: {:?}", "0slot", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
}
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub 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(())
}
}
}
impl Drop for ZeroSlotClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
tokio::spawn(async move {
let mut ping_guard = ping_handle.lock().await;
if let Some(handle) = ping_guard.as_ref() {
handle.abort();
}
*ping_guard = None;
});
}
}
+1 -1
View File
@@ -1,8 +1,8 @@
use dashmap::DashMap;
use once_cell::sync::Lazy;
use smallvec::SmallVec;
use solana_sdk::instruction::Instruction;
use solana_compute_budget_interface::ComputeBudgetInstruction;
use solana_sdk::instruction::Instruction;
use std::sync::Arc;
/// Cache key containing all parameters for compute budget instructions
+3 -3
View File
@@ -1,12 +1,12 @@
pub mod compute_budget_manager;
pub mod nonce_manager;
pub mod transaction_builder;
pub mod compute_budget_manager;
pub mod utils;
pub mod wsol_manager;
// Re-export commonly used functions
pub use compute_budget_manager::*;
pub use nonce_manager::*;
pub use transaction_builder::*;
pub use compute_budget_manager::*;
pub use utils::*;
pub use wsol_manager::*;
pub use wsol_manager::*;
+3 -2
View File
@@ -15,10 +15,11 @@ pub fn add_nonce_instruction(
durable_nonce: Option<&DurableNonceInfo>,
) -> Result<(), anyhow::Error> {
if let Some(durable_nonce) = durable_nonce {
let nonce_advance_ix = advance_nonce_account(&durable_nonce.nonce_account.unwrap(), &payer.pubkey());
let nonce_advance_ix =
advance_nonce_account(&durable_nonce.nonce_account.unwrap(), &payer.pubkey());
instructions.push(nonce_advance_ix);
}
Ok(())
}
+6 -3
View File
@@ -9,7 +9,10 @@ use std::sync::Arc;
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
use crate::{
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}},
trading::{
core::transaction_pool::{acquire_builder, release_builder},
MiddlewareManager,
},
};
/// Convert SOL amount (f64) to lamports without string allocation (hot path).
@@ -22,8 +25,8 @@ fn sol_f64_to_lamports(sol: f64) -> u64 {
(lamports.min(u64::MAX as f64)).round() as u64
}
/// Build standard RPC transaction.
/// Takes Arc/context by reference to avoid clone in worker hot path (Arc::clone is cheap but ref is zero-cost).
/// Build standard RPC transaction (worker hot path).
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/rpc/middleware.
pub async fn build_transaction(
payer: &Arc<Keypair>,
_rpc: Option<&Arc<SolanaRpcClient>>,
+1 -8
View File
@@ -40,14 +40,7 @@ pub async fn get_token_balance(
payer: &Pubkey,
mint: &Pubkey,
) -> Result<u64, anyhow::Error> {
get_token_balance_with_options(
rpc,
payer,
mint,
&crate::constants::TOKEN_PROGRAM,
false,
)
.await
get_token_balance_with_options(rpc, payer, mint, &crate::constants::TOKEN_PROGRAM, false).await
}
/// 使用与交易指令一致的 ATA 推导(可选 seed)查询余额;卖出/余额查询应与买入使用同一 ATA 地址。
+11 -21
View File
@@ -1,7 +1,10 @@
use crate::common::{
fast_fn::create_associated_token_account_idempotent_fast,
seed::{
create_associated_token_account_use_seed,
get_associated_token_address_with_program_id_use_seed,
},
spl_token::close_account,
seed::{create_associated_token_account_use_seed, get_associated_token_address_with_program_id_use_seed},
};
use smallvec::SmallVec;
use solana_sdk::{instruction::Instruction, message::AccountMeta, pubkey::Pubkey};
@@ -38,7 +41,7 @@ pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]>
pub fn close_wsol(payer: &Pubkey) -> Vec<Instruction> {
use std::sync::Arc;
let wsol_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&payer,
@@ -61,7 +64,7 @@ pub fn close_wsol(payer: &Pubkey) -> Vec<Instruction> {
.unwrap()]
},
);
// 🚀 性能优化:尝试零开销解包 Arc
Arc::try_unwrap(arc_instructions).unwrap_or_else(|arc| (*arc).clone())
}
@@ -109,10 +112,7 @@ pub fn wrap_sol_only(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 2
///
/// 注意:此函数只生成指令,不检查账户是否存在(需要调用方在发送交易前检查)
/// 如果临时账户已存在,可以安全地跳过创建步骤,直接转账并关闭
pub fn wrap_wsol_to_sol(
payer: &Pubkey,
amount: u64,
) -> Result<Vec<Instruction>, anyhow::Error> {
pub fn wrap_wsol_to_sol(payer: &Pubkey, amount: u64) -> Result<Vec<Instruction>, anyhow::Error> {
let mut instructions = Vec::new();
// 1. 创建 WSOL seed 账户(注意:如果账户已存在会失败)
@@ -151,13 +151,8 @@ pub fn wrap_wsol_to_sol(
instructions.push(transfer_instruction);
// 5. 添加关闭 WSOL seed 账户的指令
let close_instruction = close_account(
&crate::constants::TOKEN_PROGRAM,
&seed_ata_address,
payer,
payer,
&[],
)?;
let close_instruction =
close_account(&crate::constants::TOKEN_PROGRAM, &seed_ata_address, payer, payer, &[])?;
instructions.push(close_instruction);
Ok(instructions)
@@ -197,13 +192,8 @@ pub fn wrap_wsol_to_sol_without_create(
instructions.push(transfer_instruction);
// 4. 添加关闭 WSOL seed 账户的指令
let close_instruction = close_account(
&crate::constants::TOKEN_PROGRAM,
&seed_ata_address,
payer,
payer,
&[],
)?;
let close_instruction =
close_account(&crate::constants::TOKEN_PROGRAM, &seed_ata_address, payer, payer, &[])?;
instructions.push(close_instruction);
Ok(instructions)
+158 -74
View File
@@ -1,8 +1,15 @@
//! Parallel executor for multi-SWQOS submit.
//!
//! - **Pool**: Pre-spawned workers; hot path only enqueues jobs (no per-call tokio::spawn).
//! - **Arc**: Shared data is behind `Arc` so "clone" is just a refcount increment (no data copy).
//! - **Refs**: `build_transaction` takes `&Arc<..>`, `Option<&DurableNonceInfo>`, `Option<&AddressLookupTableAccount>` so the worker passes refs only (zero clone on worker path).
//! **Hot path (submit):** no lock (OnceCell + lock-free ArrayQueue), no `get_core_ids()`, only Arc clones and queue push.
//! - **Pool**: Pre-spawned workers (default 18); hot path only enqueues jobs (no per-call tokio::spawn).
//! - **Dedicated threads** (opt-in via `with_dedicated_sender_threads`): N OS threads run sender work only, optionally pinned to cores.
//! - **Arc**: Shared data behind `Arc` → clone = refcount increment (no data copy).
//! - **Refs**: `build_transaction` takes refs only; worker path avoids extra clones.
//!
//! **Core affinity & latency:** Each job is assigned a core (round-robin from `effective_core_ids`). When a worker runs a job,
//! it sets thread affinity to that core. If that core is busy with other work (e.g. node sync, bot logic), SWQOS submit on that
//! core will compete for CPU and latency can increase. For lowest latency, reserve a subset of cores for SWQOS only via
//! `with_dedicated_sender_threads(Some(indices))` and avoid running other CPU-heavy work on those core indices.
use anyhow::{anyhow, Result};
use crossbeam_queue::ArrayQueue;
@@ -14,8 +21,10 @@ use solana_sdk::{
};
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::{str::FromStr, sync::Arc, time::Instant};
use tokio::sync::Notify;
use fnv::FnvHasher;
@@ -25,11 +34,14 @@ use crate::{
common::nonce_cache::DurableNonceInfo,
common::{GasFeeStrategy, SolanaRpcClient},
swqos::{SwqosClient, SwqosType, TradeType},
trading::core::params::SenderConcurrencyConfig,
trading::{common::build_transaction, MiddlewareManager},
};
const SWQOS_POOL_WORKERS: usize = 32;
/// 与 transaction_pool::PARALLEL_SENDER_COUNT 一致,保证多路 build 不串行
const SWQOS_POOL_WORKERS: usize = 18;
const SWQOS_QUEUE_CAP: usize = 128;
const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
/// Shared across all jobs in one batch; built once, cloned as single Arc per job (minimal hot-path clone).
struct SwqosSharedContext {
@@ -105,11 +117,7 @@ async fn run_one_swqos_job(job: SwqosJob) {
let (success, err, landed_on_chain) = match job
.swqos_client
.send_transaction(
if s.is_buy {
TradeType::Buy
} else {
TradeType::Sell
},
if s.is_buy { TradeType::Buy } else { TradeType::Sell },
&transaction,
s.wait_transaction_confirmed,
)
@@ -133,25 +141,88 @@ async fn run_one_swqos_job(job: SwqosJob) {
});
}
async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>) {
async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>, notify: Arc<Notify>) {
loop {
if let Some(job) = queue.pop() {
run_one_swqos_job(job).await;
} else {
tokio::task::yield_now().await;
notify.notified().await;
}
}
}
static SWQOS_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
static SWQOS_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
static SWQOS_WORKERS_STARTED: AtomicBool = AtomicBool::new(false);
fn ensure_swqos_pool(queue: Arc<ArrayQueue<SwqosJob>>) {
/// Dedicated OS-thread sender pool. Queue and notify are in OnceCell so hot path never takes a lock after init.
static DEDICATED_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
static DEDICATED_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
/// JoinHandles kept so dedicated threads are not detached; only touched during init under lock.
static DEDICATED_INIT: Mutex<Option<Vec<std::thread::JoinHandle<()>>>> = Mutex::new(None);
fn ensure_dedicated_pool(
sender_thread_cores: Option<&[usize]>,
max_sender_concurrency: usize,
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
return (q.clone(), n.clone());
}
let mut guard = DEDICATED_INIT.lock();
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
return (q.clone(), n.clone());
}
let n = sender_thread_cores
.map(|v| v.len().min(max_sender_concurrency))
.unwrap_or_else(|| SWQOS_DEDICATED_DEFAULT_THREADS.min(max_sender_concurrency))
.min(32)
.max(1);
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
let notify = Arc::new(Notify::new());
let core_ids: Vec<core_affinity::CoreId> = core_affinity::get_core_ids()
.map(|all_ids| {
sender_thread_cores
.map(|indices| {
indices
.iter()
.take(n)
.filter_map(|&i| all_ids.get(i).cloned())
.collect()
})
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
})
.unwrap_or_default();
let mut handles = Vec::with_capacity(n);
for i in 0..n {
let queue = queue.clone();
let notify = notify.clone();
let core_id = core_ids.get(i).cloned();
let handle = std::thread::spawn(move || {
if let Some(cid) = core_id {
core_affinity::set_for_current(cid);
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("dedicated sender runtime");
rt.block_on(swqos_worker_loop(queue, notify));
});
handles.push(handle);
}
let _ = DEDICATED_QUEUE.set(queue.clone());
let _ = DEDICATED_NOTIFY.set(notify.clone());
*guard = Some(handles);
(queue, notify)
}
fn ensure_swqos_pool(queue: Arc<ArrayQueue<SwqosJob>>, max_sender_concurrency: usize) {
if SWQOS_WORKERS_STARTED.swap(true, Ordering::AcqRel) {
return;
}
for _ in 0..SWQOS_POOL_WORKERS {
tokio::spawn(swqos_worker_loop(queue.clone()));
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
let notify = SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone();
for _ in 0..n {
tokio::spawn(swqos_worker_loop(queue.clone(), notify.clone()));
}
}
@@ -193,7 +264,7 @@ fn is_landed_error(error: &anyhow::Error) -> bool {
struct ResultCollector {
results: Arc<ArrayQueue<TaskResult>>,
success_flag: Arc<AtomicBool>,
landed_failed_flag: Arc<AtomicBool>, // 🔧 Tx landed on-chain but failed (nonce consumed)
landed_failed_flag: Arc<AtomicBool>, // 🔧 Tx landed on-chain but failed (nonce consumed)
completed_count: Arc<AtomicUsize>,
total_tasks: usize,
}
@@ -226,7 +297,9 @@ impl ResultCollector {
self.completed_count.fetch_add(1, Ordering::Release);
}
async fn wait_for_success(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
async fn wait_for_success(
&self,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
let start = Instant::now();
let timeout = std::time::Duration::from_secs(5);
let poll_interval = std::time::Duration::from_millis(1000);
@@ -268,7 +341,7 @@ impl ResultCollector {
}
let completed = self.completed_count.load(Ordering::Acquire);
if completed >= self.total_tasks {
if completed >= self.total_tasks {
let mut signatures = Vec::new();
let mut last_error = None;
let mut any_success = false;
@@ -296,7 +369,9 @@ impl ResultCollector {
}
}
fn get_first(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
fn get_first(
&self,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
let mut signatures = Vec::new();
let mut has_success = false;
let mut last_error = None;
@@ -322,7 +397,10 @@ impl ResultCollector {
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
async fn wait_for_all_submitted(&self, timeout_secs: u64) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
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 timeout = std::time::Duration::from_secs(timeout_secs);
let poll_interval = std::time::Duration::from_millis(2);
@@ -337,10 +415,12 @@ impl ResultCollector {
}
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
///
/// `sender_config` merges sender_thread_cores, effective_core_ids, max_sender_concurrency (precomputed at SDK init; no get_core_ids on hot path).
pub async fn execute_parallel(
swqos_clients: &[Arc<SwqosClient>],
payer: Arc<Keypair>,
rpc: Option<Arc<SolanaRpcClient>>,
rpc: Option<&Arc<SolanaRpcClient>>,
instructions: Vec<Instruction>,
address_lookup_table_account: Option<AddressLookupTableAccount>,
recent_blockhash: Option<Hash>,
@@ -351,11 +431,10 @@ pub async fn execute_parallel(
wait_transaction_confirmed: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
use_core_affinity: bool,
use_dedicated_sender_threads: bool,
sender_config: SenderConcurrencyConfig,
check_min_tip: bool,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
let _exec_start = Instant::now();
if swqos_clients.is_empty() {
return Err(anyhow!("swqos_clients is empty"));
}
@@ -369,48 +448,40 @@ pub async fn execute_parallel(
return Err(anyhow!("No Rpc Default Swqos configured."));
}
let cores = core_affinity::get_core_ids().unwrap_or_default();
let instructions = Arc::new(instructions);
// Precompute all valid (client, gas config) combinations
let task_configs: Vec<_> = swqos_clients
.iter()
.enumerate()
.filter(|(_, swqos_client)| {
with_tip || matches!(swqos_client.get_swqos_type(), SwqosType::Default)
})
.flat_map(|(i, swqos_client)| {
let swqos_type = swqos_client.get_swqos_type();
let gas_fee_strategy_configs = gas_fee_strategy.get_strategies(if is_buy {
TradeType::Buy
} else {
TradeType::Sell
});
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
let min_tip = if check_tip {
swqos_client.min_tip_sol()
} else {
0.0
};
gas_fee_strategy_configs
.into_iter()
.filter(move |config| config.0 == swqos_type)
.filter(move |config| {
if check_tip {
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
config.0, config.2.tip, min_tip
);
}
config.2.tip >= min_tip
} else {
true
}
})
.map(move |config| (i, swqos_client.clone(), config))
})
.collect();
// One get_strategies call per batch (avoid N calls in loop).
let gas_fee_configs = gas_fee_strategy.get_strategies(if is_buy {
TradeType::Buy
} else {
TradeType::Sell
});
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
for (i, swqos_client) in swqos_clients.iter().enumerate() {
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
continue;
}
let swqos_type = swqos_client.get_swqos_type();
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
let min_tip = if check_tip { swqos_client.min_tip_sol() } else { 0.0 };
for config in &gas_fee_configs {
if config.0 != swqos_type {
continue;
}
if check_tip {
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
println!(
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
config.0, config.2.tip, min_tip
);
}
if config.2.tip < min_tip {
continue;
}
}
task_configs.push((i, swqos_client.clone(), *config));
}
}
if task_configs.is_empty() {
return Err(anyhow!("No available gas fee strategy configs"));
@@ -425,7 +496,7 @@ pub async fn execute_parallel(
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
rpc,
rpc: rpc.cloned(),
address_lookup_table_account,
recent_blockhash,
durable_nonce,
@@ -437,19 +508,28 @@ pub async fn execute_parallel(
collector: collector.clone(),
});
let queue = SWQOS_QUEUE.get_or_init(|| Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP)));
ensure_swqos_pool(queue.clone());
let (queue, notify) = if use_dedicated_sender_threads {
ensure_dedicated_pool(
sender_config.sender_thread_cores.as_ref().map(|a| a.as_slice()),
sender_config.max_sender_concurrency,
)
} else {
let q = SWQOS_QUEUE.get_or_init(|| Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP)));
ensure_swqos_pool(q.clone(), sender_config.max_sender_concurrency);
(q.clone(), SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone())
};
{
// Cache tip_account per client (one get_tip_account/from_str per unique client per batch). Dropped before await so future stays Send.
let effective_core_ids = sender_config.effective_core_ids.as_slice();
let core_len = effective_core_ids.len().max(1);
let mut tip_cache: FnvHashMap<*const (), Arc<Pubkey>> =
FnvHashMap::with_capacity_and_hasher(task_configs.len(), BuildHasherDefault::default());
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
let core_id = cores.get(i % cores.len().max(1)).copied();
let core_id = effective_core_ids.get(i % core_len).copied();
let swqos_type = swqos_client.get_swqos_type();
let key = Arc::as_ptr(&swqos_client) as *const ();
let tip_account = match tip_cache.get(&key) {
Some(tip) => tip.clone(),
Some(t) => t.clone(),
None => {
let s = swqos_client.get_tip_account()?;
let tip = Arc::new(Pubkey::from_str(&s).unwrap_or_default());
@@ -471,20 +551,24 @@ pub async fn execute_parallel(
swqos_client,
swqos_type,
core_id,
use_affinity: use_core_affinity,
use_affinity: !effective_core_ids.is_empty(),
};
let _ = queue.push(job);
}
}
notify.notify_waiters();
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
const SUBMIT_TIMEOUT_SECS: u64 = 30;
let ret = collector
.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS)
.await
.unwrap_or((false, vec![], Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)), vec![]));
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)),
vec![],
));
let (success, signatures, last_error, submit_timings) = ret;
return Ok((success, signatures, last_error, submit_timings));
}
+3 -10
View File
@@ -2,16 +2,9 @@
//! 执行模块:指令预处理、缓存预取、分支提示。
use anyhow::Result;
use solana_sdk::{
instruction::Instruction,
pubkey::Pubkey,
signature::Keypair,
};
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Keypair};
use crate::perf::{
hardware_optimizations::BranchOptimizer,
simd::SIMDMemory,
};
use crate::perf::{hardware_optimizations::BranchOptimizer, simd::SIMDMemory};
/// Solana account key size in bytes (Pubkey). 每个账户(Pubkey)的字节数。
pub const BYTES_PER_ACCOUNT: usize = 32;
@@ -150,4 +143,4 @@ impl ExecutionPath {
slow_path()
}
}
}
}
+98 -67
View File
@@ -4,10 +4,15 @@ use solana_sdk::{
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
signature::Keypair, signature::Signature,
};
use std::{sync::Arc, time::{Duration, Instant}};
use std::{
sync::Arc,
time::{Duration, Instant},
};
#[allow(unused_imports)]
use tracing::{info, trace, warn};
use super::{params::SwapParams, traits::InstructionBuilder};
use crate::swqos::TradeType;
use crate::{
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
perf::syscall_bypass::SystemCallBypassManager,
@@ -20,8 +25,6 @@ use crate::{
trading::MiddlewareManager,
};
use once_cell::sync::Lazy;
use crate::swqos::TradeType;
use super::{params::SwapParams, traits::InstructionBuilder};
/// Global syscall bypass manager (reserved for future time/IO optimizations).
/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
@@ -49,7 +52,10 @@ impl GenericTradeExecutor {
#[async_trait::async_trait]
impl TradeExecutor for GenericTradeExecutor {
async fn swap(&self, params: SwapParams) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
let timing_start_us: Option<i64> = if params.log_enabled {
@@ -58,7 +64,8 @@ impl TradeExecutor for GenericTradeExecutor {
None
};
let is_buy = params.trade_type == TradeType::Buy || params.trade_type == TradeType::CreateAndBuy;
let is_buy =
params.trade_type == TradeType::Buy || params.trade_type == TradeType::CreateAndBuy;
Prefetch::keypair(&params.payer);
@@ -85,7 +92,8 @@ impl TradeExecutor for GenericTradeExecutor {
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_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);
@@ -110,24 +118,50 @@ impl TradeExecutor for GenericTradeExecutor {
if crate::common::sdk_log::sdk_log_enabled() {
let dir = if is_buy { "Buy" } else { "Sell" };
println!();
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);
println!(
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
"-",
dir,
(end_us - start_us) as f64 / 1000.0,
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
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][{:width$}] {} before_submit: {:.4} ms",
"-",
dir,
(end_us - start_us) as f64 / 1000.0,
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
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);
println!(
" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms",
"-",
dir,
send_elapsed.as_secs_f64() * 1000.0,
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
println!(
" [SDK][{:width$}] {} total: {:.4} ms",
"-",
dir,
total_elapsed.as_secs_f64() * 1000.0,
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
return result;
}
let need_confirm = params.wait_transaction_confirmed;
let need_confirm = params.wait_tx_confirmed;
let sender_config = params.sender_concurrency_config();
let result = execute_parallel(
&params.swqos_clients,
params.swqos_clients.as_slice(),
params.payer,
params.rpc.clone(),
params.rpc.as_ref(),
final_instructions,
params.address_lookup_table_account,
params.recent_blockhash,
@@ -138,7 +172,8 @@ impl TradeExecutor for GenericTradeExecutor {
false, // submit only here; confirmation and log timing handled below
if is_buy { true } else { params.with_tip },
params.gas_fee_strategy,
params.use_core_affinity,
params.use_dedicated_sender_threads,
sender_config,
params.check_min_tip,
)
.await;
@@ -146,14 +181,12 @@ impl TradeExecutor for GenericTradeExecutor {
let log_enabled = params.log_enabled && crate::common::sdk_log::sdk_log_enabled();
let (ok, signatures, err, submit_timings) = match result {
Ok((success, sigs, last_error, timings)) => (
success,
sigs,
last_error.map(|e| anyhow::anyhow!("{}", e)),
timings,
),
Ok((success, sigs, last_error, timings)) => {
(success, sigs, last_error.map(|e| anyhow::anyhow!("{}", e)), timings)
}
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e)), vec![]),
};
// submit_timings 为完成先后顺序(先完成的先 push),打印不排序、不增加延迟
let submit_timings_ref: &[(crate::swqos::SwqosType, i64)] = submit_timings.as_slice();
let result = if need_confirm {
@@ -165,22 +198,14 @@ impl TradeExecutor for GenericTradeExecutor {
let confirm_done_us = log_enabled.then(crate::common::clock::now_micros);
if log_enabled {
let dir = if is_buy { "Buy" } else { "Sell" };
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);
}
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_ref {
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);
}
}
}
crate::common::sdk_log::print_sdk_timing_block(
dir,
timing_start_us,
build_end_us,
before_submit_us,
submit_timings_ref,
confirm_done_us,
);
}
match poll_res {
Ok(_) => (true, signatures, None),
@@ -192,20 +217,17 @@ impl TradeExecutor for GenericTradeExecutor {
};
Ok(confirm_result)
} else {
// Not waiting for confirmation: confirmed is not measured (-); total is per-channel submit time only.
if log_enabled {
let dir = if is_buy { "Buy" } else { "Sell" };
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_ref {
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);
}
}
crate::common::sdk_log::print_sdk_timing_block(
dir,
timing_start_us,
build_end_us,
before_submit_us,
submit_timings_ref,
None,
);
}
Ok((ok, signatures, err))
};
@@ -278,14 +300,14 @@ async fn simulate_transaction(
.simulate_transaction_with_config(
&transaction,
RpcSimulateTransactionConfig {
sig_verify: false, // Don't verify signature during simulation for speed
sig_verify: false, // Don't verify signature during simulation for speed
replace_recent_blockhash: false, // Use actual blockhash from transaction
commitment: Some(CommitmentConfig {
commitment: CommitmentLevel::Processed, // Use Processed level to get latest state
}),
encoding: Some(UiTransactionEncoding::Base64), // Base64 encoding
accounts: None, // Don't return specific account states (can be specified if needed)
min_context_slot: None, // Don't specify minimum context slot
accounts: None, // Don't return specific account states (can be specified if needed)
min_context_slot: None, // Don't specify minimum context slot
inner_instructions: true, // Enable inner instructions for debugging and detailed execution flow
},
)
@@ -336,38 +358,47 @@ mod tests {
let dir = "Buy";
let build_ms = 12.34;
let before_submit_ms = 15.67;
let w = 12usize; // same as crate::common::sdk_log::SWQOS_LABEL_WIDTH
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!(" [SDK][{:width$}] {} build_instructions: {:.4} ms", "-", dir, build_ms, width = w);
println!(" [SDK][{:width$}] {} before_submit: {:.4} ms", "-", dir, before_submit_ms, width = w);
println!("\n--- 2. 每个 SWQOS 独立耗时:submit=起点→该通道返回, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
(SwqosType::Jito, 45.12, 83.38, 128.50),
(SwqosType::Helius, 52.30, 76.20, 128.50),
(SwqosType::ZeroSlot, 48.90, 79.60, 128.50),
] {
println!(
" [SDK] {} {:?} submit: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
dir, swqos_type, submit_ms, confirmed_ms, total_ms
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
swqos_type.as_str(),
dir,
submit_ms,
confirmed_ms,
total_ms,
width = w
);
}
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!("\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时---\n");
for (swqos_type, submit_ms, total_ms) in
[(SwqosType::Jito, 44.20, 44.20), (SwqosType::Helius, 51.80, 51.80)]
{
println!(
" [SDK] {} {:?} submit: {:.4} ms, confirmed: -, total: {:.4} ms",
dir, swqos_type, submit_ms, total_ms
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
swqos_type.as_str(),
dir,
submit_ms,
total_ms,
width = w
);
}
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!(" [SDK][{:width$}] {} build_instructions: {:.4} ms", "-", dir, build_ms, width = w);
println!(" [SDK][{:width$}] {} before_submit: {:.4} ms", "-", dir, before_submit_ms, width = w);
println!(" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms", "-", dir, 8.50, width = w);
println!(" [SDK][{:width$}] {} total: {:.4} ms", "-", dir, 36.51, width = w);
println!();
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
pub mod async_executor;
pub mod execution;
pub mod executor;
pub mod params;
pub mod traits;
pub mod executor;
pub mod async_executor;
pub mod transaction_pool;
pub mod execution;
+32 -4
View File
@@ -3,7 +3,16 @@ use crate::common::nonce_cache::DurableNonceInfo;
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::{GasFeeStrategy, SolanaRpcClient};
use crate::constants::TOKEN_PROGRAM;
use core_affinity::CoreId;
use crate::instruction::utils::pumpfun::global_constants::MAYHEM_FEE_RECIPIENT;
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
#[derive(Clone)]
pub struct SenderConcurrencyConfig {
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
pub effective_core_ids: Arc<Vec<CoreId>>,
pub max_sender_concurrency: usize,
}
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
use crate::swqos::{SwqosClient, TradeType};
use crate::trading::common::get_multi_token_balances;
@@ -53,10 +62,11 @@ pub struct SwapParams {
pub slippage_basis_points: Option<u64>,
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
pub recent_blockhash: Option<Hash>,
pub wait_transaction_confirmed: bool,
pub wait_tx_confirmed: bool,
pub protocol_params: DexParamEnum,
pub open_seed_optimize: bool,
pub swqos_clients: Vec<Arc<SwqosClient>>,
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
pub middleware_manager: Option<Arc<MiddlewareManager>>,
pub durable_nonce: Option<DurableNonceInfo>,
pub with_tip: bool,
@@ -69,8 +79,14 @@ pub struct SwapParams {
pub simulate: bool,
/// Whether to output SDK logs (from TradeConfig.log_enabled).
pub log_enabled: bool,
/// Whether to pin parallel submit tasks to cores (from TradeConfig.use_core_affinity).
pub use_core_affinity: bool,
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
pub use_dedicated_sender_threads: bool,
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
pub max_sender_concurrency: usize,
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
pub effective_core_ids: Arc<Vec<CoreId>>,
/// 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.
@@ -82,6 +98,18 @@ pub struct SwapParams {
pub use_exact_sol_amount: Option<bool>,
}
impl SwapParams {
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
#[inline]
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
SenderConcurrencyConfig {
sender_thread_cores: self.sender_thread_cores.clone(),
effective_core_ids: self.effective_core_ids.clone(),
max_sender_concurrency: self.max_sender_concurrency,
}
}
}
impl std::fmt::Debug for SwapParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "SwapParams: ...")
+4 -1
View File
@@ -9,7 +9,10 @@ pub trait TradeExecutor: Send + Sync {
/// - bool: 是否至少有一个交易成功
/// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
/// - Option<anyhow::Error>: 最后一个错误(如果全部失败)
async fn swap(&self, params: SwapParams) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)>;
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)>;
/// 获取协议名称
fn protocol_name(&self) -> &'static str;
}
+15 -18
View File
@@ -12,13 +12,18 @@ const TX_BUILDER_INSTRUCTION_CAP: usize = 32;
const TX_BUILDER_LOOKUP_TABLE_CAP: usize = 8;
/// 对象池最大容量
const TX_BUILDER_POOL_CAP: usize = 1000;
/// 启动时预填充对象池数量
const TX_BUILDER_POOL_PREFILL: usize = 100;
/// 多路提交并发数(与 async_executor SWQOS_DEDICATED_DEFAULT_THREADS 一致,保证不串行)
const PARALLEL_SENDER_COUNT: usize = 18;
/// 启动时预填充数量,必须 >= PARALLEL_SENDER_COUNT,否则 18 路并发 build 会触发分配或争抢
const TX_BUILDER_POOL_PREFILL: usize = 64;
use crossbeam_queue::ArrayQueue;
use once_cell::sync::Lazy;
use solana_sdk::{
hash::Hash, instruction::Instruction, message::{v0, AddressLookupTableAccount, Message, VersionedMessage}, pubkey::Pubkey
hash::Hash,
instruction::Instruction,
message::{v0, AddressLookupTableAccount, Message, VersionedMessage},
pubkey::Pubkey,
};
use std::sync::Arc;
/// 预分配的交易构建器
@@ -91,11 +96,8 @@ impl PreallocatedTxBuilder {
VersionedMessage::V0(message)
} else {
// ✅ 没有查找表,使用 Legacy 消息(兼容所有 RPC
let message = Message::new_with_blockhash(
&self.instructions,
Some(payer),
&recent_blockhash,
);
let message =
Message::new_with_blockhash(&self.instructions, Some(payer), &recent_blockhash);
VersionedMessage::Legacy(message)
}
}
@@ -104,20 +106,17 @@ impl PreallocatedTxBuilder {
/// 🚀 全局交易构建器对象池
static TX_BUILDER_POOL: Lazy<Arc<ArrayQueue<PreallocatedTxBuilder>>> = Lazy::new(|| {
let pool = ArrayQueue::new(TX_BUILDER_POOL_CAP);
for _ in 0..TX_BUILDER_POOL_PREFILL {
let prefill = TX_BUILDER_POOL_PREFILL.max(PARALLEL_SENDER_COUNT);
for _ in 0..prefill {
let _ = pool.push(PreallocatedTxBuilder::new());
}
Arc::new(pool)
});
/// 🚀 从池中获取构建器
#[inline(always)]
pub fn acquire_builder() -> PreallocatedTxBuilder {
TX_BUILDER_POOL
.pop()
.unwrap_or_else(|| PreallocatedTxBuilder::new())
TX_BUILDER_POOL.pop().unwrap_or_else(|| PreallocatedTxBuilder::new())
}
/// 🚀 归还构建器到池
@@ -139,9 +138,7 @@ pub struct TxBuilderGuard {
impl TxBuilderGuard {
pub fn new() -> Self {
Self {
builder: Some(acquire_builder()),
}
Self { builder: Some(acquire_builder()) }
}
pub fn get_mut(&mut self) -> &mut PreallocatedTxBuilder {
@@ -155,4 +152,4 @@ impl Drop for TxBuilderGuard {
release_builder(builder);
}
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod traits;
pub mod builtin;
pub mod traits;
pub use traits::{InstructionMiddleware, MiddlewareManager};
+2 -5
View File
@@ -76,11 +76,8 @@ impl MiddlewareManager {
is_buy: bool,
) -> Result<Vec<Instruction>> {
for middleware in &self.middlewares {
full_instructions = middleware.process_full_instructions(
full_instructions,
protocol_name,
is_buy,
)?;
full_instructions =
middleware.process_full_instructions(full_instructions, protocol_name, is_buy)?;
if full_instructions.is_empty() {
break;
}
+4 -4
View File
@@ -1,6 +1,6 @@
pub mod pumpfun;
pub mod common;
pub mod pumpswap;
pub mod bonk;
pub mod common;
pub mod pumpfun;
pub mod pumpswap;
pub mod raydium_amm_v4;
pub mod raydium_cpmm;
pub mod raydium_cpmm;
+21 -4
View File
@@ -14,7 +14,8 @@ impl TradingClient {
#[inline]
pub async fn get_payer_sol_balance(&self) -> Result<u64, anyhow::Error> {
trading::common::utils::get_sol_balance(&self.infrastructure.rpc, &self.payer.pubkey()).await
trading::common::utils::get_sol_balance(&self.infrastructure.rpc, &self.payer.pubkey())
.await
}
#[inline]
@@ -28,7 +29,12 @@ impl TradingClient {
#[inline]
pub async fn get_payer_token_balance(&self, mint: &Pubkey) -> Result<u64, anyhow::Error> {
trading::common::utils::get_token_balance(&self.infrastructure.rpc, &self.payer.pubkey(), mint).await
trading::common::utils::get_token_balance(
&self.infrastructure.rpc,
&self.payer.pubkey(),
mint,
)
.await
}
/// 使用与交易一致的 ATA 推导(含 seed 优化)查询 payer 某 mint 的余额;卖出前查余额应使用此接口并传入池的 base_token_program,否则若使用 seed ATA 会查错账户。
@@ -65,11 +71,22 @@ impl TradingClient {
receive_wallet: &Pubkey,
amount: u64,
) -> Result<(), anyhow::Error> {
trading::common::utils::transfer_sol(&self.infrastructure.rpc, payer, receive_wallet, amount).await
trading::common::utils::transfer_sol(
&self.infrastructure.rpc,
payer,
receive_wallet,
amount,
)
.await
}
#[inline]
pub async fn close_token_account(&self, mint: &Pubkey) -> Result<(), anyhow::Error> {
trading::common::utils::close_token_account(&self.infrastructure.rpc, self.payer.as_ref(), mint).await
trading::common::utils::close_token_account(
&self.infrastructure.rpc,
self.payer.as_ref(),
mint,
)
.await
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
pub mod bonk;
pub mod common;
pub mod pumpfun;
pub mod pumpswap;
pub mod raydium_amm_v4;
pub mod raydium_clmm;
pub mod raydium_cpmm;
pub mod common;