Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d062279d9 | ||
|
|
15e07e9130 | ||
|
|
6eaafde4fd | ||
|
|
0f37950adf | ||
|
|
dd4f42324e | ||
|
|
d0897b53d3 | ||
|
|
e67a21df58 | ||
|
|
147c6068d1 | ||
|
|
6ce8ee582e | ||
|
|
bcc6860bc3 | ||
|
|
1b7e3781c7 | ||
|
|
807b015fc3 | ||
|
|
8f6cc6fb08 | ||
|
|
5e4977f6a6 | ||
|
|
c27e479659 | ||
|
|
9a8e3ffb2d | ||
|
|
43d1369d4e | ||
|
|
a048f3b6ad | ||
|
|
feaac5ddd2 | ||
|
|
710a8d482f |
@@ -0,0 +1,35 @@
|
|||||||
|
# 推送 tag(如 v3.4.1)时自动创建 GitHub Release
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Get version from tag
|
||||||
|
id: tag
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
name: v${{ steps.tag.outputs.VERSION }}
|
||||||
|
body: |
|
||||||
|
## sol-trade-sdk ${{ steps.tag.outputs.VERSION }}
|
||||||
|
Rust SDK to interact with the dex trade Solana program (Pump.fun, Raydium, etc.).
|
||||||
|
- **Cargo**: `sol-trade-sdk = { git = "https://github.com/${{ github.repository }}", tag = "v${{ steps.tag.outputs.VERSION }}" }`
|
||||||
|
draft: false
|
||||||
|
generate_release_notes: true
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# 更新日志
|
|
||||||
|
|
||||||
## [3.3.6] - 2025-01-30
|
|
||||||
|
|
||||||
### 新增
|
|
||||||
- **Stellium SWQOS 支持**:全新 Stellium 客户端实现
|
|
||||||
- 使用标准 Solana `sendTransaction` RPC 格式
|
|
||||||
- 自动连接保活,60 秒 ping 间隔
|
|
||||||
- 5 个小费账户用于负载分配
|
|
||||||
- 支持 8 个区域端点(纽约、法兰克福、阿姆斯特丹、东京、伦敦等)
|
|
||||||
- 最低小费要求:0.001 SOL
|
|
||||||
|
|
||||||
### 变更
|
|
||||||
- **更新最低小费要求**以提高交易成功率:
|
|
||||||
- NextBlock: 0.00001 → 0.001 SOL
|
|
||||||
- ZeroSlot: 0.00001 → 0.001 SOL
|
|
||||||
- Temporal: 0.00001 → 0.001 SOL
|
|
||||||
- BloxRoute: 0.00001 → 0.001 SOL
|
|
||||||
- FlashBlock: 0.00001 → 0.001 SOL
|
|
||||||
- BlockRazor: 0.00001 → 0.001 SOL
|
|
||||||
- 增强异步执行器,添加小费验证警告
|
|
||||||
+1
-4
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sol-trade-sdk"
|
name = "sol-trade-sdk"
|
||||||
version = "3.4.0"
|
version = "3.5.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"William <byteblock6@gmail.com>",
|
"William <byteblock6@gmail.com>",
|
||||||
@@ -19,8 +19,6 @@ members = [
|
|||||||
"examples/trading_client",
|
"examples/trading_client",
|
||||||
"examples/shared_infrastructure",
|
"examples/shared_infrastructure",
|
||||||
"examples/middleware_system",
|
"examples/middleware_system",
|
||||||
"examples/pumpfun_copy_trading",
|
|
||||||
"examples/pumpfun_sniper_trading",
|
|
||||||
"examples/pumpswap_trading",
|
"examples/pumpswap_trading",
|
||||||
"examples/bonk_sniper_trading",
|
"examples/bonk_sniper_trading",
|
||||||
"examples/bonk_copy_trading",
|
"examples/bonk_copy_trading",
|
||||||
@@ -81,7 +79,6 @@ rustls = { version = "0.23.23", features = ["ring"] }
|
|||||||
rustls-native-certs = "0.8.1"
|
rustls-native-certs = "0.8.1"
|
||||||
tokio-rustls = "0.26.1"
|
tokio-rustls = "0.26.1"
|
||||||
core_affinity = "0.8"
|
core_affinity = "0.8"
|
||||||
log = "0.4.22"
|
|
||||||
chrono = "0.4.39"
|
chrono = "0.4.39"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
|||||||
@@ -47,10 +47,11 @@
|
|||||||
- [📋 Example Usage](#-example-usage)
|
- [📋 Example Usage](#-example-usage)
|
||||||
- [⚡ Trading Parameters](#-trading-parameters)
|
- [⚡ Trading Parameters](#-trading-parameters)
|
||||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||||
- [⚙️ SWQOS Service Configuration](#️-swqos-service-configuration)
|
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||||
- [🔧 Middleware System](#-middleware-system)
|
- [🔧 Middleware System](#-middleware-system)
|
||||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||||
- [🔍 Nonce Cache](#-nonce-cache)
|
- [🔍 Nonce Cache](#-nonce-cache)
|
||||||
|
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
|
||||||
- [🛡️ MEV Protection Services](#️-mev-protection-services)
|
- [🛡️ MEV Protection Services](#️-mev-protection-services)
|
||||||
- [📁 Project Structure](#-project-structure)
|
- [📁 Project Structure](#-project-structure)
|
||||||
- [📄 License](#-license)
|
- [📄 License](#-license)
|
||||||
@@ -71,7 +72,7 @@
|
|||||||
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
|
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
|
||||||
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
|
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
|
||||||
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
|
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
|
||||||
11. **Shared Infrastructure**: Share expensive RPC and SWQOS clients across multiple wallets for reduced resource usage
|
11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
@@ -88,14 +89,14 @@ Add the dependency to your `Cargo.toml`:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Add to your Cargo.toml
|
# Add to your Cargo.toml
|
||||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" }
|
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.4" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Use crates.io
|
### Use crates.io
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# Add to your Cargo.toml
|
# Add to your Cargo.toml
|
||||||
sol-trade-sdk = "3.4.0"
|
sol-trade-sdk = "3.5.4"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ Usage Examples
|
## 🛠️ Usage Examples
|
||||||
@@ -113,7 +114,7 @@ let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
|||||||
// RPC URL
|
// RPC URL
|
||||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||||
let commitment = CommitmentConfig::processed();
|
let commitment = CommitmentConfig::processed();
|
||||||
// Multiple SWQOS services can be configured
|
// Multiple SWQoS services can be configured
|
||||||
let swqos_configs: Vec<SwqosConfig> = vec |
|
| Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
||||||
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||||
|
|
||||||
### ⚙️ SWQOS Service Configuration
|
### ⚙️ SWQoS Service Configuration
|
||||||
|
|
||||||
When configuring SWQOS services, note the different parameter requirements for each service:
|
When configuring SWQoS services, note the different parameter requirements for each service:
|
||||||
|
|
||||||
- **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`)
|
- **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`)
|
||||||
- **Other MEV services**: The first parameter is the API Token
|
- **Other MEV services**: The first parameter is the API Token
|
||||||
|
|
||||||
#### Custom URL Support
|
#### Custom URL Support
|
||||||
|
|
||||||
Each SWQOS service now supports an optional custom URL parameter:
|
Each SWQoS service now supports an optional custom URL parameter:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Using custom URL (third parameter)
|
// Using custom URL (third parameter)
|
||||||
@@ -273,6 +278,17 @@ Address Lookup Tables (ALT) allow you to optimize transaction size and reduce fe
|
|||||||
|
|
||||||
Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md).
|
Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md).
|
||||||
|
|
||||||
|
## 💰 Cashback Support (PumpFun / PumpSwap)
|
||||||
|
|
||||||
|
PumpFun and PumpSwap support **cashback** for eligible tokens: part of the trading fee can be returned to the user. The SDK **must know** whether the token has cashback enabled so that buy/sell instructions include the correct accounts (e.g. `UserVolumeAccumulator` as remaining account for cashback coins).
|
||||||
|
|
||||||
|
- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step.
|
||||||
|
- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK:
|
||||||
|
- **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `is_cashback_coin`).
|
||||||
|
- **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data.
|
||||||
|
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
|
||||||
|
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
|
||||||
|
|
||||||
## 🛡️ MEV Protection Services
|
## 🛡️ MEV Protection Services
|
||||||
|
|
||||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||||
|
|||||||
+40
-20
@@ -47,10 +47,11 @@
|
|||||||
- [📋 使用示例](#-使用示例)
|
- [📋 使用示例](#-使用示例)
|
||||||
- [⚡ 交易参数](#-交易参数)
|
- [⚡ 交易参数](#-交易参数)
|
||||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||||
- [⚙️ SWQOS 服务配置说明](#️-swqos-服务配置说明)
|
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||||
- [🔍 地址查找表](#-地址查找表)
|
- [🔍 地址查找表](#-地址查找表)
|
||||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||||
|
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||||
- [📁 项目结构](#-项目结构)
|
- [📁 项目结构](#-项目结构)
|
||||||
- [📄 许可证](#-许可证)
|
- [📄 许可证](#-许可证)
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||||
|
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
||||||
@@ -87,14 +89,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
# 添加到您的 Cargo.toml
|
# 添加到您的 Cargo.toml
|
||||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" }
|
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.4" }
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用 crates.io
|
### 使用 crates.io
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
# 添加到您的 Cargo.toml
|
# 添加到您的 Cargo.toml
|
||||||
sol-trade-sdk = "3.4.0"
|
sol-trade-sdk = "3.5.4"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🛠️ 使用示例
|
## 🛠️ 使用示例
|
||||||
@@ -103,40 +105,46 @@ sol-trade-sdk = "3.4.0"
|
|||||||
|
|
||||||
#### 1. 创建 TradingClient 实例
|
#### 1. 创建 TradingClient 实例
|
||||||
|
|
||||||
可以参考 [示例:创建 TradingClient 实例](examples/trading_client/src/main.rs)。
|
可参考 [示例:创建 TradingClient 实例](examples/trading_client/src/main.rs)。
|
||||||
|
|
||||||
|
**方式一:简单创建(单钱包)**
|
||||||
```rust
|
```rust
|
||||||
// 钱包
|
// 钱包
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||||
// RPC 地址
|
// RPC 地址
|
||||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||||
let commitment = CommitmentConfig::processed();
|
let commitment = CommitmentConfig::processed();
|
||||||
// 可以配置多个SWQOS服务
|
// 可配置多个 SWQoS 服务
|
||||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||||
SwqosConfig::Default(rpc_url.clone()),
|
SwqosConfig::Default(rpc_url.clone()),
|
||||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
|
||||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
|
||||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
|
||||||
SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
|
||||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
|
||||||
SwqosConfig::Astralane("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
|
||||||
];
|
];
|
||||||
// 创建 TradeConfig 实例
|
// 创建 TradeConfig 实例
|
||||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||||
|
|
||||||
// 可选:自定义 WSOL ATA 和 Seed 优化设置
|
// 可选:自定义 WSOL ATA 与 Seed 优化
|
||||||
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||||
// .with_wsol_ata_config(
|
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
|
||||||
// true, // create_wsol_ata_on_startup: 启动时检查并创建 WSOL ATA(默认: true)
|
|
||||||
// true // use_seed_optimize: 全局启用所有 ATA 操作的 seed 优化(默认: true)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// 创建 TradingClient 客户端
|
// 创建 TradingClient
|
||||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**方式二:共享基础设施(多钱包)**
|
||||||
|
|
||||||
|
多钱包场景下可先创建一份基础设施,再复用到多个钱包。参见 [示例:共享基础设施](examples/shared_infrastructure/src/main.rs)。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 创建一次基础设施(开销较大)
|
||||||
|
let infra_config = InfrastructureConfig::new(rpc_url, swqos_configs, commitment);
|
||||||
|
let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
|
||||||
|
|
||||||
|
// 基于同一基础设施创建多个客户端(开销小)
|
||||||
|
let client1 = TradingClient::from_infrastructure(Arc::new(payer1), infrastructure.clone(), true);
|
||||||
|
let client2 = TradingClient::from_infrastructure(Arc::new(payer2), infrastructure.clone(), true);
|
||||||
|
```
|
||||||
|
|
||||||
#### 2. 配置 Gas Fee 策略
|
#### 2. 配置 Gas Fee 策略
|
||||||
|
|
||||||
有关 Gas Fee 策略的详细信息,请参阅 [Gas Fee 策略参考手册](docs/GAS_FEE_STRATEGY_CN.md)。
|
有关 Gas Fee 策略的详细信息,请参阅 [Gas Fee 策略参考手册](docs/GAS_FEE_STRATEGY_CN.md)。
|
||||||
@@ -198,6 +206,7 @@ client.buy(buy_params).await?;
|
|||||||
| 描述 | 运行命令 | 源码路径 |
|
| 描述 | 运行命令 | 源码路径 |
|
||||||
|------|---------|----------|
|
|------|---------|----------|
|
||||||
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
||||||
|
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
|
||||||
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
||||||
| PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
|
| PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
|
||||||
| PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
|
| PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
|
||||||
@@ -213,16 +222,16 @@ client.buy(buy_params).await?;
|
|||||||
| Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
| Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
||||||
| Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
| Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||||
|
|
||||||
### ⚙️ SWQOS 服务配置说明
|
### ⚙️ SWQoS 服务配置说明
|
||||||
|
|
||||||
在配置 SWQOS 服务时,需要注意不同服务的参数要求:
|
在配置 SWQoS 服务时,需要注意不同服务的参数要求:
|
||||||
|
|
||||||
- **Jito**: 第一个参数为 UUID(如无 UUID 请传入空字符串 `""`)
|
- **Jito**: 第一个参数为 UUID(如无 UUID 请传入空字符串 `""`)
|
||||||
- 其他的MEV服务,第一个参数为 API Token
|
- 其他的MEV服务,第一个参数为 API Token
|
||||||
|
|
||||||
#### 自定义 URL 支持
|
#### 自定义 URL 支持
|
||||||
|
|
||||||
每个 SWQOS 服务现在都支持可选的自定义 URL 参数:
|
每个 SWQoS 服务现在都支持可选的自定义 URL 参数:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// 使用自定义 URL(第三个参数)
|
// 使用自定义 URL(第三个参数)
|
||||||
@@ -268,6 +277,17 @@ let middleware_manager = MiddlewareManager::new()
|
|||||||
|
|
||||||
使用 Durable Nonce 来实现交易重放保护和优化交易处理。详细信息请参阅 [Nonce 使用指南](docs/NONCE_CACHE_CN.md)。
|
使用 Durable Nonce 来实现交易重放保护和优化交易处理。详细信息请参阅 [Nonce 使用指南](docs/NONCE_CACHE_CN.md)。
|
||||||
|
|
||||||
|
## 💰 Cashback 支持(PumpFun / PumpSwap)
|
||||||
|
|
||||||
|
PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还给用户。SDK **必须知道**该代币是否开启返现,才能为 buy/sell 指令传入正确的账户(例如返现代币需要把 `UserVolumeAccumulator` 作为 remaining account)。
|
||||||
|
|
||||||
|
- **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc` 或 `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。
|
||||||
|
- **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK:
|
||||||
|
- **PumpFun**:`PumpFunParams::from_trade(..., is_cashback_coin)` 与 `PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
|
||||||
|
- **PumpSwap**:`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。
|
||||||
|
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。
|
||||||
|
- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。
|
||||||
|
|
||||||
## 🛡️ MEV 保护服务
|
## 🛡️ MEV 保护服务
|
||||||
|
|
||||||
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"url": "https://context7.com/0xfnzero/sol-trade-sdk",
|
||||||
|
"public_key": "pk_ShleAZazFTUV8ORpmH4jy"
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Pump Cashback 集成说明
|
||||||
|
|
||||||
|
本 SDK 已支持 [Pump Cashback Rewards](https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CASHBACK_README.md):在启用 cashback 的币种上交易时,用户可获得手续费返还而非支付给创作者。
|
||||||
|
|
||||||
|
## 行为概览
|
||||||
|
|
||||||
|
- **Bonding Curve (Pump)**
|
||||||
|
- **Buy**:无需改指令,若币种启用 cashback 会自动累计。
|
||||||
|
- **Sell**:当 `bonding_curve.is_cashback_coin == true` 时,SDK 会在指令中追加 `UserVolumeAccumulator` PDA(remaining account),用于累计可领取的 cashback。
|
||||||
|
- **Pump Swap**
|
||||||
|
- **Buy**:当 `PumpSwapParams.is_cashback_coin == true` 时,会追加 UserVolumeAccumulator 的 **WSOL ATA** 作为 remaining account。
|
||||||
|
- **Sell**:当 `is_cashback_coin == true` 时,会追加 **WSOL ATA**(0th)和 **UserVolumeAccumulator PDA**(1st)作为 remaining accounts。
|
||||||
|
|
||||||
|
`PumpFunParams` 通过 `from_mint_by_rpc` 拉取 bonding curve 时会解析链上 `is_cashback_coin`;`PumpSwapParams` 通过 `from_pool_address_by_rpc` / `from_mint_by_rpc` 拉取 pool 时会解析 `is_cashback_coin`。
|
||||||
|
|
||||||
|
## 领取 Cashback
|
||||||
|
|
||||||
|
### 推荐:使用 TradingClient 一键领取
|
||||||
|
|
||||||
|
若已有 `TradingClient`(例如用于买卖的同一个客户端),可直接调用以下方法,内部会完成构建交易、签名与发送:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 领取 Pump 曲线产生的 Cashback(到账为 native SOL)
|
||||||
|
let sig = client.claim_cashback_pumpfun().await?;
|
||||||
|
|
||||||
|
// 领取 PumpSwap 产生的 Cashback(到账为 WSOL,自动确保用户 WSOL ATA 存在)
|
||||||
|
let sig = client.claim_cashback_pumpswap().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`claim_cashback_pumpfun()`**:领取 Bonding Curve (Pump) 的返还,到账为钱包 SOL。
|
||||||
|
- **`claim_cashback_pumpswap()`**:领取 PumpSwap (AMM) 的返还,到账为用户的 WSOL ATA;若用户尚无 WSOL ATA 会先自动创建再领取。
|
||||||
|
|
||||||
|
### 仅构建指令(自行组交易时使用)
|
||||||
|
|
||||||
|
#### Bonding Curve (Pump)
|
||||||
|
|
||||||
|
将 native lamports 从 UserVolumeAccumulator 转到用户钱包:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sol_trade_sdk::instruction::pumpfun;
|
||||||
|
|
||||||
|
let ix = pumpfun::claim_cashback_pumpfun_instruction(&payer.pubkey());
|
||||||
|
// 将 ix 放入交易并发送
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pump Swap (AMM)
|
||||||
|
|
||||||
|
将 WSOL 从 UserVolumeAccumulator 的 WSOL ATA 转到用户的 WSOL ATA。**调用前需确保用户 WSOL ATA 已存在**(或使用上面的 `claim_cashback_pumpswap()` 会自动处理):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sol_trade_sdk::instruction::pumpswap;
|
||||||
|
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||||
|
use sol_trade_sdk::constants::TOKEN_PROGRAM;
|
||||||
|
|
||||||
|
let ix = pumpswap::claim_cashback_pumpswap_instruction(
|
||||||
|
&payer.pubkey(),
|
||||||
|
WSOL_TOKEN_ACCOUNT,
|
||||||
|
TOKEN_PROGRAM,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 读取未领取金额
|
||||||
|
|
||||||
|
- **Pump (Bonding Curve)**:读 Pump 程序的 `UserVolumeAccumulator` PDA 的 lamports,减去维持账户所需的 rent-exempt 金额,即为未领取 cashback(lamports)。
|
||||||
|
- **Pump Swap**:读 Pump AMM 程序的 UserVolumeAccumulator 的 **WSOL ATA** 的 token balance,即为未领取 cashback(WSOL 数量)。
|
||||||
|
|
||||||
|
PDA 推导(本 SDK 已实现):
|
||||||
|
|
||||||
|
- Pump:`instruction::utils::pumpfun::get_user_volume_accumulator_pda(user)`
|
||||||
|
- Pump AMM:`instruction::utils::pumpswap::get_user_volume_accumulator_pda(user)`,WSOL ATA:`instruction::utils::pumpswap::get_user_volume_accumulator_wsol_ata(user)`
|
||||||
|
|
||||||
|
## IDL 来源
|
||||||
|
|
||||||
|
根目录 `idl/` 下的 `pump.json`、`pump_amm.json`、`pump_fees.json` 从 [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs) 的 `idl` 目录同步,便于与官方 IDL 对照和后续升级。
|
||||||
@@ -143,13 +143,14 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
trade_info.mint,
|
trade_info.mint,
|
||||||
trade_info.creator,
|
trade_info.creator,
|
||||||
trade_info.creator_vault,
|
trade_info.creator_vault,
|
||||||
trade_info.virtual_sol_reserves,
|
trade_info.virtual_token_reserves,
|
||||||
trade_info.virtual_sol_reserves,
|
trade_info.virtual_sol_reserves,
|
||||||
trade_info.real_token_reserves,
|
trade_info.real_token_reserves,
|
||||||
trade_info.real_sol_reserves,
|
trade_info.real_sol_reserves,
|
||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
trade_info.fee_recipient,
|
||||||
trade_info.token_program,
|
trade_info.token_program,
|
||||||
|
false, // is_cashback_coin: set from event/parser when available
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: address_lookup_table_account,
|
address_lookup_table_account: address_lookup_table_account,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
trade_info.fee_recipient,
|
||||||
trade_info.token_program,
|
trade_info.token_program,
|
||||||
|
false, // is_cashback_coin: set from event/parser when available
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
solana-streamer-sdk = "0.5.0"
|
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
use std::sync::{
|
//! PumpFun 跟单示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
|
||||||
atomic::{AtomicBool, Ordering},
|
//!
|
||||||
Arc,
|
//! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。
|
||||||
};
|
|
||||||
|
|
||||||
|
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||||
|
|
||||||
|
use sol_parser_sdk::grpc::{
|
||||||
|
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||||
|
TransactionFilter, YellowstoneGrpc,
|
||||||
|
};
|
||||||
|
use sol_parser_sdk::DexEvent;
|
||||||
use sol_trade_sdk::common::{
|
use sol_trade_sdk::common::{
|
||||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
|
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
|
||||||
};
|
};
|
||||||
@@ -16,143 +22,128 @@ use sol_trade_sdk::{
|
|||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
use solana_sdk::signer::Signer;
|
use solana_sdk::signer::Signer;
|
||||||
use solana_streamer_sdk::match_event;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
|
||||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
|
||||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
|
||||||
|
|
||||||
// Global static flag to ensure transaction is executed only once
|
|
||||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Subscribing to GRPC events...");
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
println!("PumpFun 跟单示例(sol-parser-sdk gRPC)...");
|
||||||
|
|
||||||
let grpc = YellowstoneGrpc::new(
|
let config = ClientConfig {
|
||||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
enable_metrics: false,
|
||||||
None,
|
connection_timeout_ms: 10000,
|
||||||
)?;
|
request_timeout_ms: 30000,
|
||||||
|
enable_tls: true,
|
||||||
let callback = create_event_callback();
|
order_mode: OrderMode::Unordered,
|
||||||
let protocols = vec![Protocol::PumpFun];
|
..Default::default()
|
||||||
// Filter accounts
|
|
||||||
let account_include = vec![
|
|
||||||
PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID
|
|
||||||
];
|
|
||||||
let account_exclude = vec![];
|
|
||||||
let account_required = vec![];
|
|
||||||
|
|
||||||
// Listen to transaction data
|
|
||||||
let transaction_filter = TransactionFilter {
|
|
||||||
account_include: account_include.clone(),
|
|
||||||
account_exclude,
|
|
||||||
account_required,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to account data belonging to owner programs -> account event monitoring
|
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
|
||||||
|
let grpc = YellowstoneGrpc::new_with_config(
|
||||||
|
grpc_endpoint.clone(),
|
||||||
|
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||||
|
config,
|
||||||
|
)?;
|
||||||
|
|
||||||
// listen to specific event type
|
let protocols = vec![Protocol::PumpFun];
|
||||||
let event_type_filter =
|
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||||
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
|
let account_filter = AccountFilter::for_protocols(&protocols);
|
||||||
|
let event_filter = EventTypeFilter::include_only(vec![
|
||||||
|
EventType::PumpFunBuy,
|
||||||
|
EventType::PumpFunSell,
|
||||||
|
EventType::PumpFunBuyExactSolIn,
|
||||||
|
EventType::PumpFunTrade,
|
||||||
|
]);
|
||||||
|
|
||||||
grpc.subscribe_events_immediate(
|
let queue = grpc
|
||||||
protocols,
|
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||||
None,
|
.await?;
|
||||||
vec![transaction_filter],
|
|
||||||
vec![account_filter],
|
println!("订阅已启动,等待一条 PumpFun 交易后执行跟单(仅一次)...\n");
|
||||||
Some(event_type_filter),
|
|
||||||
None,
|
loop {
|
||||||
callback,
|
if let Some(event) = queue.pop() {
|
||||||
)
|
let run = match &event {
|
||||||
.await?;
|
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||||
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
Some(e.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DexEvent::PumpFunTrade(e) => {
|
||||||
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
Some(e.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(e) = run {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = pumpfun_copy_trade(e).await {
|
||||||
|
eprintln!("跟单执行错误: {:?}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await?;
|
tokio::signal::ctrl_c().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an event callback function that handles different types of events
|
|
||||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
|
||||||
|event: Box<dyn UnifiedEvent>| {
|
|
||||||
match_event!(event, {
|
|
||||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
|
||||||
// Test code, only test one transaction
|
|
||||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
|
||||||
let event_clone = e.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await {
|
|
||||||
eprintln!("Error in copy trade: {:?}", err);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create SolanaTrade client
|
|
||||||
/// Initializes a new SolanaTrade client with configuration
|
|
||||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||||
println!("🚀 Initializing SolanaTrade client...");
|
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||||
let rpc_url = "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 commitment = CommitmentConfig::confirmed();
|
||||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||||
println!("✅ SolanaTrade client initialized successfully!");
|
|
||||||
Ok(solana_trade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PumpFun sniper trade
|
async fn pumpfun_copy_trade(
|
||||||
/// This function demonstrates how to snipe a new token from a PumpFun trade event
|
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||||
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
) -> AnyResult<()> {
|
||||||
println!("Testing PumpFun trading...");
|
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
let client = create_solana_trade_client().await?;
|
||||||
let mint_pubkey = trade_info.mint;
|
let mint_pubkey = e.mint;
|
||||||
let slippage_basis_points = Some(100);
|
let slippage_basis_points = Some(100u64);
|
||||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||||
|
|
||||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||||
gas_fee_strategy.set_global_fee_strategy(
|
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||||
150000,
|
|
||||||
150000,
|
|
||||||
500000,
|
|
||||||
500000,
|
|
||||||
0.001,
|
|
||||||
0.001,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Buy tokens
|
// 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析)
|
||||||
println!("Buying tokens from PumpFun...");
|
let buy_sol_amount = 100_000u64;
|
||||||
let buy_sol_amount = 100_000;
|
|
||||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
input_token_type: TradeTokenType::SOL,
|
input_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: buy_sol_amount,
|
input_token_amount: buy_sol_amount,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
trade_info.bonding_curve,
|
e.bonding_curve,
|
||||||
trade_info.associated_bonding_curve,
|
e.associated_bonding_curve,
|
||||||
trade_info.mint,
|
e.mint,
|
||||||
trade_info.creator,
|
e.creator,
|
||||||
trade_info.creator_vault,
|
e.creator_vault,
|
||||||
trade_info.virtual_token_reserves,
|
e.virtual_token_reserves,
|
||||||
trade_info.virtual_sol_reserves,
|
e.virtual_sol_reserves,
|
||||||
trade_info.real_token_reserves,
|
e.real_token_reserves,
|
||||||
trade_info.real_sol_reserves,
|
e.real_sol_reserves,
|
||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
e.fee_recipient,
|
||||||
trade_info.token_program,
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
@@ -167,43 +158,40 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
// Sell tokens
|
// 卖出:查询余额后卖出,同样传入 is_cashback_coin
|
||||||
println!("Selling tokens from PumpFun...");
|
|
||||||
|
|
||||||
let rpc = client.infrastructure.rpc.clone();
|
let rpc = client.infrastructure.rpc.clone();
|
||||||
let payer = client.payer.pubkey();
|
let payer = client.payer.pubkey();
|
||||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||||
&payer,
|
&payer,
|
||||||
&mint_pubkey,
|
&mint_pubkey,
|
||||||
&trade_info.token_program,
|
&e.token_program,
|
||||||
client.use_seed_optimize,
|
client.use_seed_optimize,
|
||||||
);
|
);
|
||||||
let balance = rpc.get_token_account_balance(&account).await?;
|
let balance = rpc.get_token_account_balance(&account).await?;
|
||||||
println!("Balance: {:?}", balance);
|
|
||||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||||
|
|
||||||
println!("Selling {} tokens", amount_token);
|
|
||||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
output_token_type: TradeTokenType::SOL,
|
output_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: amount_token,
|
input_token_amount: amount_token,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
with_tip: false,
|
with_tip: false,
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
trade_info.bonding_curve,
|
e.bonding_curve,
|
||||||
trade_info.associated_bonding_curve,
|
e.associated_bonding_curve,
|
||||||
trade_info.mint,
|
e.mint,
|
||||||
trade_info.creator,
|
e.creator,
|
||||||
trade_info.creator_vault,
|
e.creator_vault,
|
||||||
trade_info.virtual_token_reserves,
|
e.virtual_token_reserves,
|
||||||
trade_info.virtual_sol_reserves,
|
e.virtual_sol_reserves,
|
||||||
trade_info.real_token_reserves,
|
e.real_token_reserves,
|
||||||
trade_info.real_sol_reserves,
|
e.real_sol_reserves,
|
||||||
Some(true),
|
Some(true),
|
||||||
trade_info.fee_recipient,
|
e.fee_recipient,
|
||||||
trade_info.token_program,
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
@@ -212,14 +200,11 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
close_mint_token_ata: false,
|
close_mint_token_ata: false,
|
||||||
durable_nonce: None,
|
durable_nonce: None,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell)
|
println!("跟单一次买+卖完成");
|
||||||
// creator_vault can be obtained from the trade event
|
Ok(())
|
||||||
|
|
||||||
// Exit program
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
solana-streamer-sdk = "0.5.0"
|
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
//! PumpFun 狙击示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
|
||||||
|
//!
|
||||||
|
//! 监听创建者首次买入(Create 后同笔/首笔 Buy,is_created_buy == true),
|
||||||
|
//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。
|
||||||
|
|
||||||
|
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||||
|
|
||||||
|
use sol_parser_sdk::grpc::{
|
||||||
|
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||||
|
TransactionFilter, YellowstoneGrpc,
|
||||||
|
};
|
||||||
|
use sol_parser_sdk::DexEvent;
|
||||||
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
||||||
use sol_trade_sdk::common::TradeConfig;
|
use sol_trade_sdk::common::TradeConfig;
|
||||||
use sol_trade_sdk::TradeTokenType;
|
use sol_trade_sdk::TradeTokenType;
|
||||||
@@ -10,108 +22,120 @@ use sol_trade_sdk::{
|
|||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
use solana_sdk::signer::Signer;
|
use solana_sdk::signer::Signer;
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
|
||||||
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
|
|
||||||
use std::sync::{
|
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Atomic flag to ensure the sniper trade is executed only once
|
|
||||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// Main entry point - subscribes to PumpFun events and executes sniper trades on token creation
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Subscribing to ShredStream events...");
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?;
|
println!("PumpFun 狙击示例(sol-parser-sdk gRPC)...");
|
||||||
let callback = create_event_callback();
|
|
||||||
let protocols = vec![Protocol::PumpFun];
|
let config = ClientConfig {
|
||||||
let event_type_filter = EventTypeFilter {
|
enable_metrics: false,
|
||||||
include: vec![EventType::PumpFunBuy, EventType::PumpFunSell, EventType::PumpFunCreateToken],
|
connection_timeout_ms: 10000,
|
||||||
|
request_timeout_ms: 30000,
|
||||||
|
enable_tls: true,
|
||||||
|
order_mode: OrderMode::Unordered,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
println!("Starting to listen for events, press Ctrl+C to stop...");
|
|
||||||
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?;
|
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||||
|
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
|
||||||
|
let grpc = YellowstoneGrpc::new_with_config(
|
||||||
|
grpc_endpoint,
|
||||||
|
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||||
|
config,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let protocols = vec![Protocol::PumpFun];
|
||||||
|
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||||
|
let account_filter = AccountFilter::for_protocols(&protocols);
|
||||||
|
let event_filter = EventTypeFilter::include_only(vec![
|
||||||
|
EventType::PumpFunCreate,
|
||||||
|
EventType::PumpFunBuy,
|
||||||
|
EventType::PumpFunBuyExactSolIn,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let queue = grpc
|
||||||
|
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("订阅已启动,等待创建者首次买入(is_created_buy)后执行狙击(仅一次)...\n");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(event) = queue.pop() {
|
||||||
|
let run = match &event {
|
||||||
|
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||||
|
if e.is_created_buy && !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
Some(e.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(e) = run {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = pumpfun_sniper_trade(e).await {
|
||||||
|
eprintln!("狙击执行错误: {:?}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await?;
|
tokio::signal::ctrl_c().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an event callback function that handles different types of events
|
|
||||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
|
||||||
|event: Box<dyn UnifiedEvent>| {
|
|
||||||
match_event!(event, {
|
|
||||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
|
||||||
// Only process developer token creation events
|
|
||||||
if !e.is_dev_create_token_trade {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Ensure we only execute the trade once using atomic compare-and-swap
|
|
||||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
|
||||||
let event_clone = e.clone();
|
|
||||||
// Spawn a new task to handle the trading operation
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = pumpfun_sniper_trade_with_shreds(event_clone).await {
|
|
||||||
eprintln!("Error in copy trade: {:?}", err);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create SolanaTrade client
|
|
||||||
/// Initializes a new SolanaTrade client with configuration
|
|
||||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||||
println!("🚀 Initializing SolanaTrade client...");
|
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||||
let rpc_url = "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 commitment = CommitmentConfig::confirmed();
|
||||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||||
println!("✅ SolanaTrade client initialized successfully!");
|
|
||||||
Ok(solana_trade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute PumpFun sniper trading strategy based on received token creation event
|
async fn pumpfun_sniper_trade(
|
||||||
/// This function buys tokens immediately after creation and then sells all tokens
|
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||||
async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
) -> AnyResult<()> {
|
||||||
println!("Testing PumpFun trading...");
|
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
let client = create_solana_trade_client().await?;
|
||||||
let mint_pubkey = trade_info.mint;
|
let mint_pubkey = e.mint;
|
||||||
let slippage_basis_points = Some(300);
|
let slippage_basis_points = Some(300u64);
|
||||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||||
|
|
||||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||||
|
|
||||||
// Buy tokens
|
// 创建者首次买入:用 from_dev_trade,max_sol_cost 用事件中的 sol_amount(可酌情加滑点)
|
||||||
println!("Buying tokens from PumpFun...");
|
let buy_sol_amount = 100_000u64;
|
||||||
let buy_sol_amount = 100_000;
|
let max_sol_cost = e.sol_amount.saturating_add(e.sol_amount / 10); // 约 +10% 作为上限
|
||||||
|
|
||||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
input_token_type: TradeTokenType::SOL,
|
input_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: buy_sol_amount,
|
input_token_amount: buy_sol_amount,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade(
|
||||||
trade_info.mint,
|
e.mint,
|
||||||
trade_info.token_amount,
|
e.token_amount,
|
||||||
trade_info.max_sol_cost,
|
max_sol_cost,
|
||||||
trade_info.creator,
|
e.creator,
|
||||||
trade_info.bonding_curve,
|
e.bonding_curve,
|
||||||
trade_info.associated_bonding_curve,
|
e.associated_bonding_curve,
|
||||||
trade_info.creator_vault,
|
e.creator_vault,
|
||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
e.fee_recipient,
|
||||||
trade_info.token_program,
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
@@ -126,26 +150,35 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
|
|||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
// Sell tokens
|
|
||||||
println!("Selling tokens from PumpFun...");
|
|
||||||
|
|
||||||
let rpc = client.infrastructure.rpc.clone();
|
let rpc = client.infrastructure.rpc.clone();
|
||||||
let payer = client.payer.pubkey();
|
let payer = client.payer.pubkey();
|
||||||
let account = get_associated_token_address(&payer, &mint_pubkey);
|
let account = get_associated_token_address(&payer, &mint_pubkey);
|
||||||
let balance = rpc.get_token_account_balance(&account).await?;
|
let balance = rpc.get_token_account_balance(&account).await?;
|
||||||
println!("Balance: {:?}", balance);
|
|
||||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||||
|
|
||||||
println!("Selling {} tokens", amount_token);
|
|
||||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
output_token_type: TradeTokenType::SOL,
|
output_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: amount_token,
|
input_token_amount: amount_token,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
with_tip: false,
|
with_tip: false,
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::immediate_sell(trade_info.creator_vault, trade_info.token_program, true)),
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
|
e.bonding_curve,
|
||||||
|
e.associated_bonding_curve,
|
||||||
|
e.mint,
|
||||||
|
e.creator,
|
||||||
|
e.creator_vault,
|
||||||
|
e.virtual_token_reserves,
|
||||||
|
e.virtual_sol_reserves,
|
||||||
|
e.real_token_reserves,
|
||||||
|
e.real_sol_reserves,
|
||||||
|
Some(true),
|
||||||
|
e.fee_recipient,
|
||||||
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
create_output_token_ata: true,
|
create_output_token_ata: true,
|
||||||
@@ -153,14 +186,11 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
|
|||||||
close_mint_token_ata: false,
|
close_mint_token_ata: false,
|
||||||
durable_nonce: None,
|
durable_nonce: None,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell)
|
println!("狙击一次买+卖完成");
|
||||||
// creator_vault can be obtained from the trade event
|
Ok(())
|
||||||
|
|
||||||
// Exit program after completing the trade
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7143
File diff suppressed because it is too large
Load Diff
+6313
File diff suppressed because it is too large
Load Diff
+2761
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
# sol-trade-sdk v3.4.1
|
||||||
|
|
||||||
|
Rust SDK for Solana DEX trading (Pump.fun, PumpSwap, Raydium, Bonk, Meteora, etc.).
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- **PumpFun & PumpSwap Cashback** (#77): Support for cashback in PumpFun and PumpSwap trading flows. See [Cashback documentation](docs/PUMP_CASHBACK_README.md).
|
||||||
|
- **Events**: `is_cashback_coin` is now passed from events; PumpFun examples use `sol-parser-sdk` only for event parsing.
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **SWQoS**: Reduced submit latency; fixed high latency after ~5 minutes idle.
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **WSOL ATA**: WSOL Associated Token Account creation now runs in background with retry and timeout for more reliable setup.
|
||||||
|
- Silenced unused and deprecated compiler warnings.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- README (EN/中文): Added Cashback section, outline, examples and tables.
|
||||||
|
- Updated crates.io / docs references in README.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cargo
|
||||||
|
|
||||||
|
**From Git (this release):**
|
||||||
|
```toml
|
||||||
|
sol-trade-sdk = { git = "https://github.com/0xfnzero/sol-trade-sdk", tag = "v3.4.1" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**From crates.io** (when published):
|
||||||
|
```toml
|
||||||
|
sol-trade-sdk = "3.4.1"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/0xfnzero/sol-trade-sdk/compare/v3.4.0...v3.4.1
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# sol-trade-sdk v3.5.0
|
||||||
|
|
||||||
|
Rust SDK for Solana DEX trading (Pump.fun, PumpSwap, Raydium, Bonk, Meteora, etc.).
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- **Executor hot path**: Sample `Instant::now()` only when `log_enabled` or `simulate` (total, build, submit, confirm) to reduce cold-path syscalls.
|
||||||
|
- **SWQOS**: `execute_parallel` now takes `&[Arc<SwqosClient>]` to avoid cloning the client list on each swap.
|
||||||
|
- **Constants**: Named constants for instruction/account sizes and HTTP client timeouts; no magic numbers in hot paths.
|
||||||
|
|
||||||
|
### Code Quality
|
||||||
|
|
||||||
|
- **Protocol params**: Single `validate_protocol_params()` used for both buy and sell; removed duplicate match blocks in `lib.rs`.
|
||||||
|
- **Comments**: Bilingual (English + 中文) doc and inline comments across execution, executor, perf (hardware_optimizations, syscall_bypass), and swqos/common.
|
||||||
|
- **Prefetch/safety**: Clearer docs for cache prefetch and `unsafe` usage (valid read-only ref, no concurrent write).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- README (EN/CN): Version references updated to 3.5.0 for path and crates.io usage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cargo
|
||||||
|
|
||||||
|
**From Git (this release):**
|
||||||
|
```toml
|
||||||
|
sol-trade-sdk = { git = "https://github.com/0xfnzero/sol-trade-sdk", tag = "v3.5.0" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**From crates.io:**
|
||||||
|
```toml
|
||||||
|
sol-trade-sdk = "3.5.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/0xfnzero/sol-trade-sdk/compare/v3.4.1...v3.5.0
|
||||||
@@ -60,9 +60,13 @@ pub struct BondingCurveAccount {
|
|||||||
pub creator: Pubkey,
|
pub creator: Pubkey,
|
||||||
/// Whether this is a mayhem mode token (Token2022)
|
/// Whether this is a mayhem mode token (Token2022)
|
||||||
pub is_mayhem_mode: bool,
|
pub is_mayhem_mode: bool,
|
||||||
|
/// Whether this coin has cashback enabled (creator fee redirected to users)
|
||||||
|
pub is_cashback_coin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BondingCurveAccount {
|
impl BondingCurveAccount {
|
||||||
|
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
|
||||||
|
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
|
||||||
pub fn from_dev_trade(
|
pub fn from_dev_trade(
|
||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
mint: &Pubkey,
|
mint: &Pubkey,
|
||||||
@@ -70,6 +74,7 @@ impl BondingCurveAccount {
|
|||||||
dev_sol_amount: u64,
|
dev_sol_amount: u64,
|
||||||
creator: Pubkey,
|
creator: Pubkey,
|
||||||
is_mayhem_mode: bool,
|
is_mayhem_mode: bool,
|
||||||
|
is_cashback_coin: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let account = if bonding_curve != Pubkey::default() {
|
let account = if bonding_curve != Pubkey::default() {
|
||||||
bonding_curve
|
bonding_curve
|
||||||
@@ -87,9 +92,12 @@ impl BondingCurveAccount {
|
|||||||
complete: false,
|
complete: false,
|
||||||
creator: creator,
|
creator: creator,
|
||||||
is_mayhem_mode: is_mayhem_mode,
|
is_mayhem_mode: is_mayhem_mode,
|
||||||
|
is_cashback_coin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
|
||||||
|
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
|
||||||
pub fn from_trade(
|
pub fn from_trade(
|
||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
mint: Pubkey,
|
mint: Pubkey,
|
||||||
@@ -99,6 +107,7 @@ impl BondingCurveAccount {
|
|||||||
real_token_reserves: u64,
|
real_token_reserves: u64,
|
||||||
real_sol_reserves: u64,
|
real_sol_reserves: u64,
|
||||||
is_mayhem_mode: bool,
|
is_mayhem_mode: bool,
|
||||||
|
is_cashback_coin: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let account = if bonding_curve != Pubkey::default() {
|
let account = if bonding_curve != Pubkey::default() {
|
||||||
bonding_curve
|
bonding_curve
|
||||||
@@ -116,6 +125,7 @@ impl BondingCurveAccount {
|
|||||||
complete: false,
|
complete: false,
|
||||||
creator: creator,
|
creator: creator,
|
||||||
is_mayhem_mode: is_mayhem_mode,
|
is_mayhem_mode: is_mayhem_mode,
|
||||||
|
is_cashback_coin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
//! High-performance clock (same design as sol-parser-sdk for consistent grpc_recv_us vs "now").
|
||||||
|
//!
|
||||||
|
//! Uses monotonic clock + base UTC timestamp to avoid frequent syscalls; aligned with sol-parser-sdk
|
||||||
|
//! so event-side grpc_recv_us and SDK-side now_micros() share the same time scale.
|
||||||
|
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
/// High-performance clock: monotonic + base UTC microsecond timestamp.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct HighPerformanceClock {
|
||||||
|
base_instant: Instant,
|
||||||
|
base_timestamp_us: i64,
|
||||||
|
last_calibration: Instant,
|
||||||
|
calibration_interval_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HighPerformanceClock {
|
||||||
|
/// Calibrate every 5 minutes by default.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::new_with_calibration_interval(300)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sample multiple times and use the lowest-latency baseline to reduce init error.
|
||||||
|
pub fn new_with_calibration_interval(calibration_interval_secs: u64) -> Self {
|
||||||
|
let mut best_offset = i64::MAX;
|
||||||
|
let mut best_instant = Instant::now();
|
||||||
|
let mut best_timestamp = chrono::Utc::now().timestamp_micros();
|
||||||
|
|
||||||
|
for _ in 0..3 {
|
||||||
|
let instant_before = Instant::now();
|
||||||
|
let timestamp = chrono::Utc::now().timestamp_micros();
|
||||||
|
let instant_after = Instant::now();
|
||||||
|
let sample_latency = instant_after.duration_since(instant_before).as_nanos() as i64;
|
||||||
|
if sample_latency < best_offset {
|
||||||
|
best_offset = sample_latency;
|
||||||
|
best_instant = instant_before;
|
||||||
|
best_timestamp = timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self {
|
||||||
|
base_instant: best_instant,
|
||||||
|
base_timestamp_us: best_timestamp,
|
||||||
|
last_calibration: best_instant,
|
||||||
|
calibration_interval_secs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn now_micros(&self) -> i64 {
|
||||||
|
let elapsed = self.base_instant.elapsed();
|
||||||
|
self.base_timestamp_us + elapsed.as_micros() as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recalibrate when needed to prevent drift.
|
||||||
|
pub fn now_micros_with_calibration(&mut self) -> i64 {
|
||||||
|
if self.last_calibration.elapsed().as_secs() >= self.calibration_interval_secs {
|
||||||
|
self.recalibrate();
|
||||||
|
}
|
||||||
|
self.now_micros()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recalibrate(&mut self) {
|
||||||
|
let current_monotonic = Instant::now();
|
||||||
|
let current_utc = chrono::Utc::now().timestamp_micros();
|
||||||
|
let expected_utc = self.base_timestamp_us
|
||||||
|
+ current_monotonic.duration_since(self.base_instant).as_micros() as i64;
|
||||||
|
let drift_us = current_utc - expected_utc;
|
||||||
|
if drift_us.abs() > 1000 {
|
||||||
|
self.base_instant = current_monotonic;
|
||||||
|
self.base_timestamp_us = current_utc;
|
||||||
|
}
|
||||||
|
self.last_calibration = current_monotonic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for HighPerformanceClock {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static HIGH_PERF_CLOCK: once_cell::sync::OnceCell<HighPerformanceClock> =
|
||||||
|
once_cell::sync::OnceCell::new();
|
||||||
|
|
||||||
|
/// Current time in microseconds (UTC scale); same as sol-parser-sdk clock::now_micros for comparable grpc_recv_us.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn now_micros() -> i64 {
|
||||||
|
let clock = HIGH_PERF_CLOCK.get_or_init(HighPerformanceClock::new);
|
||||||
|
clock.now_micros()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elapsed microseconds from start_timestamp_us to now.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn elapsed_micros_since(start_timestamp_us: i64) -> i64 {
|
||||||
|
now_micros() - start_timestamp_us
|
||||||
|
}
|
||||||
@@ -153,10 +153,12 @@ pub fn _create_associated_token_account_idempotent_fast(
|
|||||||
pub enum PdaCacheKey {
|
pub enum PdaCacheKey {
|
||||||
PumpFunUserVolume(Pubkey),
|
PumpFunUserVolume(Pubkey),
|
||||||
PumpFunBondingCurve(Pubkey),
|
PumpFunBondingCurve(Pubkey),
|
||||||
|
PumpFunBondingCurveV2(Pubkey),
|
||||||
PumpFunCreatorVault(Pubkey),
|
PumpFunCreatorVault(Pubkey),
|
||||||
BonkPool(Pubkey, Pubkey),
|
BonkPool(Pubkey, Pubkey),
|
||||||
BonkVault(Pubkey, Pubkey),
|
BonkVault(Pubkey, Pubkey),
|
||||||
PumpSwapUserVolume(Pubkey),
|
PumpSwapUserVolume(Pubkey),
|
||||||
|
PumpSwapPoolV2(Pubkey),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global lock-free PDA cache for storing computation results
|
/// Global lock-free PDA cache for storing computation results
|
||||||
|
|||||||
@@ -174,7 +174,9 @@ mod tests {
|
|||||||
let total_elapsed = start.elapsed();
|
let total_elapsed = start.elapsed();
|
||||||
let avg_per_call = total_elapsed.as_nanos() / iterations;
|
let avg_per_call = total_elapsed.as_nanos() / iterations;
|
||||||
|
|
||||||
println!("Average fast_now_nanos() call: {}ns", avg_per_call);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
println!("Average fast_now_nanos() call: {}ns", avg_per_call);
|
||||||
|
}
|
||||||
|
|
||||||
// 快速时间戳应该非常快(< 100ns per call)
|
// 快速时间戳应该非常快(< 100ns per call)
|
||||||
assert!(avg_per_call < 100);
|
assert!(avg_per_call < 100);
|
||||||
@@ -193,6 +195,8 @@ mod tests {
|
|||||||
let total_elapsed = start.elapsed();
|
let total_elapsed = start.elapsed();
|
||||||
let avg_per_call = total_elapsed.as_nanos() / iterations;
|
let avg_per_call = total_elapsed.as_nanos() / iterations;
|
||||||
|
|
||||||
println!("Average Instant::now() call: {}ns", avg_per_call);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
println!("Average Instant::now() call: {}ns", avg_per_call);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ impl GasFeeStrategy {
|
|||||||
pub fn update_buy_tip(&self, buy_tip: f64) {
|
pub fn update_buy_tip(&self, buy_tip: f64) {
|
||||||
self.strategies.rcu(|current_map| {
|
self.strategies.rcu(|current_map| {
|
||||||
let mut new_map = (**current_map).clone();
|
let mut new_map = (**current_map).clone();
|
||||||
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() {
|
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||||
if *trade_type == TradeType::Buy {
|
if *trade_type == TradeType::Buy {
|
||||||
value.tip = buy_tip;
|
value.tip = buy_tip;
|
||||||
}
|
}
|
||||||
@@ -314,7 +314,7 @@ impl GasFeeStrategy {
|
|||||||
pub fn update_sell_tip(&self, sell_tip: f64) {
|
pub fn update_sell_tip(&self, sell_tip: f64) {
|
||||||
self.strategies.rcu(|current_map| {
|
self.strategies.rcu(|current_map| {
|
||||||
let mut new_map = (**current_map).clone();
|
let mut new_map = (**current_map).clone();
|
||||||
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() {
|
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||||
if *trade_type == TradeType::Sell {
|
if *trade_type == TradeType::Sell {
|
||||||
value.tip = sell_tip;
|
value.tip = sell_tip;
|
||||||
}
|
}
|
||||||
@@ -328,7 +328,7 @@ impl GasFeeStrategy {
|
|||||||
pub fn update_buy_cu_price(&self, buy_cu_price: u64) {
|
pub fn update_buy_cu_price(&self, buy_cu_price: u64) {
|
||||||
self.strategies.rcu(|current_map| {
|
self.strategies.rcu(|current_map| {
|
||||||
let mut new_map = (**current_map).clone();
|
let mut new_map = (**current_map).clone();
|
||||||
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() {
|
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||||
if *trade_type == TradeType::Buy {
|
if *trade_type == TradeType::Buy {
|
||||||
value.cu_price = buy_cu_price;
|
value.cu_price = buy_cu_price;
|
||||||
}
|
}
|
||||||
@@ -342,7 +342,7 @@ impl GasFeeStrategy {
|
|||||||
pub fn update_sell_cu_price(&self, sell_cu_price: u64) {
|
pub fn update_sell_cu_price(&self, sell_cu_price: u64) {
|
||||||
self.strategies.rcu(|current_map| {
|
self.strategies.rcu(|current_map| {
|
||||||
let mut new_map = (**current_map).clone();
|
let mut new_map = (**current_map).clone();
|
||||||
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() {
|
for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
|
||||||
if *trade_type == TradeType::Sell {
|
if *trade_type == TradeType::Sell {
|
||||||
value.cu_price = sell_cu_price;
|
value.cu_price = sell_cu_price;
|
||||||
}
|
}
|
||||||
@@ -354,6 +354,9 @@ impl GasFeeStrategy {
|
|||||||
/// 打印所有策略。
|
/// 打印所有策略。
|
||||||
/// Print all strategies
|
/// Print all strategies
|
||||||
pub fn print_all_strategies(&self) {
|
pub fn print_all_strategies(&self) {
|
||||||
|
if !crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for strategy in self.get_strategies(TradeType::Buy) {
|
for strategy in self.get_strategies(TradeType::Buy) {
|
||||||
println!("[buy] - {:?}", strategy);
|
println!("[buy] - {:?}", strategy);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,8 @@
|
|||||||
|
pub mod address_lookup;
|
||||||
pub mod bonding_curve;
|
pub mod bonding_curve;
|
||||||
|
pub mod clock;
|
||||||
pub mod fast_fn;
|
pub mod fast_fn;
|
||||||
|
pub mod sdk_log;
|
||||||
pub mod fast_timing;
|
pub mod fast_timing;
|
||||||
pub mod gas_fee_strategy;
|
pub mod gas_fee_strategy;
|
||||||
pub mod global;
|
pub mod global;
|
||||||
@@ -10,7 +13,6 @@ pub mod spl_token;
|
|||||||
pub mod spl_token_2022;
|
pub mod spl_token_2022;
|
||||||
pub mod subscription_handle;
|
pub mod subscription_handle;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod address_lookup;
|
|
||||||
|
|
||||||
pub use gas_fee_strategy::*;
|
pub use gas_fee_strategy::*;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
//! sol-trade-sdk global log switch
|
||||||
|
//!
|
||||||
|
//! 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::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
static SDK_LOG_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||||
|
|
||||||
|
/// Whether SDK logging is enabled (set from TradeConfig.log_enabled in TradingClient::new).
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn sdk_log_enabled() -> bool {
|
||||||
|
SDK_LOG_ENABLED.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the SDK global log switch (only called from TradingClient::new).
|
||||||
|
pub fn set_sdk_log_enabled(enabled: bool) {
|
||||||
|
SDK_LOG_ENABLED.store(enabled, Ordering::Relaxed);
|
||||||
|
}
|
||||||
+12
-4
@@ -72,6 +72,10 @@ pub struct TradeConfig {
|
|||||||
pub create_wsol_ata_on_startup: bool,
|
pub create_wsol_ata_on_startup: bool,
|
||||||
/// Whether to use seed optimization for all ATA operations (default: true)
|
/// Whether to use seed optimization for all ATA operations (default: true)
|
||||||
pub use_seed_optimize: bool,
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TradeConfig {
|
impl TradeConfig {
|
||||||
@@ -80,14 +84,18 @@ impl TradeConfig {
|
|||||||
swqos_configs: Vec<SwqosConfig>,
|
swqos_configs: Vec<SwqosConfig>,
|
||||||
commitment: CommitmentConfig,
|
commitment: CommitmentConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
println!("🔧 TradeConfig create_wsol_ata_on_startup default value: true");
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!("🔧 TradeConfig use_seed_optimize default value: true");
|
println!("🔧 TradeConfig create_wsol_ata_on_startup default: true");
|
||||||
|
println!("🔧 TradeConfig use_seed_optimize default: true");
|
||||||
|
}
|
||||||
Self {
|
Self {
|
||||||
rpc_url,
|
rpc_url,
|
||||||
swqos_configs,
|
swqos_configs,
|
||||||
commitment,
|
commitment,
|
||||||
create_wsol_ata_on_startup: true, // 默认:启动时检查并创建
|
create_wsol_ata_on_startup: true, // default: check and create on startup
|
||||||
use_seed_optimize: true, // 默认:使用seed优化
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+20
-16
@@ -227,26 +227,30 @@ pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
|
|||||||
"http://ny.flashblock.trade",
|
"http://ny.flashblock.trade",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
|
||||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
||||||
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
|
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://amsterdam.solana.blockrazor.xyz:443/sendTransaction",
|
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://tokyo.solana.blockrazor.xyz:443/sendTransaction",
|
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
|
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://newyork.solana.blockrazor.xyz:443/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/sendTransaction",
|
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
|
||||||
|
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
|
||||||
|
|
||||||
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
|
||||||
"http://ny.gateway.astralane.io/iris",
|
"http://ny.gateway.astralane.io/irisb",
|
||||||
"http://fr.gateway.astralane.io/iris",
|
"http://fr.gateway.astralane.io/irisb",
|
||||||
"http://ams.gateway.astralane.io/iris",
|
"http://ams.gateway.astralane.io/irisb",
|
||||||
"http://ny.gateway.astralane.io/iris",
|
"http://ny.gateway.astralane.io/irisb",
|
||||||
"http://jp.gateway.astralane.io/iris",
|
"http://jp.gateway.astralane.io/irisb",
|
||||||
"http://ny.gateway.astralane.io/iris",
|
"http://ny.gateway.astralane.io/irisb",
|
||||||
"http://lax.gateway.astralane.io/iris",
|
"http://lax.gateway.astralane.io/irisb",
|
||||||
"http://lim.gateway.astralane.io/iris",
|
"http://lim.gateway.astralane.io/irisb",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
instruction::utils::pumpfun::{
|
instruction::utils::pumpfun::{
|
||||||
accounts, get_bonding_curve_pda, get_creator, get_user_volume_accumulator_pda,
|
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
|
||||||
global_constants::{self}, BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
get_user_volume_accumulator_pda, global_constants::{self}, BUY_DISCRIMINATOR,
|
||||||
|
BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
||||||
},
|
},
|
||||||
utils::calc::{
|
utils::calc::{
|
||||||
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
|
common::{calculate_with_slippage_buy, calculate_with_slippage_sell},
|
||||||
@@ -143,7 +144,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
global_constants::FEE_RECIPIENT_META
|
global_constants::FEE_RECIPIENT_META
|
||||||
};
|
};
|
||||||
|
|
||||||
let accounts: [AccountMeta; 16] = [
|
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).unwrap();
|
||||||
|
let mut accounts: Vec<AccountMeta> = vec![
|
||||||
global_constants::GLOBAL_ACCOUNT_META,
|
global_constants::GLOBAL_ACCOUNT_META,
|
||||||
fee_recipient_meta,
|
fee_recipient_meta,
|
||||||
AccountMeta::new_readonly(params.output_mint, false),
|
AccountMeta::new_readonly(params.output_mint, false),
|
||||||
@@ -161,11 +163,12 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
accounts::FEE_CONFIG_META,
|
accounts::FEE_CONFIG_META,
|
||||||
accounts::FEE_PROGRAM_META,
|
accounts::FEE_PROGRAM_META,
|
||||||
];
|
];
|
||||||
|
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // bonding_curve_v2 (readonly) at end
|
||||||
|
|
||||||
instructions.push(Instruction::new_with_bytes(
|
instructions.push(Instruction::new_with_bytes(
|
||||||
accounts::PUMPFUN,
|
accounts::PUMPFUN,
|
||||||
&buy_data,
|
&buy_data,
|
||||||
accounts.to_vec(),
|
accounts,
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(instructions)
|
Ok(instructions)
|
||||||
@@ -263,7 +266,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
global_constants::FEE_RECIPIENT_META
|
global_constants::FEE_RECIPIENT_META
|
||||||
};
|
};
|
||||||
|
|
||||||
let accounts: [AccountMeta; 14] = [
|
let mut accounts: Vec<AccountMeta> = vec![
|
||||||
global_constants::GLOBAL_ACCOUNT_META,
|
global_constants::GLOBAL_ACCOUNT_META,
|
||||||
fee_recipient_meta,
|
fee_recipient_meta,
|
||||||
AccountMeta::new_readonly(params.input_mint, false),
|
AccountMeta::new_readonly(params.input_mint, false),
|
||||||
@@ -280,10 +283,20 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
accounts::FEE_PROGRAM_META,
|
accounts::FEE_PROGRAM_META,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 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(¶ms.payer.pubkey()).unwrap();
|
||||||
|
accounts.push(AccountMeta::new(user_volume_accumulator, false));
|
||||||
|
}
|
||||||
|
// Program upgrade: bonding_curve_v2 (readonly) at end of account list
|
||||||
|
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.input_mint).unwrap();
|
||||||
|
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||||
|
|
||||||
instructions.push(Instruction::new_with_bytes(
|
instructions.push(Instruction::new_with_bytes(
|
||||||
accounts::PUMPFUN,
|
accounts::PUMPFUN,
|
||||||
&sell_data,
|
&sell_data,
|
||||||
accounts.to_vec(),
|
accounts,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Optional: Close token account
|
// Optional: Close token account
|
||||||
@@ -302,3 +315,21 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
Ok(instructions)
|
Ok(instructions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Claim cashback for Bonding Curve (Pump program). Transfers native lamports from UserVolumeAccumulator to user.
|
||||||
|
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(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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||||
instruction::utils::pumpswap::{
|
instruction::utils::pumpswap::{
|
||||||
accounts, fee_recipient_ata, get_user_volume_accumulator_pda, BUY_DISCRIMINATOR,
|
accounts, fee_recipient_ata, get_pool_v2_pda, get_user_volume_accumulator_pda,
|
||||||
|
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR,
|
||||||
BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||||
},
|
},
|
||||||
trading::{
|
trading::{
|
||||||
@@ -183,6 +184,17 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
}
|
}
|
||||||
accounts.push(accounts::FEE_CONFIG_META);
|
accounts.push(accounts::FEE_CONFIG_META);
|
||||||
accounts.push(accounts::FEE_PROGRAM_META);
|
accounts.push(accounts::FEE_PROGRAM_META);
|
||||||
|
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator (after named accounts per IDL)
|
||||||
|
if protocol_params.is_cashback_coin {
|
||||||
|
if let Some(wsol_ata) = get_user_volume_accumulator_wsol_ata(¶ms.payer.pubkey()) {
|
||||||
|
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Program upgrade: pool_v2 (readonly) at end of account list
|
||||||
|
accounts.push(AccountMeta::new_readonly(
|
||||||
|
get_pool_v2_pda(&base_mint).unwrap(),
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
|
||||||
// Create instruction data
|
// Create instruction data
|
||||||
let mut data = [0u8; 24];
|
let mut data = [0u8; 24];
|
||||||
@@ -373,9 +385,23 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
false,
|
false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
accounts.push(accounts::FEE_CONFIG_META);
|
accounts.push(accounts::FEE_CONFIG_META);
|
||||||
accounts.push(accounts::FEE_PROGRAM_META);
|
accounts.push(accounts::FEE_PROGRAM_META);
|
||||||
|
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator, remaining_accounts[1] = UserVolumeAccumulator PDA
|
||||||
|
if protocol_params.is_cashback_coin {
|
||||||
|
if let (Some(wsol_ata), Some(accumulator)) = (
|
||||||
|
get_user_volume_accumulator_wsol_ata(¶ms.payer.pubkey()),
|
||||||
|
get_user_volume_accumulator_pda(¶ms.payer.pubkey()),
|
||||||
|
) {
|
||||||
|
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||||
|
accounts.push(AccountMeta::new(accumulator, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Program upgrade: pool_v2 (readonly) at end of account list
|
||||||
|
accounts.push(AccountMeta::new_readonly(
|
||||||
|
get_pool_v2_pda(&base_mint).unwrap(),
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
|
||||||
// Create instruction data
|
// Create instruction data
|
||||||
let mut data = [0u8; 24];
|
let mut data = [0u8; 24];
|
||||||
@@ -420,3 +446,38 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
Ok(instructions)
|
Ok(instructions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Claim cashback for PumpSwap (AMM). Transfers WSOL from UserVolumeAccumulator's WSOL ATA to user's WSOL ATA.
|
||||||
|
/// Caller should ensure user's WSOL ATA exists (e.g. create idempotent ATA instruction) before this instruction.
|
||||||
|
pub fn claim_cashback_pumpswap_instruction(
|
||||||
|
payer: &Pubkey,
|
||||||
|
quote_mint: Pubkey,
|
||||||
|
quote_token_program: Pubkey,
|
||||||
|
) -> Option<solana_sdk::instruction::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 user_volume_accumulator_wsol_ata = get_user_volume_accumulator_wsol_ata(payer)?;
|
||||||
|
let user_wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||||
|
payer,
|
||||||
|
"e_mint,
|
||||||
|
"e_token_program,
|
||||||
|
);
|
||||||
|
// 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_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
|
||||||
|
crate::constants::SYSTEM_PROGRAM_META,
|
||||||
|
accounts::EVENT_AUTHORITY_META,
|
||||||
|
accounts::AMM_PROGRAM_META,
|
||||||
|
];
|
||||||
|
Some(solana_sdk::instruction::Instruction::new_with_bytes(
|
||||||
|
accounts::AMM_PROGRAM,
|
||||||
|
&CLAIM_CASHBACK_DISCRIMINATOR,
|
||||||
|
accounts,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ use std::sync::Arc;
|
|||||||
pub mod seeds {
|
pub mod seeds {
|
||||||
/// Seed for bonding curve PDAs
|
/// Seed for bonding curve PDAs
|
||||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||||
|
/// Seed for bonding curve v2 PDA (required by program upgrade, readonly at end of account list)
|
||||||
|
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
|
||||||
|
|
||||||
/// Seed for creator vault PDAs
|
/// Seed for creator vault PDAs
|
||||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
||||||
@@ -178,6 +180,20 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Bonding curve v2 PDA (seeds: ["bonding-curve-v2", mint]). Required at end of buy/sell/buy_exact_sol_in accounts.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||||
|
crate::common::fast_fn::get_cached_pda(
|
||||||
|
crate::common::fast_fn::PdaCacheKey::PumpFunBondingCurveV2(*mint),
|
||||||
|
|| {
|
||||||
|
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_V2_SEED, mint.as_ref()];
|
||||||
|
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||||
|
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||||
|
pda.map(|pubkey| pubkey.0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
|
pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
|
||||||
if creator_vault_pda.eq(&Pubkey::default()) {
|
if creator_vault_pda.eq(&Pubkey::default()) {
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ pub mod seeds {
|
|||||||
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
||||||
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
||||||
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
||||||
|
|
||||||
|
/// Seed for pool v2 PDA (required by program upgrade, readonly at end of account list)
|
||||||
|
pub const POOL_V2_SEED: &[u8] = b"pool-v2";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constants related to program accounts and authorities
|
/// Constants related to program accounts and authorities
|
||||||
@@ -139,6 +142,16 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
|||||||
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
|
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
|
||||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||||
|
|
||||||
|
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||||
|
let (pda, _) = Pubkey::find_program_address(
|
||||||
|
&[seeds::POOL_V2_SEED, base_mint.as_ref()],
|
||||||
|
&accounts::AMM_PROGRAM,
|
||||||
|
);
|
||||||
|
Some(pda)
|
||||||
|
}
|
||||||
|
|
||||||
// Find a pool for a specific mint
|
// Find a pool for a specific mint
|
||||||
pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<Pubkey, anyhow::Error> {
|
pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<Pubkey, anyhow::Error> {
|
||||||
let (pool_address, _) = find_by_mint(rpc, mint).await?;
|
let (pool_address, _) = find_by_mint(rpc, mint).await?;
|
||||||
@@ -185,6 +198,16 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// WSOL ATA of UserVolumeAccumulator for Pump AMM (used for cashback remaining accounts).
|
||||||
|
pub fn get_user_volume_accumulator_wsol_ata(user: &Pubkey) -> Option<Pubkey> {
|
||||||
|
let accumulator = get_user_volume_accumulator_pda(user)?;
|
||||||
|
Some(crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||||
|
&accumulator,
|
||||||
|
&crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||||
|
&crate::constants::TOKEN_PROGRAM,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_global_volume_accumulator_pda() -> Option<Pubkey> {
|
pub fn get_global_volume_accumulator_pda() -> Option<Pubkey> {
|
||||||
let seeds: &[&[u8]; 1] = &[&seeds::GLOBAL_VOLUME_ACCUMULATOR_SEED];
|
let seeds: &[&[u8]; 1] = &[&seeds::GLOBAL_VOLUME_ACCUMULATOR_SEED];
|
||||||
let program_id: &Pubkey = &&accounts::AMM_PROGRAM;
|
let program_id: &Pubkey = &&accounts::AMM_PROGRAM;
|
||||||
@@ -227,6 +250,7 @@ pub async fn find_by_base_mint(
|
|||||||
sort_results: None,
|
sort_results: None,
|
||||||
};
|
};
|
||||||
let program_id = accounts::AMM_PROGRAM;
|
let program_id = accounts::AMM_PROGRAM;
|
||||||
|
#[allow(deprecated)]
|
||||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||||
if accounts.is_empty() {
|
if accounts.is_empty() {
|
||||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||||
@@ -277,6 +301,7 @@ pub async fn find_by_quote_mint(
|
|||||||
sort_results: None,
|
sort_results: None,
|
||||||
};
|
};
|
||||||
let program_id = accounts::AMM_PROGRAM;
|
let program_id = accounts::AMM_PROGRAM;
|
||||||
|
#[allow(deprecated)]
|
||||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||||
if accounts.is_empty() {
|
if accounts.is_empty() {
|
||||||
return Err(anyhow!("No pool found for mint {}", quote_mint));
|
return Err(anyhow!("No pool found for mint {}", quote_mint));
|
||||||
|
|||||||
@@ -15,9 +15,11 @@ pub struct Pool {
|
|||||||
pub lp_supply: u64,
|
pub lp_supply: u64,
|
||||||
pub coin_creator: Pubkey,
|
pub coin_creator: Pubkey,
|
||||||
pub is_mayhem_mode: bool,
|
pub is_mayhem_mode: bool,
|
||||||
|
/// Whether this pool's coin has cashback enabled
|
||||||
|
pub is_cashback_coin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1;
|
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||||
|
|
||||||
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||||
if data.len() < POOL_SIZE {
|
if data.len() < POOL_SIZE {
|
||||||
|
|||||||
+214
-74
@@ -6,8 +6,10 @@ pub mod swqos;
|
|||||||
pub mod trading;
|
pub mod trading;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
use crate::common::nonce_cache::DurableNonceInfo;
|
use crate::common::nonce_cache::DurableNonceInfo;
|
||||||
|
use crate::common::sdk_log;
|
||||||
use crate::common::GasFeeStrategy;
|
use crate::common::GasFeeStrategy;
|
||||||
use crate::common::{TradeConfig, InfrastructureConfig};
|
use crate::common::{TradeConfig, InfrastructureConfig};
|
||||||
|
#[cfg(feature = "perf-trace")]
|
||||||
use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
|
use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
|
||||||
use crate::constants::SOL_TOKEN_ACCOUNT;
|
use crate::constants::SOL_TOKEN_ACCOUNT;
|
||||||
use crate::constants::USD1_TOKEN_ACCOUNT;
|
use crate::constants::USD1_TOKEN_ACCOUNT;
|
||||||
@@ -36,6 +38,21 @@ use solana_sdk::message::AddressLookupTableAccount;
|
|||||||
use solana_sdk::signer::Signer;
|
use solana_sdk::signer::Signer;
|
||||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature};
|
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
/// Single place to validate that protocol params match the given DEX type (avoids duplicate match in buy/sell).
|
||||||
|
#[inline(always)]
|
||||||
|
fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool {
|
||||||
|
match dex_type {
|
||||||
|
DexType::PumpFun => params.as_any().downcast_ref::<PumpFunParams>().is_some(),
|
||||||
|
DexType::PumpSwap => params.as_any().downcast_ref::<PumpSwapParams>().is_some(),
|
||||||
|
DexType::Bonk => params.as_any().downcast_ref::<BonkParams>().is_some(),
|
||||||
|
DexType::RaydiumCpmm => params.as_any().downcast_ref::<RaydiumCpmmParams>().is_some(),
|
||||||
|
DexType::RaydiumAmmV4 => params.as_any().downcast_ref::<RaydiumAmmV4Params>().is_some(),
|
||||||
|
DexType::MeteoraDammV2 => params.as_any().downcast_ref::<MeteoraDammV2Params>().is_some(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Type of the token to buy
|
/// Type of the token to buy
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
@@ -89,7 +106,9 @@ impl TradingInfrastructure {
|
|||||||
for swqos in &config.swqos_configs {
|
for swqos in &config.swqos_configs {
|
||||||
// Check blacklist, skip disabled providers
|
// Check blacklist, skip disabled providers
|
||||||
if swqos.is_blacklisted() {
|
if swqos.is_blacklisted() {
|
||||||
eprintln!("\u{26a0}\u{fe0f} SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match SwqosConfig::get_swqos_client(
|
match SwqosConfig::get_swqos_client(
|
||||||
@@ -98,10 +117,15 @@ impl TradingInfrastructure {
|
|||||||
swqos.clone(),
|
swqos.clone(),
|
||||||
).await {
|
).await {
|
||||||
Ok(swqos_client) => swqos_clients.push(swqos_client),
|
Ok(swqos_client) => swqos_clients.push(swqos_client),
|
||||||
Err(err) => eprintln!(
|
Err(err) => {
|
||||||
"failed to create {:?} swqos client: {err}. Excluding from swqos list",
|
if sdk_log::sdk_log_enabled() {
|
||||||
swqos.swqos_type()
|
warn!(
|
||||||
),
|
target: "sol_trade_sdk",
|
||||||
|
"failed to create {:?} swqos client: {err}. Excluding from swqos list",
|
||||||
|
swqos.swqos_type()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +153,10 @@ pub struct TradingClient {
|
|||||||
/// Whether to use seed optimization for all ATA operations (default: true)
|
/// Whether to use seed optimization for all ATA operations (default: true)
|
||||||
/// Applies to all token account creations across buy and sell operations
|
/// Applies to all token account creations across buy and sell operations
|
||||||
pub use_seed_optimize: bool,
|
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,
|
||||||
|
/// Whether to output all SDK logs (from TradeConfig.log_enabled).
|
||||||
|
pub log_enabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
|
static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
|
||||||
@@ -143,6 +171,8 @@ impl Clone for TradingClient {
|
|||||||
infrastructure: self.infrastructure.clone(),
|
infrastructure: self.infrastructure.clone(),
|
||||||
middleware_manager: self.middleware_manager.clone(),
|
middleware_manager: self.middleware_manager.clone(),
|
||||||
use_seed_optimize: self.use_seed_optimize,
|
use_seed_optimize: self.use_seed_optimize,
|
||||||
|
use_core_affinity: self.use_core_affinity,
|
||||||
|
log_enabled: self.log_enabled,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,6 +222,8 @@ pub struct TradeBuyParams {
|
|||||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||||
pub use_exact_sol_amount: Option<bool>,
|
pub use_exact_sol_amount: Option<bool>,
|
||||||
|
/// 可选:事件收到时间(微秒,与 sol-parser-sdk 的 metadata.grpc_recv_us / clock::now_micros 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点,打印起点→提交耗时。
|
||||||
|
pub grpc_recv_us: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parameters for executing sell orders across different DEX protocols
|
/// Parameters for executing sell orders across different DEX protocols
|
||||||
@@ -236,6 +268,8 @@ pub struct TradeSellParams {
|
|||||||
pub gas_fee_strategy: GasFeeStrategy,
|
pub gas_fee_strategy: GasFeeStrategy,
|
||||||
/// Whether to simulate the transaction instead of executing it
|
/// Whether to simulate the transaction instead of executing it
|
||||||
pub simulate: bool,
|
pub simulate: bool,
|
||||||
|
/// 可选:事件收到时间(微秒,与 sol-parser-sdk clock 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点。
|
||||||
|
pub grpc_recv_us: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TradingClient {
|
impl TradingClient {
|
||||||
@@ -265,6 +299,8 @@ impl TradingClient {
|
|||||||
infrastructure,
|
infrastructure,
|
||||||
middleware_manager: None,
|
middleware_manager: None,
|
||||||
use_seed_optimize,
|
use_seed_optimize,
|
||||||
|
use_core_affinity: true,
|
||||||
|
log_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +322,15 @@ impl TradingClient {
|
|||||||
crate::common::fast_fn::fast_init(&payer.pubkey());
|
crate::common::fast_fn::fast_init(&payer.pubkey());
|
||||||
|
|
||||||
if create_wsol_ata {
|
if create_wsol_ata {
|
||||||
Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await;
|
// 在后台异步创建 WSOL ATA,不阻塞启动
|
||||||
|
let payer_clone = payer.clone();
|
||||||
|
let rpc_clone = infrastructure.rpc.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
Self::ensure_wsol_ata(&payer_clone, &rpc_clone).await;
|
||||||
|
});
|
||||||
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA creation started in background, does not block bot startup");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -294,6 +338,8 @@ impl TradingClient {
|
|||||||
infrastructure,
|
infrastructure,
|
||||||
middleware_manager: None,
|
middleware_manager: None,
|
||||||
use_seed_optimize,
|
use_seed_optimize,
|
||||||
|
use_core_affinity: true,
|
||||||
|
log_enabled: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,46 +354,114 @@ impl TradingClient {
|
|||||||
|
|
||||||
match rpc.get_account(&wsol_ata).await {
|
match rpc.get_account(&wsol_ata).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
println!("✅ WSOL ATA已存在: {}", wsol_ata);
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "✅ WSOL ATA already exists: {}", wsol_ata);
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
println!("🔨 创建WSOL ATA: {}", wsol_ata);
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "🔨 Creating WSOL ATA: {}", wsol_ata);
|
||||||
|
}
|
||||||
let create_ata_ixs =
|
let create_ata_ixs =
|
||||||
crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey());
|
crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey());
|
||||||
|
|
||||||
if !create_ata_ixs.is_empty() {
|
if !create_ata_ixs.is_empty() {
|
||||||
use solana_sdk::transaction::Transaction;
|
use solana_sdk::transaction::Transaction;
|
||||||
let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
|
|
||||||
let tx = Transaction::new_signed_with_payer(
|
|
||||||
&create_ata_ixs,
|
|
||||||
Some(&payer.pubkey()),
|
|
||||||
&[payer.as_ref()],
|
|
||||||
recent_blockhash,
|
|
||||||
);
|
|
||||||
|
|
||||||
match rpc.send_and_confirm_transaction(&tx).await {
|
// 重试逻辑:最多尝试3次,每次超时10秒
|
||||||
Ok(signature) => {
|
const MAX_RETRIES: usize = 3;
|
||||||
println!("✅ WSOL ATA创建成功: {}", signature);
|
const TIMEOUT_SECS: u64 = 10;
|
||||||
|
let mut last_error = None;
|
||||||
|
|
||||||
|
for attempt in 1..=MAX_RETRIES {
|
||||||
|
if attempt > 1 {
|
||||||
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "🔄 Retrying WSOL ATA creation (attempt {}/{})...", attempt, MAX_RETRIES);
|
||||||
|
}
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
match rpc.get_account(&wsol_ata).await {
|
let recent_blockhash = match rpc.get_latest_blockhash().await {
|
||||||
Ok(_) => {
|
Ok(hash) => hash,
|
||||||
println!(
|
Err(e) => {
|
||||||
"✅ WSOL ATA已存在(交易失败但账户存在): {}",
|
if sdk_log::sdk_log_enabled() {
|
||||||
wsol_ata
|
warn!(target: "sol_trade_sdk", "⚠️ Failed to get latest blockhash: {}", e);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(_) => {
|
last_error = Some(format!("Failed to get blockhash: {}", e));
|
||||||
panic!(
|
continue;
|
||||||
"❌ WSOL ATA创建失败且账户不存在: {}. 错误: {}",
|
}
|
||||||
wsol_ata, e
|
};
|
||||||
);
|
|
||||||
|
let tx = Transaction::new_signed_with_payer(
|
||||||
|
&create_ata_ixs,
|
||||||
|
Some(&payer.pubkey()),
|
||||||
|
&[payer.as_ref()],
|
||||||
|
recent_blockhash,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 使用超时包装 send_and_confirm_transaction
|
||||||
|
let send_result = tokio::time::timeout(
|
||||||
|
tokio::time::Duration::from_secs(TIMEOUT_SECS),
|
||||||
|
rpc.send_and_confirm_transaction(&tx)
|
||||||
|
).await;
|
||||||
|
|
||||||
|
match send_result {
|
||||||
|
Ok(Ok(signature)) => {
|
||||||
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "✅ WSOL ATA created successfully: {}", signature);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Ok(Err(e)) => {
|
||||||
|
last_error = Some(format!("{}", e));
|
||||||
|
|
||||||
|
// 检查账户是否实际已存在
|
||||||
|
if let Ok(_) = rpc.get_account(&wsol_ata).await {
|
||||||
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "✅ WSOL ATA already exists (tx failed but account exists): {}", wsol_ata);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if attempt < MAX_RETRIES && sdk_log::sdk_log_enabled() {
|
||||||
|
warn!(target: "sol_trade_sdk", "⚠️ Attempt {} failed: {}", attempt, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
last_error = Some(format!("Transaction confirmation timeout ({}s)", TIMEOUT_SECS));
|
||||||
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
warn!(target: "sol_trade_sdk", "⚠️ Attempt {} timed out", attempt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有重试都失败了
|
||||||
|
if let Some(err) = last_error {
|
||||||
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
error!(target: "sol_trade_sdk", "❌ WSOL ATA creation failed after {} retries: {}", MAX_RETRIES, wsol_ata);
|
||||||
|
error!(target: "sol_trade_sdk", " Error: {}", err);
|
||||||
|
error!(target: "sol_trade_sdk", " 💡 Possible causes:");
|
||||||
|
error!(target: "sol_trade_sdk", " 1. Insufficient SOL balance (need ~0.002 SOL for rent exemption)");
|
||||||
|
error!(target: "sol_trade_sdk", " 2. RPC timeout or network congestion");
|
||||||
|
error!(target: "sol_trade_sdk", " 3. Insufficient transaction fee");
|
||||||
|
error!(target: "sol_trade_sdk", " 🔧 Solutions:");
|
||||||
|
error!(target: "sol_trade_sdk", " 1. Fund wallet with at least 0.1 SOL");
|
||||||
|
error!(target: "sol_trade_sdk", " 2. Retry after a few seconds");
|
||||||
|
error!(target: "sol_trade_sdk", " 3. Check RPC connection");
|
||||||
|
error!(target: "sol_trade_sdk", " ⚠️ Process will exit in 5 seconds, restart after fixing the above");
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
panic!(
|
||||||
|
"❌ WSOL ATA creation failed and account does not exist: {}. Error: {}",
|
||||||
|
wsol_ata, err
|
||||||
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
println!("ℹ️ WSOL ATA已存在(无需创建)");
|
if sdk_log::sdk_log_enabled() {
|
||||||
|
info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA already exists (no need to create)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -366,6 +480,10 @@ impl TradingClient {
|
|||||||
/// Returns a configured `SolTradingSDK` instance ready for trading operations
|
/// Returns a configured `SolTradingSDK` instance ready for trading operations
|
||||||
#[inline]
|
#[inline]
|
||||||
pub async fn new(payer: Arc<Keypair>, trade_config: TradeConfig) -> Self {
|
pub async fn new(payer: Arc<Keypair>, trade_config: TradeConfig) -> Self {
|
||||||
|
// 设置 SDK 全局日志开关,后续所有 SDK 内日志(SWQOS/WSOL/耗时等)均受此控制
|
||||||
|
sdk_log::set_sdk_log_enabled(trade_config.log_enabled);
|
||||||
|
// 预热高性能时钟,避免首笔交易时触发 3 次 Utc::now() 校准
|
||||||
|
let _ = crate::common::clock::now_micros();
|
||||||
// Create infrastructure from trade config
|
// Create infrastructure from trade config
|
||||||
let infra_config = InfrastructureConfig::from_trade_config(&trade_config);
|
let infra_config = InfrastructureConfig::from_trade_config(&trade_config);
|
||||||
let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
|
let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
|
||||||
@@ -383,6 +501,8 @@ impl TradingClient {
|
|||||||
infrastructure,
|
infrastructure,
|
||||||
middleware_manager: None,
|
middleware_manager: None,
|
||||||
use_seed_optimize: trade_config.use_seed_optimize,
|
use_seed_optimize: trade_config.use_seed_optimize,
|
||||||
|
use_core_affinity: trade_config.use_core_affinity,
|
||||||
|
log_enabled: trade_config.log_enabled,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut current = INSTANCE.lock();
|
let mut current = INSTANCE.lock();
|
||||||
@@ -465,8 +585,9 @@ impl TradingClient {
|
|||||||
params: TradeBuyParams,
|
params: TradeBuyParams,
|
||||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||||
#[cfg(feature = "perf-trace")]
|
#[cfg(feature = "perf-trace")]
|
||||||
if params.slippage_basis_points.is_none() {
|
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||||
log::debug!(
|
debug!(
|
||||||
|
target: "sol_trade_sdk",
|
||||||
"slippage_basis_points is none, use default slippage basis points: {}",
|
"slippage_basis_points is none, use default slippage basis points: {}",
|
||||||
DEFAULT_SLIPPAGE
|
DEFAULT_SLIPPAGE
|
||||||
);
|
);
|
||||||
@@ -513,28 +634,13 @@ impl TradingClient {
|
|||||||
fixed_output_amount: params.fixed_output_token_amount,
|
fixed_output_amount: params.fixed_output_token_amount,
|
||||||
gas_fee_strategy: params.gas_fee_strategy,
|
gas_fee_strategy: params.gas_fee_strategy,
|
||||||
simulate: params.simulate,
|
simulate: params.simulate,
|
||||||
|
log_enabled: self.log_enabled,
|
||||||
|
use_core_affinity: self.use_core_affinity,
|
||||||
|
grpc_recv_us: params.grpc_recv_us,
|
||||||
use_exact_sol_amount: params.use_exact_sol_amount,
|
use_exact_sol_amount: params.use_exact_sol_amount,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate protocol params
|
if !validate_protocol_params(params.dex_type, &protocol_params) {
|
||||||
let is_valid_params = match params.dex_type {
|
|
||||||
DexType::PumpFun => protocol_params.as_any().downcast_ref::<PumpFunParams>().is_some(),
|
|
||||||
DexType::PumpSwap => {
|
|
||||||
protocol_params.as_any().downcast_ref::<PumpSwapParams>().is_some()
|
|
||||||
}
|
|
||||||
DexType::Bonk => protocol_params.as_any().downcast_ref::<BonkParams>().is_some(),
|
|
||||||
DexType::RaydiumCpmm => {
|
|
||||||
protocol_params.as_any().downcast_ref::<RaydiumCpmmParams>().is_some()
|
|
||||||
}
|
|
||||||
DexType::RaydiumAmmV4 => {
|
|
||||||
protocol_params.as_any().downcast_ref::<RaydiumAmmV4Params>().is_some()
|
|
||||||
}
|
|
||||||
DexType::MeteoraDammV2 => {
|
|
||||||
protocol_params.as_any().downcast_ref::<MeteoraDammV2Params>().is_some()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_valid_params {
|
|
||||||
return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
|
return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,8 +681,9 @@ impl TradingClient {
|
|||||||
params: TradeSellParams,
|
params: TradeSellParams,
|
||||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||||
#[cfg(feature = "perf-trace")]
|
#[cfg(feature = "perf-trace")]
|
||||||
if params.slippage_basis_points.is_none() {
|
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||||
log::debug!(
|
debug!(
|
||||||
|
target: "sol_trade_sdk",
|
||||||
"slippage_basis_points is none, use default slippage basis points: {}",
|
"slippage_basis_points is none, use default slippage basis points: {}",
|
||||||
DEFAULT_SLIPPAGE
|
DEFAULT_SLIPPAGE
|
||||||
);
|
);
|
||||||
@@ -623,32 +730,16 @@ impl TradingClient {
|
|||||||
fixed_output_amount: params.fixed_output_token_amount,
|
fixed_output_amount: params.fixed_output_token_amount,
|
||||||
gas_fee_strategy: params.gas_fee_strategy,
|
gas_fee_strategy: params.gas_fee_strategy,
|
||||||
simulate: params.simulate,
|
simulate: params.simulate,
|
||||||
|
log_enabled: self.log_enabled,
|
||||||
|
use_core_affinity: self.use_core_affinity,
|
||||||
|
grpc_recv_us: params.grpc_recv_us,
|
||||||
use_exact_sol_amount: None,
|
use_exact_sol_amount: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate protocol params
|
if !validate_protocol_params(params.dex_type, &protocol_params) {
|
||||||
let is_valid_params = match params.dex_type {
|
|
||||||
DexType::PumpFun => protocol_params.as_any().downcast_ref::<PumpFunParams>().is_some(),
|
|
||||||
DexType::PumpSwap => {
|
|
||||||
protocol_params.as_any().downcast_ref::<PumpSwapParams>().is_some()
|
|
||||||
}
|
|
||||||
DexType::Bonk => protocol_params.as_any().downcast_ref::<BonkParams>().is_some(),
|
|
||||||
DexType::RaydiumCpmm => {
|
|
||||||
protocol_params.as_any().downcast_ref::<RaydiumCpmmParams>().is_some()
|
|
||||||
}
|
|
||||||
DexType::RaydiumAmmV4 => {
|
|
||||||
protocol_params.as_any().downcast_ref::<RaydiumAmmV4Params>().is_some()
|
|
||||||
}
|
|
||||||
DexType::MeteoraDammV2 => {
|
|
||||||
protocol_params.as_any().downcast_ref::<MeteoraDammV2Params>().is_some()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_valid_params {
|
|
||||||
return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
|
return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute sell based on tip preference
|
|
||||||
let swap_result = executor.swap(sell_params).await;
|
let swap_result = executor.swap(sell_params).await;
|
||||||
let result =
|
let result =
|
||||||
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
|
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
|
||||||
@@ -842,4 +933,53 @@ impl TradingClient {
|
|||||||
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
|
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
|
||||||
Ok(signature.to_string())
|
Ok(signature.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Claim Bonding Curve (Pump) cashback.
|
||||||
|
///
|
||||||
|
/// Transfers native SOL from the user's UserVolumeAccumulator to the wallet.
|
||||||
|
/// If there is nothing to claim, the transaction may still succeed with no SOL transferred.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(String)` - Transaction signature
|
||||||
|
/// * `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 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);
|
||||||
|
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
|
||||||
|
Ok(signature.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Claim PumpSwap (AMM) cashback.
|
||||||
|
///
|
||||||
|
/// Transfers WSOL from the UserVolumeAccumulator to the user's WSOL ATA.
|
||||||
|
/// Creates the user's WSOL ATA idempotently if it does not exist, then claims.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// * `Ok(String)` - Transaction signature
|
||||||
|
/// * `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 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"))?;
|
||||||
|
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()));
|
||||||
|
transaction.sign(&[&*self.payer], recent_blockhash);
|
||||||
|
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
|
||||||
|
Ok(signature.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ impl CompilerOptimizer {
|
|||||||
|
|
||||||
/// 🚀 生成超高性能编译配置
|
/// 🚀 生成超高性能编译配置
|
||||||
pub fn generate_ultra_performance_config(&self) -> Result<CompilerConfig> {
|
pub fn generate_ultra_performance_config(&self) -> Result<CompilerConfig> {
|
||||||
log::info!("🚀 Generating ultra-performance compiler configuration...");
|
tracing::info!(target: "sol_trade_sdk","🚀 Generating ultra-performance compiler configuration...");
|
||||||
|
|
||||||
let mut rustflags = Vec::new();
|
let mut rustflags = Vec::new();
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ impl CompilerOptimizer {
|
|||||||
cargo_config: self.generate_cargo_config(),
|
cargo_config: self.generate_cargo_config(),
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("✅ Ultra-performance compiler configuration generated");
|
tracing::info!(target: "sol_trade_sdk","✅ Ultra-performance compiler configuration generated");
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
//! 🚀 硬件级性能优化 - CPU缓存行对齐 & SIMD加速
|
//! Hardware-oriented optimizations: cache-line alignment, prefetch, SIMD, branch hints, memory barriers.
|
||||||
//!
|
//! 硬件级优化:缓存行对齐与预取、SIMD、分支提示、内存屏障。
|
||||||
//! 实现CPU硬件特性的深度利用,包括:
|
|
||||||
//! - 缓存行对齐和缓存预取
|
|
||||||
//! - SIMD指令集优化
|
|
||||||
//! - 分支预测优化
|
|
||||||
//! - 内存屏障控制
|
|
||||||
//! - CPU指令流水线优化
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
@@ -13,26 +7,23 @@ use std::ptr;
|
|||||||
use crossbeam_utils::CachePadded;
|
use crossbeam_utils::CachePadded;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
// CPU缓存行大小常量 (通常为64字节)
|
/// Typical CPU cache line size in bytes. 典型 CPU 缓存行大小(字节)。
|
||||||
pub const CACHE_LINE_SIZE: usize = 64;
|
pub const CACHE_LINE_SIZE: usize = 64;
|
||||||
|
|
||||||
/// 🚀 硬件优化的数据结构基础特征
|
/// Trait for cache-line-aligned data and prefetch. 缓存行对齐与预取 trait。
|
||||||
pub trait CacheLineAligned {
|
pub trait CacheLineAligned {
|
||||||
/// 确保数据结构按缓存行对齐
|
|
||||||
fn ensure_cache_aligned(&self) -> bool;
|
fn ensure_cache_aligned(&self) -> bool;
|
||||||
/// 预取数据到CPU缓存
|
|
||||||
fn prefetch_data(&self);
|
fn prefetch_data(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 SIMD优化的内存操作
|
/// SIMD-accelerated memory operations. SIMD 加速的内存操作。
|
||||||
pub struct SIMDMemoryOps;
|
pub struct SIMDMemoryOps;
|
||||||
|
|
||||||
impl SIMDMemoryOps {
|
impl SIMDMemoryOps {
|
||||||
/// 🚀 SIMD加速的内存拷贝 - 针对小数据包优化
|
/// SIMD-optimized copy by size class. 按长度分派的 SIMD 拷贝。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn memcpy_simd_optimized(dst: *mut u8, src: *const u8, len: usize) {
|
pub unsafe fn memcpy_simd_optimized(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
match len {
|
match len {
|
||||||
// 针对不同数据大小使用不同优化策略
|
|
||||||
0 => return,
|
0 => return,
|
||||||
1..=8 => Self::memcpy_small(dst, src, len),
|
1..=8 => Self::memcpy_small(dst, src, len),
|
||||||
9..=16 => Self::memcpy_sse(dst, src, len),
|
9..=16 => Self::memcpy_sse(dst, src, len),
|
||||||
@@ -42,7 +33,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 小数据拷贝优化 (1-8字节)
|
/// Copy 1–8 bytes (scalar / small word). 小数据拷贝(1–8 字节)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcpy_small(dst: *mut u8, src: *const u8, len: usize) {
|
unsafe fn memcpy_small(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
match len {
|
match len {
|
||||||
@@ -63,7 +54,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSE优化拷贝 (9-16字节)
|
/// Copy 9–16 bytes using SSE (128-bit). SSE 拷贝(9–16 字节)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcpy_sse(dst: *mut u8, src: *const u8, len: usize) {
|
unsafe fn memcpy_sse(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -82,7 +73,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AVX优化拷贝 (17-32字节)
|
/// Copy 17–32 bytes using AVX (256-bit). AVX 拷贝(17–32 字节)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcpy_avx(dst: *mut u8, src: *const u8, len: usize) {
|
unsafe fn memcpy_avx(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -101,19 +92,16 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AVX2优化拷贝 (33-64字节)
|
/// Copy 33–64 bytes using AVX2 (256-bit, two chunks). AVX2 拷贝(33–64 字节,两段)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcpy_avx2(dst: *mut u8, src: *const u8, len: usize) {
|
unsafe fn memcpy_avx2(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
{
|
{
|
||||||
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_storeu_si256};
|
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_storeu_si256};
|
||||||
|
|
||||||
// 拷贝前32字节
|
|
||||||
let chunk1 = _mm256_loadu_si256(src as *const __m256i);
|
let chunk1 = _mm256_loadu_si256(src as *const __m256i);
|
||||||
_mm256_storeu_si256(dst as *mut __m256i, chunk1);
|
_mm256_storeu_si256(dst as *mut __m256i, chunk1);
|
||||||
|
|
||||||
if len > 32 {
|
if len > 32 {
|
||||||
// 拷贝剩余字节
|
|
||||||
let remaining = len - 32;
|
let remaining = len - 32;
|
||||||
if remaining <= 32 {
|
if remaining <= 32 {
|
||||||
let chunk2 = _mm256_loadu_si256(src.add(32) as *const __m256i);
|
let chunk2 = _mm256_loadu_si256(src.add(32) as *const __m256i);
|
||||||
@@ -128,7 +116,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AVX512或回退拷贝 (>64字节)
|
/// Copy >64 bytes: AVX-512 64-byte chunks when available, else AVX2 32-byte chunks. >64 字节:有 AVX512 用 64 字节块,否则 AVX2 32 字节块。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcpy_avx512_or_fallback(dst: *mut u8, src: *const u8, len: usize) {
|
unsafe fn memcpy_avx512_or_fallback(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
|
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
|
||||||
@@ -138,14 +126,12 @@ impl SIMDMemoryOps {
|
|||||||
let chunks = len / 64;
|
let chunks = len / 64;
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
|
|
||||||
// 使用AVX512处理64字节块
|
|
||||||
for _ in 0..chunks {
|
for _ in 0..chunks {
|
||||||
let chunk = _mm512_loadu_si512(src.add(offset) as *const __m512i);
|
let chunk = _mm512_loadu_si512(src.add(offset) as *const __m512i);
|
||||||
_mm512_storeu_si512(dst.add(offset) as *mut __m512i, chunk);
|
_mm512_storeu_si512(dst.add(offset) as *mut __m512i, chunk);
|
||||||
offset += 64;
|
offset += 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理剩余字节
|
|
||||||
let remaining = len % 64;
|
let remaining = len % 64;
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
Self::memcpy_avx2(dst.add(offset), src.add(offset), remaining);
|
Self::memcpy_avx2(dst.add(offset), src.add(offset), remaining);
|
||||||
@@ -154,7 +140,6 @@ impl SIMDMemoryOps {
|
|||||||
|
|
||||||
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
|
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
|
||||||
{
|
{
|
||||||
// 回退到AVX2分块处理
|
|
||||||
let chunks = len / 32;
|
let chunks = len / 32;
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
|
|
||||||
@@ -170,7 +155,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 SIMD加速的内存比较
|
/// SIMD-optimized byte equality; dispatches by length (small / SSE / AVX2 / large). SIMD 加速的内存比较,按长度分派。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn memcmp_simd_optimized(a: *const u8, b: *const u8, len: usize) -> bool {
|
pub unsafe fn memcmp_simd_optimized(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||||
match len {
|
match len {
|
||||||
@@ -182,7 +167,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 小数据比较
|
/// Compare 1–8 bytes (scalar). 小数据比较(1–8 字节)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
|
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||||
match len {
|
match len {
|
||||||
@@ -198,7 +183,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SSE比较
|
/// Compare 9–16 bytes using SSE. SSE 比较(9–16 字节)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcmp_sse(a: *const u8, b: *const u8, len: usize) -> bool {
|
unsafe fn memcmp_sse(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -210,7 +195,6 @@ impl SIMDMemoryOps {
|
|||||||
let cmp_result = _mm_cmpeq_epi8(chunk_a, chunk_b);
|
let cmp_result = _mm_cmpeq_epi8(chunk_a, chunk_b);
|
||||||
let mask = _mm_movemask_epi8(cmp_result) as u32;
|
let mask = _mm_movemask_epi8(cmp_result) as u32;
|
||||||
|
|
||||||
// 检查前len字节是否相等
|
|
||||||
let valid_mask = if len >= 16 { 0xFFFF } else { (1u32 << len) - 1 };
|
let valid_mask = if len >= 16 { 0xFFFF } else { (1u32 << len) - 1 };
|
||||||
(mask & valid_mask) == valid_mask
|
(mask & valid_mask) == valid_mask
|
||||||
}
|
}
|
||||||
@@ -221,7 +205,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AVX2比较
|
/// Compare 17–32 bytes using AVX2. AVX2 比较(17–32 字节)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcmp_avx2(a: *const u8, b: *const u8, len: usize) -> bool {
|
unsafe fn memcmp_avx2(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -243,7 +227,7 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 大数据比较
|
/// Compare >32 bytes in 32-byte AVX2 chunks. 大数据比较(32 字节 AVX2 分块)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
unsafe fn memcmp_large(a: *const u8, b: *const u8, len: usize) -> bool {
|
unsafe fn memcmp_large(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||||
let chunks = len / 32;
|
let chunks = len / 32;
|
||||||
@@ -263,7 +247,7 @@ impl SIMDMemoryOps {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 SIMD加速的内存清零
|
/// SIMD-optimized zero memory. SIMD 加速的内存清零。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn memzero_simd_optimized(ptr: *mut u8, len: usize) {
|
pub unsafe fn memzero_simd_optimized(ptr: *mut u8, len: usize) {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -279,7 +263,6 @@ impl SIMDMemoryOps {
|
|||||||
offset += 32;
|
offset += 32;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理剩余字节
|
|
||||||
let remaining = len % 32;
|
let remaining = len % 32;
|
||||||
for i in 0..remaining {
|
for i in 0..remaining {
|
||||||
*ptr.add(offset + i) = 0;
|
*ptr.add(offset + i) = 0;
|
||||||
@@ -293,14 +276,15 @@ impl SIMDMemoryOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 缓存行对齐的原子计数器
|
/// Cache-line-aligned atomic counter. 缓存行对齐的原子计数器。
|
||||||
#[repr(align(64))] // 强制64字节对齐
|
#[repr(align(64))]
|
||||||
pub struct CacheAlignedCounter {
|
pub struct CacheAlignedCounter {
|
||||||
value: AtomicU64,
|
value: AtomicU64,
|
||||||
_padding: [u8; CACHE_LINE_SIZE - size_of::<AtomicU64>()],
|
_padding: [u8; CACHE_LINE_SIZE - size_of::<AtomicU64>()],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CacheAlignedCounter {
|
impl CacheAlignedCounter {
|
||||||
|
/// Create counter with initial value. 创建并设置初值。
|
||||||
pub fn new(initial: u64) -> Self {
|
pub fn new(initial: u64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: AtomicU64::new(initial),
|
value: AtomicU64::new(initial),
|
||||||
@@ -339,23 +323,18 @@ impl CacheLineAligned for CacheAlignedCounter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 缓存友好的环形缓冲区
|
/// Cache-friendly lock-free ring buffer. 缓存友好的无锁环形缓冲区。
|
||||||
#[repr(align(64))]
|
#[repr(align(64))]
|
||||||
pub struct CacheOptimizedRingBuffer<T> {
|
pub struct CacheOptimizedRingBuffer<T> {
|
||||||
/// 数据缓冲区
|
|
||||||
buffer: Vec<T>,
|
buffer: Vec<T>,
|
||||||
/// 生产者头指针 (独占缓存行)
|
|
||||||
producer_head: CachePadded<AtomicU64>,
|
producer_head: CachePadded<AtomicU64>,
|
||||||
/// 消费者尾指针 (独占缓存行)
|
|
||||||
consumer_tail: CachePadded<AtomicU64>,
|
consumer_tail: CachePadded<AtomicU64>,
|
||||||
/// 容量 (2的幂次方)
|
|
||||||
capacity: usize,
|
capacity: usize,
|
||||||
/// 掩码 (capacity - 1)
|
|
||||||
mask: usize,
|
mask: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
||||||
/// 创建缓存优化的环形缓冲区
|
/// Create ring buffer; capacity must be a power of 2. 创建环形缓冲区,容量须为 2 的幂。
|
||||||
pub fn new(capacity: usize) -> Result<Self> {
|
pub fn new(capacity: usize) -> Result<Self> {
|
||||||
if !capacity.is_power_of_two() {
|
if !capacity.is_power_of_two() {
|
||||||
return Err(anyhow::anyhow!("Capacity must be a power of 2"));
|
return Err(anyhow::anyhow!("Capacity must be a power of 2"));
|
||||||
@@ -373,53 +352,41 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 无锁写入元素
|
/// Lock-free push; returns false if full. 无锁写入,满则返回 false。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn try_push(&self, item: T) -> bool {
|
pub fn try_push(&self, item: T) -> bool {
|
||||||
let current_head = self.producer_head.load(Ordering::Relaxed);
|
let current_head = self.producer_head.load(Ordering::Relaxed);
|
||||||
let current_tail = self.consumer_tail.load(Ordering::Acquire);
|
let current_tail = self.consumer_tail.load(Ordering::Acquire);
|
||||||
|
|
||||||
// 检查是否还有空间
|
|
||||||
if (current_head + 1) & self.mask as u64 == current_tail & self.mask as u64 {
|
if (current_head + 1) & self.mask as u64 == current_tail & self.mask as u64 {
|
||||||
return false; // 缓冲区满
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入数据
|
|
||||||
unsafe {
|
unsafe {
|
||||||
let index = current_head & self.mask as u64;
|
let index = current_head & self.mask as u64;
|
||||||
let ptr = self.buffer.as_ptr().add(index as usize) as *mut T;
|
let ptr = self.buffer.as_ptr().add(index as usize) as *mut T;
|
||||||
ptr.write(item);
|
ptr.write(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发布新的头指针
|
|
||||||
self.producer_head.store(current_head + 1, Ordering::Release);
|
self.producer_head.store(current_head + 1, Ordering::Release);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 无锁读取元素
|
/// Lock-free pop; returns None if empty. 无锁读取,空则返回 None。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn try_pop(&self) -> Option<T> {
|
pub fn try_pop(&self) -> Option<T> {
|
||||||
let current_tail = self.consumer_tail.load(Ordering::Relaxed);
|
let current_tail = self.consumer_tail.load(Ordering::Relaxed);
|
||||||
let current_head = self.producer_head.load(Ordering::Acquire);
|
let current_head = self.producer_head.load(Ordering::Acquire);
|
||||||
|
|
||||||
// 检查是否有数据
|
|
||||||
if current_tail == current_head {
|
if current_tail == current_head {
|
||||||
return None; // 缓冲区空
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取数据
|
|
||||||
let item = unsafe {
|
let item = unsafe {
|
||||||
let index = current_tail & self.mask as u64;
|
let index = current_tail & self.mask as u64;
|
||||||
let ptr = self.buffer.as_ptr().add(index as usize);
|
let ptr = self.buffer.as_ptr().add(index as usize);
|
||||||
ptr.read()
|
ptr.read()
|
||||||
};
|
};
|
||||||
|
|
||||||
// 发布新的尾指针
|
|
||||||
self.consumer_tail.store(current_tail + 1, Ordering::Release);
|
self.consumer_tail.store(current_tail + 1, Ordering::Release);
|
||||||
Some(item)
|
Some(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前元素数量
|
/// Current number of elements. 当前元素个数。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
let head = self.producer_head.load(Ordering::Relaxed);
|
let head = self.producer_head.load(Ordering::Relaxed);
|
||||||
@@ -427,7 +394,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
|||||||
((head + self.capacity as u64 - tail) & self.mask as u64) as usize
|
((head + self.capacity as u64 - tail) & self.mask as u64) as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 检查是否为空
|
/// True if no elements. 是否为空。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.producer_head.load(Ordering::Relaxed) ==
|
self.producer_head.load(Ordering::Relaxed) ==
|
||||||
@@ -445,24 +412,18 @@ impl<T> CacheLineAligned for CacheOptimizedRingBuffer<T> {
|
|||||||
unsafe {
|
unsafe {
|
||||||
use std::arch::x86_64::_mm_prefetch;
|
use std::arch::x86_64::_mm_prefetch;
|
||||||
use std::arch::x86_64::_MM_HINT_T0;
|
use std::arch::x86_64::_MM_HINT_T0;
|
||||||
|
|
||||||
// 预取头指针
|
|
||||||
_mm_prefetch(self.producer_head.as_ptr() as *const i8, _MM_HINT_T0);
|
_mm_prefetch(self.producer_head.as_ptr() as *const i8, _MM_HINT_T0);
|
||||||
|
|
||||||
// 预取尾指针
|
|
||||||
_mm_prefetch(self.consumer_tail.as_ptr() as *const i8, _MM_HINT_T0);
|
_mm_prefetch(self.consumer_tail.as_ptr() as *const i8, _MM_HINT_T0);
|
||||||
|
|
||||||
// 预取缓冲区开始位置
|
|
||||||
_mm_prefetch(self.buffer.as_ptr() as *const i8, _MM_HINT_T0);
|
_mm_prefetch(self.buffer.as_ptr() as *const i8, _MM_HINT_T0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 CPU分支预测优化工具
|
/// Branch hint helpers (likely/unlikely) and prefetch. 分支提示与预取。
|
||||||
pub struct BranchOptimizer;
|
pub struct BranchOptimizer;
|
||||||
|
|
||||||
impl BranchOptimizer {
|
impl BranchOptimizer {
|
||||||
/// likely宏 - 告诉编译器条件大概率为真
|
/// Hint: condition is usually true. 提示编译器条件大概率为真。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn likely(condition: bool) -> bool {
|
pub fn likely(condition: bool) -> bool {
|
||||||
#[cold]
|
#[cold]
|
||||||
@@ -474,7 +435,7 @@ impl BranchOptimizer {
|
|||||||
condition
|
condition
|
||||||
}
|
}
|
||||||
|
|
||||||
/// unlikely宏 - 告诉编译器条件大概率为假
|
/// Hint: condition is usually false. 提示编译器条件大概率为假。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn unlikely(condition: bool) -> bool {
|
pub fn unlikely(condition: bool) -> bool {
|
||||||
#[cold]
|
#[cold]
|
||||||
@@ -486,7 +447,7 @@ impl BranchOptimizer {
|
|||||||
condition
|
condition
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 预取指令 - 提前加载数据到缓存
|
/// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
|
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -497,7 +458,7 @@ impl BranchOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 预取指令 - 提前加载数据到缓存(写优化)
|
/// Prefetch for write (T1 hint). 写预取(T1 提示)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
|
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
@@ -509,35 +470,35 @@ impl BranchOptimizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🚀 内存屏障控制
|
/// Memory barrier helpers. 内存屏障辅助。
|
||||||
pub struct MemoryBarriers;
|
pub struct MemoryBarriers;
|
||||||
|
|
||||||
impl MemoryBarriers {
|
impl MemoryBarriers {
|
||||||
/// 编译器屏障 - 防止编译器重排序
|
/// Compiler barrier only (no CPU reorder). 仅编译器屏障,防止重排序。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn compiler_barrier() {
|
pub fn compiler_barrier() {
|
||||||
std::sync::atomic::compiler_fence(Ordering::SeqCst);
|
std::sync::atomic::compiler_fence(Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 轻量级内存屏障 - 仅CPU重排序保护
|
/// Light barrier (Acquire). 轻量级屏障(Acquire)。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn memory_barrier_light() {
|
pub fn memory_barrier_light() {
|
||||||
std::sync::atomic::fence(Ordering::Acquire);
|
std::sync::atomic::fence(Ordering::Acquire);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 重量级内存屏障 - 全序一致性
|
/// Full sequential consistency barrier. 全序一致性屏障。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn memory_barrier_heavy() {
|
pub fn memory_barrier_heavy() {
|
||||||
std::sync::atomic::fence(Ordering::SeqCst);
|
std::sync::atomic::fence(Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 存储屏障 - 确保写入可见性
|
/// Store/release barrier. 存储屏障,保证写入可见性。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn store_barrier() {
|
pub fn store_barrier() {
|
||||||
std::sync::atomic::fence(Ordering::Release);
|
std::sync::atomic::fence(Ordering::Release);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 加载屏障 - 确保读取正确性
|
/// Load/acquire barrier. 加载屏障,保证读取顺序。
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn load_barrier() {
|
pub fn load_barrier() {
|
||||||
std::sync::atomic::fence(Ordering::Acquire);
|
std::sync::atomic::fence(Ordering::Acquire);
|
||||||
|
|||||||
+2
-8
@@ -1,11 +1,5 @@
|
|||||||
//! 🚀 性能优化模块
|
//! Performance: SIMD, cache prefetch, branch hints, zero-copy I/O, syscall bypass, compiler hints.
|
||||||
//!
|
//! 性能优化:SIMD、缓存预取、分支提示、零拷贝 I/O、系统调用绕过、编译器提示。
|
||||||
//! 提供多层次性能优化:
|
|
||||||
//! - SIMD 向量化:AVX2 内存操作、批量计算
|
|
||||||
//! - 硬件级优化:分支预测、缓存预取
|
|
||||||
//! - 零拷贝 I/O:内存映射、DMA传输
|
|
||||||
//! - 系统调用绕过:批处理、快速时间
|
|
||||||
//! - 编译器优化:内联、向量化
|
|
||||||
|
|
||||||
pub mod simd;
|
pub mod simd;
|
||||||
pub mod hardware_optimizations;
|
pub mod hardware_optimizations;
|
||||||
|
|||||||
+27
-59
@@ -1,13 +1,5 @@
|
|||||||
//! 🚀 系统调用绕过机制 - 最小化系统调用开销
|
//! Syscall bypass: batching, vDSO fast time, io_uring, mmap, userspace impl.
|
||||||
//!
|
//! 系统调用绕过:批处理、vDSO 快速时间、io_uring、mmap、用户态实现。
|
||||||
//! 实现系统调用级别的极致优化,包括:
|
|
||||||
//! - 系统调用批处理
|
|
||||||
//! - vDSO快速系统调用
|
|
||||||
//! - io_uring异步I/O优化
|
|
||||||
//! - 内存映射系统调用
|
|
||||||
//! - 用户空间系统调用实现
|
|
||||||
//! - 系统调用拦截与优化
|
|
||||||
//! - 直接硬件访问
|
|
||||||
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -18,38 +10,25 @@ use std::fs::OpenOptions;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossbeam_utils::CachePadded;
|
use crossbeam_utils::CachePadded;
|
||||||
|
|
||||||
/// 🚀 系统调用绕过管理器
|
/// Syscall bypass manager (batch, fast time, I/O). 系统调用绕过管理器。
|
||||||
pub struct SystemCallBypassManager {
|
pub struct SystemCallBypassManager {
|
||||||
/// 绕过配置
|
|
||||||
config: SyscallBypassConfig,
|
config: SyscallBypassConfig,
|
||||||
/// 批处理器
|
|
||||||
batch_processor: Arc<SyscallBatchProcessor>,
|
batch_processor: Arc<SyscallBatchProcessor>,
|
||||||
/// 快速时间获取器
|
|
||||||
fast_time_provider: Arc<FastTimeProvider>,
|
fast_time_provider: Arc<FastTimeProvider>,
|
||||||
/// I/O优化器
|
|
||||||
_io_optimizer: Arc<IOOptimizer>,
|
_io_optimizer: Arc<IOOptimizer>,
|
||||||
/// 统计信息
|
|
||||||
stats: Arc<SyscallBypassStats>,
|
stats: Arc<SyscallBypassStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 系统调用绕过配置
|
/// Syscall bypass configuration. 系统调用绕过配置。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct SyscallBypassConfig {
|
pub struct SyscallBypassConfig {
|
||||||
/// 启用系统调用批处理
|
|
||||||
pub enable_batch_processing: bool,
|
pub enable_batch_processing: bool,
|
||||||
/// 批处理大小
|
|
||||||
pub batch_size: usize,
|
pub batch_size: usize,
|
||||||
/// 启用快速时间获取
|
|
||||||
pub enable_fast_time: bool,
|
pub enable_fast_time: bool,
|
||||||
/// 启用vDSO优化
|
|
||||||
pub enable_vdso: bool,
|
pub enable_vdso: bool,
|
||||||
/// 启用io_uring
|
|
||||||
pub enable_io_uring: bool,
|
pub enable_io_uring: bool,
|
||||||
/// 启用内存映射优化
|
|
||||||
pub enable_mmap_optimization: bool,
|
pub enable_mmap_optimization: bool,
|
||||||
/// 启用用户空间实现
|
|
||||||
pub enable_userspace_impl: bool,
|
pub enable_userspace_impl: bool,
|
||||||
/// 系统调用缓存大小
|
|
||||||
pub syscall_cache_size: usize,
|
pub syscall_cache_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,30 +47,19 @@ impl Default for SyscallBypassConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 系统调用批处理器
|
|
||||||
pub struct SyscallBatchProcessor {
|
pub struct SyscallBatchProcessor {
|
||||||
/// 待处理的系统调用队列
|
|
||||||
pending_calls: crossbeam_queue::ArrayQueue<SyscallRequest>,
|
pending_calls: crossbeam_queue::ArrayQueue<SyscallRequest>,
|
||||||
/// 批处理线程池
|
|
||||||
_executor: tokio::runtime::Handle,
|
_executor: tokio::runtime::Handle,
|
||||||
/// 批处理统计
|
|
||||||
batch_stats: CachePadded<AtomicU64>,
|
batch_stats: CachePadded<AtomicU64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 系统调用请求
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum SyscallRequest {
|
pub enum SyscallRequest {
|
||||||
/// 文件写入
|
|
||||||
Write { fd: i32, data: Vec<u8> },
|
Write { fd: i32, data: Vec<u8> },
|
||||||
/// 文件读取
|
|
||||||
Read { fd: i32, size: usize },
|
Read { fd: i32, size: usize },
|
||||||
/// 网络发送
|
|
||||||
Send { socket: i32, data: Vec<u8> },
|
Send { socket: i32, data: Vec<u8> },
|
||||||
/// 网络接收
|
|
||||||
Recv { socket: i32, size: usize },
|
Recv { socket: i32, size: usize },
|
||||||
/// 时间获取
|
|
||||||
GetTime,
|
GetTime,
|
||||||
/// 内存分配
|
|
||||||
MemAlloc { size: usize },
|
MemAlloc { size: usize },
|
||||||
/// 内存释放
|
/// 内存释放
|
||||||
MemFree { ptr: usize },
|
MemFree { ptr: usize },
|
||||||
@@ -132,7 +100,7 @@ impl FastTimeProvider {
|
|||||||
vdso_enabled: enable_vdso,
|
vdso_enabled: enable_vdso,
|
||||||
};
|
};
|
||||||
|
|
||||||
log::info!("🚀 Fast time provider initialized with vDSO: {}", enable_vdso);
|
tracing::info!(target: "sol_trade_sdk","🚀 Fast time provider initialized with vDSO: {}", enable_vdso);
|
||||||
Ok(provider)
|
Ok(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +201,7 @@ impl IOOptimizer {
|
|||||||
pub fn new(_config: &SyscallBypassConfig) -> Result<Self> {
|
pub fn new(_config: &SyscallBypassConfig) -> Result<Self> {
|
||||||
let io_uring_available = Self::check_io_uring_support();
|
let io_uring_available = Self::check_io_uring_support();
|
||||||
|
|
||||||
log::info!("🚀 I/O Optimizer initialized - io_uring: {}", io_uring_available);
|
tracing::info!(target: "sol_trade_sdk","🚀 I/O Optimizer initialized - io_uring: {}", io_uring_available);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
io_uring_available,
|
io_uring_available,
|
||||||
@@ -249,7 +217,7 @@ impl IOOptimizer {
|
|||||||
// 检查内核版本和io_uring支持
|
// 检查内核版本和io_uring支持
|
||||||
if let Ok(uname) = std::process::Command::new("uname").arg("-r").output() {
|
if let Ok(uname) = std::process::Command::new("uname").arg("-r").output() {
|
||||||
let kernel_version = String::from_utf8_lossy(&uname.stdout);
|
let kernel_version = String::from_utf8_lossy(&uname.stdout);
|
||||||
log::info!("Kernel version: {}", kernel_version.trim());
|
tracing::info!(target: "sol_trade_sdk","Kernel version: {}", kernel_version.trim());
|
||||||
|
|
||||||
// 简单检查:内核版本 >= 5.1 支持io_uring
|
// 简单检查:内核版本 >= 5.1 支持io_uring
|
||||||
if let Some(version_str) = kernel_version.split('.').next() {
|
if let Some(version_str) = kernel_version.split('.').next() {
|
||||||
@@ -277,7 +245,7 @@ impl IOOptimizer {
|
|||||||
/// 使用io_uring进行批量写入
|
/// 使用io_uring进行批量写入
|
||||||
async fn io_uring_batch_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
|
async fn io_uring_batch_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
|
||||||
// 这里是伪代码 - 实际实现需要io_uring库
|
// 这里是伪代码 - 实际实现需要io_uring库
|
||||||
log::trace!("Using io_uring for {} write operations", requests.len());
|
tracing::trace!(target: "sol_trade_sdk","Using io_uring for {} write operations", requests.len());
|
||||||
|
|
||||||
let mut results = Vec::with_capacity(requests.len());
|
let mut results = Vec::with_capacity(requests.len());
|
||||||
|
|
||||||
@@ -362,7 +330,7 @@ impl IOOptimizer {
|
|||||||
|
|
||||||
self.mmap_regions.push(region);
|
self.mmap_regions.push(region);
|
||||||
|
|
||||||
log::info!("✅ Memory mapped I/O created: {} bytes at {:p}", size, addr);
|
tracing::info!(target: "sol_trade_sdk","✅ Memory mapped I/O created: {} bytes at {:p}", size, addr);
|
||||||
Ok(addr as usize)
|
Ok(addr as usize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,7 +358,7 @@ impl SyscallBatchProcessor {
|
|||||||
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
|
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
|
||||||
let executor = tokio::runtime::Handle::current();
|
let executor = tokio::runtime::Handle::current();
|
||||||
|
|
||||||
log::info!("🚀 Syscall batch processor created with batch size: {}", batch_size);
|
tracing::info!(target: "sol_trade_sdk","🚀 Syscall batch processor created with batch size: {}", batch_size);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
pending_calls,
|
pending_calls,
|
||||||
@@ -464,7 +432,7 @@ impl SyscallBatchProcessor {
|
|||||||
|
|
||||||
self.batch_stats.fetch_add(1, Ordering::Relaxed);
|
self.batch_stats.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
log::trace!("Executed batch of {} syscalls", batch_size);
|
tracing::trace!(target: "sol_trade_sdk","Executed batch of {} syscalls", batch_size);
|
||||||
Ok(batch_size)
|
Ok(batch_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,7 +441,7 @@ impl SyscallBatchProcessor {
|
|||||||
// 使用writev系统调用进行批量写入
|
// 使用writev系统调用进行批量写入
|
||||||
for (fd, data) in requests {
|
for (fd, data) in requests {
|
||||||
// 实际实现会使用writev或io_uring
|
// 实际实现会使用writev或io_uring
|
||||||
log::trace!("Batched write to fd {}: {} bytes", fd, data.len());
|
tracing::trace!(target: "sol_trade_sdk","Batched write to fd {}: {} bytes", fd, data.len());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -482,7 +450,7 @@ impl SyscallBatchProcessor {
|
|||||||
async fn batch_read_operations(&self, requests: Vec<(i32, usize)>) -> Result<()> {
|
async fn batch_read_operations(&self, requests: Vec<(i32, usize)>) -> Result<()> {
|
||||||
// 使用readv系统调用进行批量读取
|
// 使用readv系统调用进行批量读取
|
||||||
for (fd, size) in requests {
|
for (fd, size) in requests {
|
||||||
log::trace!("Batched read from fd {}: {} bytes", fd, size);
|
tracing::trace!(target: "sol_trade_sdk","Batched read from fd {}: {} bytes", fd, size);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -491,7 +459,7 @@ impl SyscallBatchProcessor {
|
|||||||
async fn batch_network_operations(&self, requests: Vec<(i32, Vec<u8>)>) -> Result<()> {
|
async fn batch_network_operations(&self, requests: Vec<(i32, Vec<u8>)>) -> Result<()> {
|
||||||
// 使用sendmsg/recvmsg进行批量网络操作
|
// 使用sendmsg/recvmsg进行批量网络操作
|
||||||
for (socket, data) in requests {
|
for (socket, data) in requests {
|
||||||
log::trace!("Batched network send to socket {}: {} bytes", socket, data.len());
|
tracing::trace!(target: "sol_trade_sdk","Batched network send to socket {}: {} bytes", socket, data.len());
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -515,11 +483,11 @@ impl SystemCallBypassManager {
|
|||||||
let io_optimizer = Arc::new(IOOptimizer::new(&config)?);
|
let io_optimizer = Arc::new(IOOptimizer::new(&config)?);
|
||||||
let stats = Arc::new(SyscallBypassStats::default());
|
let stats = Arc::new(SyscallBypassStats::default());
|
||||||
|
|
||||||
log::info!("🚀 System Call Bypass Manager initialized");
|
tracing::info!(target: "sol_trade_sdk","🚀 System Call Bypass Manager initialized");
|
||||||
log::info!(" 📦 Batch Processing: {}", config.enable_batch_processing);
|
tracing::info!(target: "sol_trade_sdk"," 📦 Batch Processing: {}", config.enable_batch_processing);
|
||||||
log::info!(" ⏰ Fast Time: {}", config.enable_fast_time);
|
tracing::info!(target: "sol_trade_sdk"," ⏰ Fast Time: {}", config.enable_fast_time);
|
||||||
log::info!(" 🚀 vDSO: {}", config.enable_vdso);
|
tracing::info!(target: "sol_trade_sdk"," 🚀 vDSO: {}", config.enable_vdso);
|
||||||
log::info!(" 📁 io_uring: {}", config.enable_io_uring);
|
tracing::info!(target: "sol_trade_sdk"," 📁 io_uring: {}", config.enable_io_uring);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
config,
|
config,
|
||||||
@@ -626,7 +594,7 @@ impl SystemCallBypassManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log::info!("✅ Batch processing worker started");
|
tracing::info!(target: "sol_trade_sdk","✅ Batch processing worker started");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,16 +637,16 @@ pub struct SyscallBypassStatsSnapshot {
|
|||||||
impl SyscallBypassStatsSnapshot {
|
impl SyscallBypassStatsSnapshot {
|
||||||
/// 打印统计信息
|
/// 打印统计信息
|
||||||
pub fn print_stats(&self) {
|
pub fn print_stats(&self) {
|
||||||
log::info!("📊 System Call Bypass Stats:");
|
tracing::info!(target: "sol_trade_sdk","📊 System Call Bypass Stats:");
|
||||||
log::info!(" 🚫 Syscalls Bypassed: {}", self.syscalls_bypassed);
|
tracing::info!(target: "sol_trade_sdk"," 🚫 Syscalls Bypassed: {}", self.syscalls_bypassed);
|
||||||
log::info!(" 📦 Syscalls Batched: {}", self.syscalls_batched);
|
tracing::info!(target: "sol_trade_sdk"," 📦 Syscalls Batched: {}", self.syscalls_batched);
|
||||||
log::info!(" ⏰ Time Calls Cached: {}", self.time_calls_cached);
|
tracing::info!(target: "sol_trade_sdk"," ⏰ Time Calls Cached: {}", self.time_calls_cached);
|
||||||
log::info!(" 📁 I/O Operations Optimized: {}", self.io_operations_optimized);
|
tracing::info!(target: "sol_trade_sdk"," 📁 I/O Operations Optimized: {}", self.io_operations_optimized);
|
||||||
log::info!(" 💾 Memory Operations Avoided: {}", self.memory_operations_avoided);
|
tracing::info!(target: "sol_trade_sdk"," 💾 Memory Operations Avoided: {}", self.memory_operations_avoided);
|
||||||
|
|
||||||
let total_optimizations = self.syscalls_bypassed + self.time_calls_cached +
|
let total_optimizations = self.syscalls_bypassed + self.time_calls_cached +
|
||||||
self.io_operations_optimized + self.memory_operations_avoided;
|
self.io_operations_optimized + self.memory_operations_avoided;
|
||||||
log::info!(" 🏆 Total Optimizations: {}", total_optimizations);
|
tracing::info!(target: "sol_trade_sdk"," 🏆 Total Optimizations: {}", total_optimizations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -73,7 +73,7 @@ impl SharedMemoryPool {
|
|||||||
free_blocks.push(AtomicU64::new(bits));
|
free_blocks.push(AtomicU64::new(bits));
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("🚀 Created shared memory pool {} with {} blocks of {} bytes each",
|
tracing::info!(target: "sol_trade_sdk","🚀 Created shared memory pool {} with {} blocks of {} bytes each",
|
||||||
pool_id, total_blocks, aligned_block_size);
|
pool_id, total_blocks, aligned_block_size);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -155,7 +155,7 @@ impl SharedMemoryPool {
|
|||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn deallocate_block(&self, block: ZeroCopyBlock) {
|
pub fn deallocate_block(&self, block: ZeroCopyBlock) {
|
||||||
if block.pool_id != self.pool_id {
|
if block.pool_id != self.pool_id {
|
||||||
log::error!("Attempting to deallocate block from wrong pool");
|
tracing::error!(target: "sol_trade_sdk", "Attempting to deallocate block from wrong pool");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,7 +267,7 @@ impl MemoryMappedBuffer {
|
|||||||
.map_anon()
|
.map_anon()
|
||||||
.context("Failed to create memory mapped buffer")?;
|
.context("Failed to create memory mapped buffer")?;
|
||||||
|
|
||||||
log::info!("🚀 Created memory mapped buffer {} with size {} bytes", buffer_id, size);
|
tracing::info!(target: "sol_trade_sdk","🚀 Created memory mapped buffer {} with size {} bytes", buffer_id, size);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
mmap,
|
mmap,
|
||||||
@@ -425,7 +425,7 @@ impl DirectMemoryAccessManager {
|
|||||||
dma_channels.push(Arc::new(DMAChannel::new(i)?));
|
dma_channels.push(Arc::new(DMAChannel::new(i)?));
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("🚀 Created DMA manager with {} channels", num_channels);
|
tracing::info!(target: "sol_trade_sdk","🚀 Created DMA manager with {} channels", num_channels);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
dma_channels,
|
dma_channels,
|
||||||
@@ -549,12 +549,12 @@ impl ZeroCopyStats {
|
|||||||
let bytes = self.bytes_transferred.load(Ordering::Relaxed);
|
let bytes = self.bytes_transferred.load(Ordering::Relaxed);
|
||||||
let mmap_usage = self.mmap_buffer_usage.load(Ordering::Relaxed);
|
let mmap_usage = self.mmap_buffer_usage.load(Ordering::Relaxed);
|
||||||
|
|
||||||
log::info!("🚀 Zero-Copy Stats:");
|
tracing::info!(target: "sol_trade_sdk","🚀 Zero-Copy Stats:");
|
||||||
log::info!(" 📦 Blocks: Allocated={}, Freed={}, Active={}",
|
tracing::info!(target: "sol_trade_sdk"," 📦 Blocks: Allocated={}, Freed={}, Active={}",
|
||||||
allocated, freed, allocated.saturating_sub(freed));
|
allocated, freed, allocated.saturating_sub(freed));
|
||||||
log::info!(" 📊 Bytes Transferred: {} ({:.2} MB)",
|
tracing::info!(target: "sol_trade_sdk"," 📊 Bytes Transferred: {} ({:.2} MB)",
|
||||||
bytes, bytes as f64 / 1024.0 / 1024.0);
|
bytes, bytes as f64 / 1024.0 / 1024.0);
|
||||||
log::info!(" 💾 Memory Mapped Usage: {} ({:.2} MB)",
|
tracing::info!(target: "sol_trade_sdk"," 💾 Memory Mapped Usage: {} ({:.2} MB)",
|
||||||
mmap_usage, mmap_usage as f64 / 1024.0 / 1024.0);
|
mmap_usage, mmap_usage as f64 / 1024.0 / 1024.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -581,10 +581,10 @@ impl ZeroCopyMemoryManager {
|
|||||||
let dma_manager = Arc::new(DirectMemoryAccessManager::new(16)?); // 16 DMA channels
|
let dma_manager = Arc::new(DirectMemoryAccessManager::new(16)?); // 16 DMA channels
|
||||||
let stats = Arc::new(ZeroCopyStats::new());
|
let stats = Arc::new(ZeroCopyStats::new());
|
||||||
|
|
||||||
log::info!("🚀 Zero-Copy Memory Manager initialized");
|
tracing::info!(target: "sol_trade_sdk","🚀 Zero-Copy Memory Manager initialized");
|
||||||
log::info!(" 📦 Memory Pools: {}", shared_pools.len());
|
tracing::info!(target: "sol_trade_sdk"," 📦 Memory Pools: {}", shared_pools.len());
|
||||||
log::info!(" 💾 Mapped Buffers: {}", mmap_buffers.len());
|
tracing::info!(target: "sol_trade_sdk"," 💾 Mapped Buffers: {}", mmap_buffers.len());
|
||||||
log::info!(" 🔄 DMA Channels: 16");
|
tracing::info!(target: "sol_trade_sdk"," 🔄 DMA Channels: 16");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
shared_pools,
|
shared_pools,
|
||||||
|
|||||||
+35
-74
@@ -1,13 +1,12 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use bincode::serialize as bincode_serialize;
|
||||||
|
use solana_client::rpc_client::SerializableTransaction;
|
||||||
use solana_sdk::transaction::VersionedTransaction;
|
use solana_sdk::transaction::VersionedTransaction;
|
||||||
use crate::swqos::{SwqosType, TradeType};
|
use crate::swqos::{SwqosType, TradeType};
|
||||||
use crate::swqos::SwqosClientTrait;
|
use crate::swqos::SwqosClientTrait;
|
||||||
@@ -17,6 +16,9 @@ use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS};
|
|||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
|
/// Empty body for getHealth POST; avoid per-request allocation.
|
||||||
|
static PING_BODY: &[u8] = &[];
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AstralaneClient {
|
pub struct AstralaneClient {
|
||||||
pub endpoint: String,
|
pub endpoint: String,
|
||||||
@@ -50,19 +52,7 @@ impl SwqosClientTrait for AstralaneClient {
|
|||||||
impl AstralaneClient {
|
impl AstralaneClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
@@ -90,17 +80,12 @@ impl AstralaneClient {
|
|||||||
let stop_ping = self.stop_ping.clone();
|
let stop_ping = self.stop_ping.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await; // first tick completes immediately → one ping at start
|
||||||
|
|
||||||
if stop_ping.load(Ordering::Relaxed) {
|
if stop_ping.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ping request
|
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
||||||
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!("Astralane ping request failed: {}", e);
|
eprintln!("Astralane ping request failed: {}", e);
|
||||||
}
|
}
|
||||||
@@ -117,73 +102,49 @@ impl AstralaneClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send ping request to /gethealth endpoint
|
/// Send ping request: POST endpoint?api-key=...&method=getHealth (endpoint is irisb from constants).
|
||||||
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||||
// Build ping URL by replacing /iris with /gethealth
|
let response = http_client
|
||||||
let ping_url = if endpoint.ends_with("/iris") {
|
.post(endpoint)
|
||||||
endpoint.replace("/iris", "/gethealth")
|
.query(&[("api-key", auth_token), ("method", "getHealth")])
|
||||||
} else if endpoint.ends_with("/iris/") {
|
.timeout(Duration::from_millis(1500))
|
||||||
endpoint.replace("/iris/", "/gethealth")
|
.body(PING_BODY)
|
||||||
} else if endpoint.ends_with('/') {
|
|
||||||
format!("{}gethealth", endpoint)
|
|
||||||
} else {
|
|
||||||
format!("{}/gethealth", endpoint)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send GET request to /gethealth endpoint with api_key header
|
|
||||||
let response = http_client.get(&ping_url)
|
|
||||||
.header("api_key", auth_token)
|
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
let status = response.status();
|
||||||
if response.status().is_success() {
|
let _ = response.bytes().await; // consume body so connection returns to pool
|
||||||
// ping successful, connection remains active
|
if !status.is_success() {
|
||||||
// println!("send getHealth to keep connection alive");
|
eprintln!("Astralane ping request returned non-success status: {}", status);
|
||||||
} else {
|
|
||||||
eprintln!("Astralane ping request returned non-success status: {}", response.status());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send transaction via /irisb binary API (no Base64; lower latency).
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let signature = transaction.get_signature();
|
||||||
|
|
||||||
let request_body = serde_json::to_string(&json!({
|
let body_bytes = bincode_serialize(transaction).map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"method": "sendTransaction",
|
|
||||||
"params": [
|
|
||||||
content,
|
|
||||||
{ "encoding": "base64", "skipPreflight": true },
|
|
||||||
{ "mevProtect": false }
|
|
||||||
]
|
|
||||||
}))?;
|
|
||||||
|
|
||||||
// Send request with api_key header
|
let response = self.http_client
|
||||||
let response_text = self.http_client.post(&self.endpoint)
|
.post(&self.endpoint)
|
||||||
.body(request_body)
|
.query(&[("api-key", self.auth_token.as_str()), ("method", "sendTransaction")])
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/octet-stream")
|
||||||
.header("api_key", &self.auth_token)
|
.body(body_bytes)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Parse JSON response
|
let status = response.status();
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
let _ = response.bytes().await;
|
||||||
if response_json.get("result").is_some() {
|
if status.is_success() {
|
||||||
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
|
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
|
||||||
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, _error);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, response_text);
|
eprintln!(" [astralane] {} submission failed: status {}", trade_type, status);
|
||||||
|
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_time: Instant = Instant::now();
|
let start_time = Instant::now();
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
|
|||||||
+46
-76
@@ -1,7 +1,6 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::{Client, header::{HeaderMap, HeaderValue, CONTENT_TYPE}};
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -50,19 +49,7 @@ impl SwqosClientTrait for BlockRazorClient {
|
|||||||
impl BlockRazorClient {
|
impl BlockRazorClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
@@ -90,18 +77,22 @@ impl BlockRazorClient {
|
|||||||
let stop_ping = self.stop_ping.clone();
|
let stop_ping = self.stop_ping.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
// 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 {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
if stop_ping.load(Ordering::Relaxed) {
|
if stop_ping.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ping request
|
|
||||||
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!("BlockRazor ping request failed: {}", e);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!("BlockRazor ping request failed: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -116,84 +107,63 @@ impl BlockRazorClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send ping request to /health endpoint
|
/// Send ping request: POST /v2/health?auth=... (Keep Alive). Only required param: auth.
|
||||||
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||||
// Build health URL by replacing sendTransaction with health
|
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
|
||||||
let ping_url = if endpoint.ends_with("sendTransaction") {
|
let response = http_client
|
||||||
endpoint.replace("sendTransaction", "health")
|
.post(&ping_url)
|
||||||
} else if endpoint.ends_with("/sendTransaction") {
|
.query(&[("auth", auth_token)])
|
||||||
endpoint.replace("/sendTransaction", "/health")
|
.header("Content-Type", "text/plain")
|
||||||
} else {
|
.timeout(Duration::from_millis(1500))
|
||||||
// Fallback to original logic if endpoint doesn't end with sendTransaction
|
.body(&[] as &[u8])
|
||||||
if endpoint.ends_with('/') {
|
|
||||||
format!("{}health", endpoint)
|
|
||||||
} else {
|
|
||||||
format!("{}/health", endpoint)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Prepare headers
|
|
||||||
let mut headers = HeaderMap::new();
|
|
||||||
headers.insert("apikey", HeaderValue::from_str(auth_token)?);
|
|
||||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
|
||||||
|
|
||||||
// Send GET request to /health endpoint with headers
|
|
||||||
let response = http_client.get(&ping_url)
|
|
||||||
.headers(headers)
|
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
let status = response.status();
|
||||||
if response.status().is_success() {
|
let _ = response.bytes().await;
|
||||||
// ping successful, connection remains active
|
if !status.is_success() {
|
||||||
// Can optionally log, but to reduce noise, not printing here
|
eprintln!("BlockRazor ping request failed with status: {}", status);
|
||||||
} else {
|
|
||||||
eprintln!("BlockRazor ping request failed with status: {}", response.status());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send transaction via v2 API: plain Base64 body, Content-Type: text/plain. Only required URI param: auth.
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
// BlockRazor使用fast模式的请求格式
|
let response = self.http_client
|
||||||
let request_body = serde_json::to_string(&json!({
|
.post(&self.endpoint)
|
||||||
"transaction": content,
|
.query(&[("auth", self.auth_token.as_str())])
|
||||||
"mode": "fast"
|
.header("Content-Type", "text/plain")
|
||||||
}))?;
|
.body(content)
|
||||||
|
|
||||||
// BlockRazor使用apikey header
|
|
||||||
let response_text = self.http_client.post(&self.endpoint)
|
|
||||||
.body(request_body)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.header("apikey", &self.auth_token)
|
|
||||||
.send()
|
.send()
|
||||||
.await?
|
|
||||||
.text()
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Parse JSON response
|
let status = response.status();
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
let _ = response.bytes().await;
|
||||||
if response_json.get("result").is_some() || response_json.get("signature").is_some() {
|
if status.is_success() {
|
||||||
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
|
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
|
||||||
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, _error);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, response_text);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!(" [blockrazor] {} submission failed: status {}", trade_type, status);
|
||||||
|
}
|
||||||
|
return Err(anyhow::anyhow!("BlockRazor sendTransaction failed: {}", status));
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_time: Instant = Instant::now();
|
let start_time = Instant::now();
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [blockrazor] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
println!(" signature: {:?}", signature);
|
||||||
|
println!(" [blockrazor] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||||
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if wait_confirmation {
|
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
println!(" [blockrazor] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
println!(" [blockrazor] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-50
@@ -1,4 +1,7 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
|
use crate::swqos::common::default_http_client_builder;
|
||||||
|
use crate::swqos::common::poll_transaction_confirmation;
|
||||||
|
use crate::swqos::common::serialize_transaction_and_encode;
|
||||||
|
use crate::swqos::serialization;
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
@@ -45,17 +48,9 @@ impl SwqosClientTrait for BloxrouteClient {
|
|||||||
impl BloxrouteClient {
|
impl BloxrouteClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder()
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
.pool_idle_timeout(Duration::from_secs(120))
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
.pool_max_idle_per_host(256)
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
@@ -63,34 +58,34 @@ impl BloxrouteClient {
|
|||||||
|
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
let body = serde_json::json!({
|
// Single format! for body to avoid json! + to_string() double allocation
|
||||||
"transaction": {
|
let body = format!(
|
||||||
"content": content,
|
r#"{{"transaction":{{"content":"{}"}},"frontRunningProtection":false,"useStakedRPCs":true}}"#,
|
||||||
},
|
content
|
||||||
"frontRunningProtection": false,
|
);
|
||||||
"useStakedRPCs": true,
|
|
||||||
});
|
|
||||||
|
|
||||||
let endpoint = format!("{}/api/v2/submit", self.endpoint);
|
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.to_string())
|
.body(body)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Authorization", self.auth_token.clone())
|
.header("Authorization", self.auth_token.as_str())
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.text()
|
.text()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
|
// Parse with from_str to avoid extra wait from .json().await
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||||
if response_json.get("result").is_some() {
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
|
if response_json.get("result").is_some() {
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, _error);
|
} else if let Some(_error) = response_json.get("error") {
|
||||||
|
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, _error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, response_text);
|
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, response_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,12 +93,14 @@ impl BloxrouteClient {
|
|||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [bloxroute] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
println!(" signature: {:?}", signature);
|
||||||
|
println!(" [bloxroute] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||||
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if wait_confirmation {
|
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
println!(" [bloxroute] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
println!(" [bloxroute] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||||
}
|
}
|
||||||
@@ -111,37 +108,37 @@ impl BloxrouteClient {
|
|||||||
Ok(())
|
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 start_time = Instant::now();
|
||||||
|
|
||||||
let body = serde_json::json!({
|
let contents = serialization::serialize_transactions_batch_sync(
|
||||||
"entries": transactions
|
transactions.as_slice(),
|
||||||
.iter()
|
UiTransactionEncoding::Base64,
|
||||||
.map(|tx| {
|
)?;
|
||||||
serde_json::json!({
|
let entries: String = contents
|
||||||
"transaction": {
|
.iter()
|
||||||
"content": tx.to_base64_string(),
|
.map(|c| format!(r#"{{"transaction":{{"content":"{}"}}}}"#, c))
|
||||||
},
|
.collect::<Vec<_>>()
|
||||||
})
|
.join(",");
|
||||||
})
|
let body = format!(r#"{{"entries":[{}]}}"#, entries);
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let endpoint = format!("{}/api/v2/submit-batch", self.endpoint);
|
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.to_string())
|
.body(body)
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.header("Authorization", self.auth_token.clone())
|
.header("Authorization", self.auth_token.as_str())
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
.text()
|
.text()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
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() {
|
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||||
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
|
if response_json.get("result").is_some() {
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
eprintln!(" bloxroute {} submission failed: {:?}", trade_type, _error);
|
} else if let Some(_error) = response_json.get("error") {
|
||||||
|
eprintln!(" bloxroute {} submission failed: {:?}", trade_type, _error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+83
-45
@@ -3,6 +3,7 @@ use anyhow::Result;
|
|||||||
use base64::engine::general_purpose::{self, STANDARD};
|
use base64::engine::general_purpose::{self, STANDARD};
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use bincode::serialize;
|
use bincode::serialize;
|
||||||
|
use crate::swqos::serialization;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json;
|
use serde_json;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -16,6 +17,36 @@ use std::str::FromStr;
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
|
|
||||||
|
/// Default pool idle timeout for SWQOS HTTP client (seconds). 连接池空闲超时(秒)。
|
||||||
|
const HTTP_POOL_IDLE_TIMEOUT_SECS: u64 = 300;
|
||||||
|
/// Max idle connections per host. 每主机最大空闲连接数。
|
||||||
|
const HTTP_POOL_MAX_IDLE_PER_HOST: usize = 4;
|
||||||
|
/// TCP keepalive interval (seconds). TCP 保活间隔(秒)。
|
||||||
|
const HTTP_TCP_KEEPALIVE_SECS: u64 = 60;
|
||||||
|
/// HTTP/2 keepalive interval (seconds). HTTP/2 保活间隔(秒)。
|
||||||
|
const HTTP2_KEEPALIVE_INTERVAL_SECS: u64 = 10;
|
||||||
|
/// HTTP/2 keepalive timeout (seconds). HTTP/2 保活超时(秒)。
|
||||||
|
const HTTP2_KEEPALIVE_TIMEOUT_SECS: u64 = 5;
|
||||||
|
/// Request timeout (milliseconds). 请求超时(毫秒)。
|
||||||
|
const HTTP_TIMEOUT_MS: u64 = 3000;
|
||||||
|
/// Connect timeout (milliseconds). 连接超时(毫秒)。
|
||||||
|
const HTTP_CONNECT_TIMEOUT_MS: u64 = 2000;
|
||||||
|
|
||||||
|
/// Shared HTTP client builder for SWQOS clients; call `.build().unwrap()` or override pool first. SWQOS 共用 HTTP 客户端构建器。
|
||||||
|
pub fn default_http_client_builder() -> reqwest::ClientBuilder {
|
||||||
|
Client::builder()
|
||||||
|
.pool_idle_timeout(Duration::from_secs(HTTP_POOL_IDLE_TIMEOUT_SECS))
|
||||||
|
.pool_max_idle_per_host(HTTP_POOL_MAX_IDLE_PER_HOST)
|
||||||
|
.tcp_keepalive(Some(Duration::from_secs(HTTP_TCP_KEEPALIVE_SECS)))
|
||||||
|
.tcp_nodelay(true)
|
||||||
|
.http2_keep_alive_interval(Duration::from_secs(HTTP2_KEEPALIVE_INTERVAL_SECS))
|
||||||
|
.http2_keep_alive_timeout(Duration::from_secs(HTTP2_KEEPALIVE_TIMEOUT_SECS))
|
||||||
|
.http2_adaptive_window(true)
|
||||||
|
.timeout(Duration::from_millis(HTTP_TIMEOUT_MS))
|
||||||
|
.connect_timeout(Duration::from_millis(HTTP_CONNECT_TIMEOUT_MS))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Trade/on-chain error with code and optional instruction index. 交易/链上错误,含错误码与可选指令下标。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct TradeError {
|
pub struct TradeError {
|
||||||
pub code: u32,
|
pub code: u32,
|
||||||
@@ -40,7 +71,7 @@ impl From<anyhow::Error> for TradeError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用高性能序列化
|
// High-performance serialization
|
||||||
|
|
||||||
pub trait FormatBase64VersionedTransaction {
|
pub trait FormatBase64VersionedTransaction {
|
||||||
fn to_base64_string(&self) -> String;
|
fn to_base64_string(&self) -> String;
|
||||||
@@ -58,51 +89,64 @@ pub async fn poll_transaction_confirmation(
|
|||||||
txt_sig: Signature,
|
txt_sig: Signature,
|
||||||
wait_confirmation: bool,
|
wait_confirmation: bool,
|
||||||
) -> Result<Signature> {
|
) -> Result<Signature> {
|
||||||
// 如果不需要等待确认,立即返回签名
|
poll_any_transaction_confirmation(rpc, &[txt_sig], wait_confirmation).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Poll multiple signatures in parallel (one RPC call per poll) and return the first one that confirms.
|
||||||
|
/// When transactions are submitted to multiple SWQOS channels, each channel produces a different
|
||||||
|
/// signature. Only one will land on-chain, so we must check all of them.
|
||||||
|
pub async fn poll_any_transaction_confirmation(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
signatures: &[Signature],
|
||||||
|
wait_confirmation: bool,
|
||||||
|
) -> Result<Signature> {
|
||||||
|
if signatures.is_empty() {
|
||||||
|
return Err(anyhow::anyhow!("No signatures to confirm"));
|
||||||
|
}
|
||||||
|
// If no confirmation needed, return first signature immediately
|
||||||
if !wait_confirmation {
|
if !wait_confirmation {
|
||||||
return Ok(txt_sig);
|
return Ok(signatures[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeout: Duration = Duration::from_secs(15); // 🔧 增加到15秒,避免网络拥堵时超时
|
let timeout: Duration = Duration::from_secs(15);
|
||||||
let interval: Duration = Duration::from_millis(1000);
|
let interval: Duration = Duration::from_millis(1000);
|
||||||
let start: Instant = Instant::now();
|
let start: Instant = Instant::now();
|
||||||
let mut poll_count = 0u32;
|
let mut poll_count = 0u32;
|
||||||
|
// Track which signature landed (confirmed or failed on-chain)
|
||||||
|
let mut landed_sig: Option<Signature> = None;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if start.elapsed() >= timeout {
|
if start.elapsed() >= timeout {
|
||||||
return Err(anyhow::anyhow!("Transaction {}'s confirmation timed out", txt_sig));
|
return Err(anyhow::anyhow!("Transaction confirmation timed out after {}s ({} signatures polled)", timeout.as_secs(), signatures.len()));
|
||||||
}
|
}
|
||||||
|
|
||||||
poll_count += 1;
|
poll_count += 1;
|
||||||
|
|
||||||
let status = rpc.get_signature_statuses(&[txt_sig]).await?;
|
let status = rpc.get_signature_statuses(signatures).await?;
|
||||||
match status.value[0].clone() {
|
// Check all signatures for any that confirmed successfully
|
||||||
Some(status) => {
|
for (i, maybe_status) in status.value.iter().enumerate() {
|
||||||
if status.err.is_none()
|
if let Some(s) = maybe_status {
|
||||||
&& (status.confirmation_status
|
if s.err.is_none()
|
||||||
== Some(TransactionConfirmationStatus::Confirmed)
|
&& (s.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|
||||||
|| status.confirmation_status
|
|| s.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
|
||||||
== Some(TransactionConfirmationStatus::Finalized))
|
|
||||||
{
|
{
|
||||||
return Ok(txt_sig);
|
return Ok(signatures[i]);
|
||||||
}
|
}
|
||||||
// 如果 getSignatureStatuses 返回了错误,立即获取详细信息
|
// Track the first signature that landed on-chain (even if errored)
|
||||||
if status.err.is_some() {
|
if landed_sig.is_none() {
|
||||||
// 直接跳转到获取交易详情
|
landed_sig = Some(signatures[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
|
||||||
// 交易还未上链,继续等待,不调用 getTransaction
|
|
||||||
sleep(interval).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 优化:只在以下情况调用 getTransaction
|
// If no signature has any status yet, keep waiting
|
||||||
// 1. getSignatureStatuses 返回了错误
|
if landed_sig.is_none() {
|
||||||
// 2. 或者已经轮询了较长时间(超过10次,即10秒)
|
sleep(interval).await;
|
||||||
let should_get_transaction = status.value[0].as_ref().map(|s| s.err.is_some()).unwrap_or(false)
|
continue;
|
||||||
|| poll_count >= 10;
|
}
|
||||||
|
|
||||||
|
let landed = landed_sig.unwrap();
|
||||||
|
let should_get_transaction = poll_count >= 10;
|
||||||
|
|
||||||
if !should_get_transaction {
|
if !should_get_transaction {
|
||||||
sleep(interval).await;
|
sleep(interval).await;
|
||||||
@@ -111,7 +155,7 @@ pub async fn poll_transaction_confirmation(
|
|||||||
|
|
||||||
let tx_details = match rpc
|
let tx_details = match rpc
|
||||||
.get_transaction_with_config(
|
.get_transaction_with_config(
|
||||||
&txt_sig,
|
&landed,
|
||||||
RpcTransactionConfig {
|
RpcTransactionConfig {
|
||||||
encoding: Some(UiTransactionEncoding::JsonParsed),
|
encoding: Some(UiTransactionEncoding::JsonParsed),
|
||||||
max_supported_transaction_version: Some(0),
|
max_supported_transaction_version: Some(0),
|
||||||
@@ -122,7 +166,7 @@ pub async fn poll_transaction_confirmation(
|
|||||||
{
|
{
|
||||||
Ok(details) => details,
|
Ok(details) => details,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// 交易可能还未上链,继续等待
|
// Tx may not be on chain yet, keep waiting
|
||||||
sleep(interval).await;
|
sleep(interval).await;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -134,9 +178,9 @@ pub async fn poll_transaction_confirmation(
|
|||||||
} else {
|
} else {
|
||||||
let meta = meta.unwrap();
|
let meta = meta.unwrap();
|
||||||
if meta.err.is_none() {
|
if meta.err.is_none() {
|
||||||
return Ok(txt_sig);
|
return Ok(landed);
|
||||||
} else {
|
} else {
|
||||||
// 从 log_messages 中提取错误信息
|
// Extract error message from log_messages
|
||||||
let mut error_msg = String::new();
|
let mut error_msg = String::new();
|
||||||
if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) =
|
if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) =
|
||||||
&meta.log_messages
|
&meta.log_messages
|
||||||
@@ -162,12 +206,12 @@ pub async fn poll_transaction_confirmation(
|
|||||||
let tx_err: TransactionError =
|
let tx_err: TransactionError =
|
||||||
serde_json::from_value(serde_json::to_value(&ui_err)?)?;
|
serde_json::from_value(serde_json::to_value(&ui_err)?)?;
|
||||||
|
|
||||||
// 直接使用Solana原生的InstructionError中的错误码
|
// Use Solana InstructionError codes directly
|
||||||
let mut code = 0u32;
|
let mut code = 0u32;
|
||||||
let mut index = None;
|
let mut index = None;
|
||||||
match &tx_err {
|
match &tx_err {
|
||||||
TransactionError::InstructionError(i, i_error) => {
|
TransactionError::InstructionError(i, i_error) => {
|
||||||
// 直接匹配所有InstructionError类型,Custom也是其中之一
|
// Match all InstructionError variants including Custom
|
||||||
code = match i_error {
|
code = match i_error {
|
||||||
solana_sdk::instruction::InstructionError::Custom(c) => *c,
|
solana_sdk::instruction::InstructionError::Custom(c) => *c,
|
||||||
solana_sdk::instruction::InstructionError::GenericError => 1,
|
solana_sdk::instruction::InstructionError::GenericError => 1,
|
||||||
@@ -180,7 +224,7 @@ pub async fn poll_transaction_confirmation(
|
|||||||
solana_sdk::instruction::InstructionError::MissingRequiredSignature => 8,
|
solana_sdk::instruction::InstructionError::MissingRequiredSignature => 8,
|
||||||
solana_sdk::instruction::InstructionError::AccountAlreadyInitialized => 9,
|
solana_sdk::instruction::InstructionError::AccountAlreadyInitialized => 9,
|
||||||
solana_sdk::instruction::InstructionError::UninitializedAccount => 10,
|
solana_sdk::instruction::InstructionError::UninitializedAccount => 10,
|
||||||
_ => 999, // 其他未知错误
|
_ => 999, // Other unknown errors
|
||||||
};
|
};
|
||||||
index = Some(*i);
|
index = Some(*i);
|
||||||
}
|
}
|
||||||
@@ -198,11 +242,11 @@ pub async fn poll_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)
|
let serialized = bincode::serialize(transaction)
|
||||||
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
|
||||||
|
|
||||||
// Base64编码
|
// Base64 encode
|
||||||
let encoded = STANDARD.encode(serialized);
|
let encoded = STANDARD.encode(serialized);
|
||||||
|
|
||||||
let request_data = json!({
|
let request_data = json!({
|
||||||
@@ -250,18 +294,12 @@ pub async fn serialize_and_encode(
|
|||||||
Ok(serialized)
|
Ok(serialized)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serialize_transaction_and_encode(
|
/// Sync serialize and encode; uses buffer pool when possible for lower allocs and latency.
|
||||||
|
pub fn serialize_transaction_and_encode(
|
||||||
transaction: &impl SerializableTransaction,
|
transaction: &impl SerializableTransaction,
|
||||||
encoding: UiTransactionEncoding,
|
encoding: UiTransactionEncoding,
|
||||||
) -> Result<(String, Signature)> {
|
) -> Result<(String, Signature)> {
|
||||||
let signature = transaction.get_signature();
|
serialization::serialize_transaction_sync(transaction, encoding)
|
||||||
let serialized_tx = serialize(transaction)?;
|
|
||||||
let serialized = match encoding {
|
|
||||||
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
|
|
||||||
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
|
|
||||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
|
||||||
};
|
|
||||||
Ok((serialized, *signature))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn serialize_smart_transaction_and_encode(
|
pub async fn serialize_smart_transaction_and_encode(
|
||||||
|
|||||||
+3
-16
@@ -1,10 +1,9 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -46,25 +45,13 @@ impl SwqosClientTrait for FlashBlockClient {
|
|||||||
impl FlashBlockClient {
|
impl FlashBlockClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
// FlashBlock API format
|
// FlashBlock API format
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
|
|||||||
+3
-16
@@ -1,11 +1,10 @@
|
|||||||
|
|
||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -49,25 +48,13 @@ impl SwqosClientTrait for JitoClient {
|
|||||||
impl JitoClient {
|
impl JitoClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
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 start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
"id": 1,
|
"id": 1,
|
||||||
|
|||||||
+3
-16
@@ -1,10 +1,9 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -47,25 +46,13 @@ impl LightspeedClient {
|
|||||||
// Lightspeed endpoint should already include /lightspeed path
|
// Lightspeed endpoint should already include /lightspeed path
|
||||||
// Format: https://<tier>.rpc.solanavibestation.com/lightspeed?api_key=<key>
|
// Format: https://<tier>.rpc.solanavibestation.com/lightspeed?api_key=<key>
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256)
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60)))
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000))
|
|
||||||
.connect_timeout(Duration::from_millis(2000))
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction
|
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
|
|||||||
+3
-16
@@ -1,10 +1,9 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -51,25 +50,13 @@ impl NextBlockClient {
|
|||||||
format!("{}/api/v2/submit", endpoint.trim_end_matches('/'))
|
format!("{}/api/v2/submit", endpoint.trim_end_matches('/'))
|
||||||
};
|
};
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
"transaction": {
|
"transaction": {
|
||||||
|
|||||||
+31
-37
@@ -1,4 +1,4 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -50,19 +50,7 @@ impl SwqosClientTrait for Node1Client {
|
|||||||
impl Node1Client {
|
impl Node1Client {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
@@ -90,18 +78,22 @@ impl Node1Client {
|
|||||||
let stop_ping = self.stop_ping.clone();
|
let stop_ping = self.stop_ping.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
// 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!("Node1 ping request failed: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
if stop_ping.load(Ordering::Relaxed) {
|
if stop_ping.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ping request
|
|
||||||
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!("Node1 ping request failed: {}", e);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!("Node1 ping request failed: {}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -125,24 +117,22 @@ impl Node1Client {
|
|||||||
format!("{}/ping", endpoint)
|
format!("{}/ping", endpoint)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send GET request to /ping endpoint (no api-key required)
|
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||||
let response = http_client.get(&ping_url)
|
let response = http_client.get(&ping_url)
|
||||||
|
.timeout(Duration::from_millis(1500))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
let status = response.status();
|
||||||
if response.status().is_success() {
|
let _ = response.bytes().await;
|
||||||
// ping successful, connection remains active
|
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
// Can optionally log, but to reduce noise, not printing here
|
eprintln!("Node1 ping request returned non-success status: {}", status);
|
||||||
} else {
|
|
||||||
eprintln!("Node1 ping request returned non-success status: {}", response.status());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -166,12 +156,14 @@ impl Node1Client {
|
|||||||
|
|
||||||
// Parse JSON response
|
// Parse JSON response
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||||
if response_json.get("result").is_some() {
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [node1] {} submitted: {:?}", trade_type, start_time.elapsed());
|
if response_json.get("result").is_some() {
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
println!(" [node1] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
eprintln!(" [node1] {} submission failed: {:?}", trade_type, _error);
|
} else if let Some(_error) = response_json.get("error") {
|
||||||
|
eprintln!(" [node1] {} submission failed: {:?}", trade_type, _error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
eprintln!(" [node1] {} submission failed: {:?}", trade_type, response_text);
|
eprintln!(" [node1] {} submission failed: {:?}", trade_type, response_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,12 +171,14 @@ impl Node1Client {
|
|||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [node1] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
println!(" signature: {:?}", signature);
|
||||||
|
println!(" [node1] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||||
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if wait_confirmation {
|
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
println!(" [node1] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
println!(" [node1] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||||
}
|
}
|
||||||
|
|||||||
+91
-25
@@ -1,4 +1,4 @@
|
|||||||
//! 交易序列化模块
|
//! Transaction serialization module.
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
@@ -14,7 +14,7 @@ use crate::perf::{
|
|||||||
compiler_optimization::CompileTimeOptimizedEventProcessor,
|
compiler_optimization::CompileTimeOptimizedEventProcessor,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 零分配序列化器 - 使用缓冲池避免运行时分配
|
/// Zero-allocation serializer using a buffer pool to avoid runtime allocation.
|
||||||
pub struct ZeroAllocSerializer {
|
pub struct ZeroAllocSerializer {
|
||||||
buffer_pool: Arc<ArrayQueue<Vec<u8>>>,
|
buffer_pool: Arc<ArrayQueue<Vec<u8>>>,
|
||||||
buffer_size: usize,
|
buffer_size: usize,
|
||||||
@@ -24,7 +24,7 @@ impl ZeroAllocSerializer {
|
|||||||
pub fn new(pool_size: usize, buffer_size: usize) -> Self {
|
pub fn new(pool_size: usize, buffer_size: usize) -> Self {
|
||||||
let pool = ArrayQueue::new(pool_size);
|
let pool = ArrayQueue::new(pool_size);
|
||||||
|
|
||||||
// 预分配缓冲区
|
// Pre-allocate buffers
|
||||||
for _ in 0..pool_size {
|
for _ in 0..pool_size {
|
||||||
let mut buffer = Vec::with_capacity(buffer_size);
|
let mut buffer = Vec::with_capacity(buffer_size);
|
||||||
buffer.resize(buffer_size, 0);
|
buffer.resize(buffer_size, 0);
|
||||||
@@ -38,14 +38,14 @@ impl ZeroAllocSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize_zero_alloc<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> {
|
pub fn serialize_zero_alloc<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> {
|
||||||
// 尝试从池中获取缓冲区
|
// Try to get a buffer from the pool
|
||||||
let mut buffer = self.buffer_pool.pop().unwrap_or_else(|| {
|
let mut buffer = self.buffer_pool.pop().unwrap_or_else(|| {
|
||||||
let mut buf = Vec::with_capacity(self.buffer_size);
|
let mut buf = Vec::with_capacity(self.buffer_size);
|
||||||
buf.resize(self.buffer_size, 0);
|
buf.resize(self.buffer_size, 0);
|
||||||
buf
|
buf
|
||||||
});
|
});
|
||||||
|
|
||||||
// 序列化到缓冲区
|
// Serialize into buffer
|
||||||
let serialized = bincode::serialize(data)?;
|
let serialized = bincode::serialize(data)?;
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
buffer.extend_from_slice(&serialized);
|
buffer.extend_from_slice(&serialized);
|
||||||
@@ -54,11 +54,11 @@ impl ZeroAllocSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn return_buffer(&self, buffer: Vec<u8>) {
|
pub fn return_buffer(&self, buffer: Vec<u8>) {
|
||||||
// 归还缓冲区到池中
|
// Return buffer to the pool
|
||||||
let _ = self.buffer_pool.push(buffer);
|
let _ = self.buffer_pool.push(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取池统计信息
|
/// Get pool statistics.
|
||||||
pub fn get_pool_stats(&self) -> (usize, usize) {
|
pub fn get_pool_stats(&self) -> (usize, usize) {
|
||||||
let available = self.buffer_pool.len();
|
let available = self.buffer_pool.len();
|
||||||
let capacity = self.buffer_pool.capacity();
|
let capacity = self.buffer_pool.capacity();
|
||||||
@@ -66,32 +66,32 @@ impl ZeroAllocSerializer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 全局序列化器实例
|
/// Global serializer instance.
|
||||||
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
|
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
|
||||||
Arc::new(ZeroAllocSerializer::new(
|
Arc::new(ZeroAllocSerializer::new(
|
||||||
10_000, // 池大小
|
10_000, // Pool size
|
||||||
256 * 1024, // 缓冲区大小: 256KB
|
256 * 1024, // Buffer size: 256KB
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
|
|
||||||
/// 🚀 编译时优化的事件处理器 (零运行时开销)
|
/// Compile-time optimized event processor (zero runtime cost).
|
||||||
static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor =
|
static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor =
|
||||||
CompileTimeOptimizedEventProcessor::new();
|
CompileTimeOptimizedEventProcessor::new();
|
||||||
|
|
||||||
/// Base64 编码器
|
/// Base64 encoder.
|
||||||
pub struct Base64Encoder;
|
pub struct Base64Encoder;
|
||||||
|
|
||||||
impl Base64Encoder {
|
impl Base64Encoder {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn encode(data: &[u8]) -> String {
|
pub fn encode(data: &[u8]) -> String {
|
||||||
// 使用编译时优化的哈希进行快速路由
|
// Use compile-time optimized hash for fast routing
|
||||||
let _route = if !data.is_empty() {
|
let _route = if !data.is_empty() {
|
||||||
COMPILE_TIME_PROCESSOR.route_event_zero_cost(data[0])
|
COMPILE_TIME_PROCESSOR.route_event_zero_cost(data[0])
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用 SIMD 加速的 Base64 编码
|
// Use SIMD-accelerated Base64 encoding
|
||||||
SIMDSerializer::encode_base64_simd(data)
|
SIMDSerializer::encode_base64_simd(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,32 +105,98 @@ impl Base64Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 交易序列化
|
/// Guard that returns the serialization buffer to the pool on drop.
|
||||||
|
pub struct PooledTxBufGuard(pub Vec<u8>);
|
||||||
|
|
||||||
|
impl std::ops::Deref for PooledTxBufGuard {
|
||||||
|
type Target = [u8];
|
||||||
|
fn deref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for PooledTxBufGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if !self.0.is_empty() {
|
||||||
|
SERIALIZER.return_buffer(std::mem::take(&mut self.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize transaction to bincode bytes using buffer pool. The returned guard returns the buffer
|
||||||
|
/// to the pool when dropped; use `&*guard` or `guard.as_ref()` for `&[u8]`.
|
||||||
|
pub fn serialize_transaction_bincode_sync(
|
||||||
|
transaction: &impl SerializableTransaction,
|
||||||
|
) -> Result<(PooledTxBufGuard, Signature)> {
|
||||||
|
let signature = transaction.get_signature();
|
||||||
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
||||||
|
Ok((PooledTxBufGuard(serialized_tx), *signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return a buffer to the pool (for manual use when not using `PooledTxBufGuard`).
|
||||||
|
pub fn return_serialization_buffer(buffer: Vec<u8>) {
|
||||||
|
SERIALIZER.return_buffer(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync serialize + encode using buffer pool; use in hot path to reduce allocs.
|
||||||
|
/// Base64 path uses SIMD-accelerated encoding.
|
||||||
|
pub fn serialize_transaction_sync(
|
||||||
|
transaction: &impl SerializableTransaction,
|
||||||
|
encoding: UiTransactionEncoding,
|
||||||
|
) -> Result<(String, Signature)> {
|
||||||
|
let signature = transaction.get_signature();
|
||||||
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
||||||
|
let serialized = match encoding {
|
||||||
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||||
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
||||||
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||||
|
};
|
||||||
|
SERIALIZER.return_buffer(serialized_tx);
|
||||||
|
Ok((serialized, *signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize a transaction (async; no I/O, kept for API compatibility).
|
||||||
pub async fn serialize_transaction(
|
pub async fn serialize_transaction(
|
||||||
transaction: &impl SerializableTransaction,
|
transaction: &impl SerializableTransaction,
|
||||||
encoding: UiTransactionEncoding,
|
encoding: UiTransactionEncoding,
|
||||||
) -> Result<(String, Signature)> {
|
) -> Result<(String, Signature)> {
|
||||||
let signature = transaction.get_signature();
|
let signature = transaction.get_signature();
|
||||||
|
|
||||||
// 使用零分配序列化
|
// Use zero-allocation serialization
|
||||||
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
||||||
|
|
||||||
let serialized = match encoding {
|
let serialized = match encoding {
|
||||||
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||||
UiTransactionEncoding::Base64 => {
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
||||||
// 使用 SIMD 优化的 Base64 编码
|
|
||||||
STANDARD.encode(&serialized_tx)
|
|
||||||
}
|
|
||||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 立即归还缓冲区到池中
|
// Return buffer to pool immediately
|
||||||
SERIALIZER.return_buffer(serialized_tx);
|
SERIALIZER.return_buffer(serialized_tx);
|
||||||
|
|
||||||
Ok((serialized, *signature))
|
Ok((serialized, *signature))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 批量交易序列化
|
/// Sync batch serialize + encode using buffer pool.
|
||||||
|
pub fn serialize_transactions_batch_sync(
|
||||||
|
transactions: &[impl SerializableTransaction],
|
||||||
|
encoding: UiTransactionEncoding,
|
||||||
|
) -> Result<Vec<String>> {
|
||||||
|
let mut results = Vec::with_capacity(transactions.len());
|
||||||
|
for tx in transactions {
|
||||||
|
let serialized_tx = SERIALIZER.serialize_zero_alloc(tx, "transaction")?;
|
||||||
|
let encoded = match encoding {
|
||||||
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||||
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
||||||
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||||
|
};
|
||||||
|
SERIALIZER.return_buffer(serialized_tx);
|
||||||
|
results.push(encoded);
|
||||||
|
}
|
||||||
|
Ok(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Batch transaction serialization.
|
||||||
pub async fn serialize_transactions_batch(
|
pub async fn serialize_transactions_batch(
|
||||||
transactions: &[impl SerializableTransaction],
|
transactions: &[impl SerializableTransaction],
|
||||||
encoding: UiTransactionEncoding,
|
encoding: UiTransactionEncoding,
|
||||||
@@ -142,7 +208,7 @@ pub async fn serialize_transactions_batch(
|
|||||||
|
|
||||||
let encoded = match encoding {
|
let encoded = match encoding {
|
||||||
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||||
UiTransactionEncoding::Base64 => STANDARD.encode(&serialized_tx),
|
UiTransactionEncoding::Base64 => SIMDSerializer::encode_base64_simd(&serialized_tx),
|
||||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,7 +219,7 @@ pub async fn serialize_transactions_batch(
|
|||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取序列化器统计信息
|
/// Get serializer statistics.
|
||||||
pub fn get_serializer_stats() -> (usize, usize) {
|
pub fn get_serializer_stats() -> (usize, usize) {
|
||||||
SERIALIZER.get_pool_stats()
|
SERIALIZER.get_pool_stats()
|
||||||
}
|
}
|
||||||
@@ -168,7 +234,7 @@ mod tests {
|
|||||||
let encoded = Base64Encoder::encode(data);
|
let encoded = Base64Encoder::encode(data);
|
||||||
assert!(!encoded.is_empty());
|
assert!(!encoded.is_empty());
|
||||||
|
|
||||||
// 验证可以正确解码
|
// Verify it decodes correctly
|
||||||
let decoded = STANDARD.decode(&encoded).unwrap();
|
let decoded = STANDARD.decode(&encoded).unwrap();
|
||||||
assert_eq!(&decoded[..data.len()], data);
|
assert_eq!(&decoded[..data.len()], data);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-11
@@ -6,7 +6,6 @@ use quinn::{
|
|||||||
TransportConfig,
|
TransportConfig,
|
||||||
};
|
};
|
||||||
use rand::seq::IndexedRandom as _;
|
use rand::seq::IndexedRandom as _;
|
||||||
use solana_rpc_client::rpc_client::SerializableTransaction;
|
|
||||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -19,6 +18,7 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
use crate::common::SolanaRpcClient;
|
use crate::common::SolanaRpcClient;
|
||||||
use crate::swqos::common::poll_transaction_confirmation;
|
use crate::swqos::common::poll_transaction_confirmation;
|
||||||
|
use crate::swqos::serialization::serialize_transaction_bincode_sync;
|
||||||
use crate::swqos::SwqosClientTrait;
|
use crate::swqos::SwqosClientTrait;
|
||||||
use crate::{
|
use crate::{
|
||||||
constants::swqos::SPEEDLANDING_TIP_ACCOUNTS,
|
constants::swqos::SPEEDLANDING_TIP_ACCOUNTS,
|
||||||
@@ -105,27 +105,32 @@ impl SwqosClientTrait for SpeedlandingClient {
|
|||||||
wait_confirmation: bool,
|
wait_confirmation: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let signature = transaction.get_signature();
|
let (buf_guard, signature) = serialize_transaction_bincode_sync(transaction)?;
|
||||||
let serialized_tx = bincode::serialize(transaction)?;
|
|
||||||
let connection = self.connection.load_full();
|
let connection = self.connection.load_full();
|
||||||
if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() {
|
if Self::try_send_bytes(&connection, &*buf_guard).await.is_err() {
|
||||||
eprintln!(" [speedlanding] {} submission failed, reconnecting", trade_type);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!(" [speedlanding] {} submission failed, reconnecting", trade_type);
|
||||||
|
}
|
||||||
self.reconnect().await?;
|
self.reconnect().await?;
|
||||||
let connection = self.connection.load_full();
|
let connection = self.connection.load_full();
|
||||||
if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await {
|
if let Err(e) = Self::try_send_bytes(&connection, &*buf_guard).await {
|
||||||
eprintln!(" [speedlanding] {} submission failed: {:?}", trade_type, e);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!(" [speedlanding] {} submission failed: {:?}", trade_type, e);
|
||||||
|
}
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [speedlanding] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
println!(" signature: {:?}", signature);
|
||||||
|
println!(" [speedlanding] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||||
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if wait_confirmation {
|
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
println!(" [speedlanding] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
println!(" [speedlanding] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-32
@@ -1,4 +1,4 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -48,19 +48,7 @@ impl SwqosClientTrait for StelliumClient {
|
|||||||
impl StelliumClient {
|
impl StelliumClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let keep_alive_running = Arc::new(AtomicBool::new(true));
|
let keep_alive_running = Arc::new(AtomicBool::new(true));
|
||||||
|
|
||||||
@@ -89,25 +77,34 @@ impl StelliumClient {
|
|||||||
let stop_ping = self.keep_alive_running.clone();
|
let stop_ping = self.keep_alive_running.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
// 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 {
|
||||||
|
let status = resp.status();
|
||||||
|
let _ = resp.bytes().await;
|
||||||
|
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!(" [Stellium] Ping failed with status: {}", status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
if stop_ping.load(Ordering::Relaxed) {
|
if stop_ping.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ping request
|
|
||||||
let url = format!("{}/{}", endpoint, auth_token);
|
let url = format!("{}/{}", endpoint, auth_token);
|
||||||
match http_client.get(&url).send().await {
|
match http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
|
||||||
Ok(response) => {
|
Ok(response) => {
|
||||||
if !response.status().is_success() {
|
let status = response.status();
|
||||||
eprintln!(" [Stellium] Ping failed with status: {}", response.status());
|
let _ = response.bytes().await;
|
||||||
|
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!(" [Stellium] Ping failed with status: {}", status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!(" [Stellium] Ping request error: {:?}", e);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
eprintln!(" [Stellium] Ping request error: {:?}", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +113,7 @@ 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 start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
// Stellium uses standard Solana sendTransaction format
|
// Stellium uses standard Solana sendTransaction format
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
@@ -145,12 +142,14 @@ impl StelliumClient {
|
|||||||
|
|
||||||
// Parse response
|
// Parse response
|
||||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||||
if response_json.get("result").is_some() {
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [Stellium] {} submitted: {:?}", trade_type, start_time.elapsed());
|
if response_json.get("result").is_some() {
|
||||||
} else if let Some(_error) = response_json.get("error") {
|
println!(" [Stellium] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||||
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, _error);
|
} else if let Some(_error) = response_json.get("error") {
|
||||||
|
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, _error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, response_text);
|
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, response_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,12 +157,14 @@ impl StelliumClient {
|
|||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!(" signature: {:?}", signature);
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" [Stellium] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
println!(" signature: {:?}", signature);
|
||||||
|
println!(" [Stellium] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||||
|
}
|
||||||
return Err(e);
|
return Err(e);
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if wait_confirmation {
|
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(" signature: {:?}", signature);
|
println!(" signature: {:?}", signature);
|
||||||
println!(" [Stellium] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
println!(" [Stellium] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-28
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
@@ -76,19 +76,7 @@ impl SwqosClientTrait for TemporalClient {
|
|||||||
impl TemporalClient {
|
impl TemporalClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let client = Self {
|
let client = Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
@@ -116,16 +104,16 @@ impl TemporalClient {
|
|||||||
let stop_ping = self.stop_ping.clone();
|
let stop_ping = self.stop_ping.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||||
|
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||||
|
eprintln!("Temporal ping request failed: {}", e);
|
||||||
|
}
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
if stop_ping.load(Ordering::Relaxed) {
|
if stop_ping.load(Ordering::Relaxed) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send ping request
|
|
||||||
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);
|
eprintln!("Temporal ping request failed: {}", e);
|
||||||
}
|
}
|
||||||
@@ -151,24 +139,22 @@ impl TemporalClient {
|
|||||||
format!("{}/ping", endpoint)
|
format!("{}/ping", endpoint)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send GET request to /ping endpoint
|
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||||
let response = http_client.get(&ping_url)
|
let response = http_client.get(&ping_url)
|
||||||
|
.timeout(Duration::from_millis(1500))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
let status = response.status();
|
||||||
if response.status().is_success() {
|
let _ = response.bytes().await;
|
||||||
// ping successful, connection remains active
|
if !status.is_success() {
|
||||||
// Can optionally log, but to reduce noise, not printing here
|
eprintln!("Temporal ping request returned non-success status: {}", status);
|
||||||
} else {
|
|
||||||
eprintln!("Temporal ping request returned non-success status: {}", response.status());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
// Build request body according to Nozomi documentation requirements
|
// Build request body according to Nozomi documentation requirements
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
|
|||||||
+3
-16
@@ -1,10 +1,9 @@
|
|||||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode};
|
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||||
use rand::seq::IndexedRandom;
|
use rand::seq::IndexedRandom;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::Instant};
|
||||||
|
|
||||||
use std::time::Duration;
|
|
||||||
use solana_transaction_status::UiTransactionEncoding;
|
use solana_transaction_status::UiTransactionEncoding;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -46,25 +45,13 @@ impl SwqosClientTrait for ZeroSlotClient {
|
|||||||
impl ZeroSlotClient {
|
impl ZeroSlotClient {
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = Client::builder()
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
// Optimized connection pool settings for high performance
|
|
||||||
.pool_idle_timeout(Duration::from_secs(120))
|
|
||||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
|
||||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
|
||||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
|
||||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
|
||||||
.http2_keep_alive_timeout(Duration::from_secs(5))
|
|
||||||
.http2_adaptive_window(true) // Enable adaptive flow control
|
|
||||||
.timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s
|
|
||||||
.connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s
|
|
||||||
.build()
|
|
||||||
.unwrap();
|
|
||||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||||
|
|
||||||
let request_body = serde_json::to_string(&json!({
|
let request_body = serde_json::to_string(&json!({
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ use super::{
|
|||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
|
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
|
||||||
constants::swqos::NODE1_TIP_ACCOUNTS,
|
|
||||||
trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}},
|
trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Build standard RPC transaction
|
/// Build standard RPC transaction.
|
||||||
|
/// Takes `business_instructions` by reference to avoid per-task Vec clone in execute_parallel.
|
||||||
pub async fn build_transaction(
|
pub async fn build_transaction(
|
||||||
payer: Arc<Keypair>,
|
payer: Arc<Keypair>,
|
||||||
rpc: Option<Arc<SolanaRpcClient>>,
|
_rpc: Option<Arc<SolanaRpcClient>>,
|
||||||
unit_limit: u32,
|
unit_limit: u32,
|
||||||
unit_price: u64,
|
unit_price: u64,
|
||||||
business_instructions: Vec<Instruction>,
|
business_instructions: &[Instruction],
|
||||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||||
recent_blockhash: Option<Hash>,
|
recent_blockhash: Option<Hash>,
|
||||||
middleware_manager: Option<Arc<MiddlewareManager>>,
|
middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||||
@@ -58,8 +58,8 @@ pub async fn build_transaction(
|
|||||||
unit_limit,
|
unit_limit,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Add business instructions
|
// Add business instructions (clone only here; avoids per-task Vec clone in execute_parallel)
|
||||||
instructions.extend(business_instructions);
|
instructions.extend_from_slice(business_instructions);
|
||||||
|
|
||||||
// Get blockhash for transaction
|
// Get blockhash for transaction
|
||||||
let blockhash = get_transaction_blockhash(recent_blockhash, durable_nonce.clone());
|
let blockhash = get_transaction_blockhash(recent_blockhash, durable_nonce.clone());
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! 并行执行器
|
//! Parallel executor for multi-SWQOS submit.
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use crossbeam_queue::ArrayQueue;
|
use crossbeam_queue::ArrayQueue;
|
||||||
@@ -38,8 +38,9 @@ struct TaskResult {
|
|||||||
success: bool,
|
success: bool,
|
||||||
signature: Signature,
|
signature: Signature,
|
||||||
error: Option<anyhow::Error>,
|
error: Option<anyhow::Error>,
|
||||||
swqos_type: SwqosType, // 🔧 增加:记录SWQOS类型
|
#[allow(dead_code)]
|
||||||
landed_on_chain: bool, // 🔧 Whether tx landed on-chain (even if failed)
|
swqos_type: SwqosType,
|
||||||
|
landed_on_chain: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if an error indicates the transaction landed on-chain (vs network/timeout error)
|
/// Check if an error indicates the transaction landed on-chain (vs network/timeout error)
|
||||||
@@ -86,14 +87,14 @@ impl ResultCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn submit(&self, result: TaskResult) {
|
fn submit(&self, result: TaskResult) {
|
||||||
// 🚀 优化:ArrayQueue 内部已保证同步,无需额外 fence
|
// ArrayQueue is already synchronized; no extra fence needed
|
||||||
let is_success = result.success;
|
let is_success = result.success;
|
||||||
let is_landed_failed = result.landed_on_chain && !result.success;
|
let is_landed_failed = result.landed_on_chain && !result.success;
|
||||||
|
|
||||||
let _ = self.results.push(result);
|
let _ = self.results.push(result);
|
||||||
|
|
||||||
if is_success {
|
if is_success {
|
||||||
self.success_flag.store(true, Ordering::Release); // Release 确保 push 可见
|
self.success_flag.store(true, Ordering::Release);
|
||||||
} else if is_landed_failed {
|
} else if is_landed_failed {
|
||||||
// 🔧 Tx landed but failed (e.g., ExceededSlippage) - nonce is consumed, no point waiting
|
// 🔧 Tx landed but failed (e.g., ExceededSlippage) - nonce is consumed, no point waiting
|
||||||
self.landed_failed_flag.store(true, Ordering::Release);
|
self.landed_failed_flag.store(true, Ordering::Release);
|
||||||
@@ -104,12 +105,11 @@ impl ResultCollector {
|
|||||||
|
|
||||||
async fn wait_for_success(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
async fn wait_for_success(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let timeout = std::time::Duration::from_secs(30);
|
let timeout = std::time::Duration::from_secs(5);
|
||||||
|
let poll_interval = std::time::Duration::from_millis(1000);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// 🚀 Acquire 确保看到 push 的内容
|
|
||||||
if self.success_flag.load(Ordering::Acquire) {
|
if self.success_flag.load(Ordering::Acquire) {
|
||||||
// 🔧 修复:收集所有签名
|
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
let mut has_success = false;
|
let mut has_success = false;
|
||||||
while let Some(result) = self.results.pop() {
|
while let Some(result) = self.results.pop() {
|
||||||
@@ -123,7 +123,7 @@ impl ResultCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔧 Early exit: if a tx landed but failed (e.g., ExceededSlippage),
|
// Early exit: if a tx landed but failed (e.g., ExceededSlippage),
|
||||||
// nonce is consumed and other channels can't succeed - return immediately
|
// nonce is consumed and other channels can't succeed - return immediately
|
||||||
if self.landed_failed_flag.load(Ordering::Acquire) {
|
if self.landed_failed_flag.load(Ordering::Acquire) {
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
@@ -141,8 +141,7 @@ impl ResultCollector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let completed = self.completed_count.load(Ordering::Acquire);
|
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 signatures = Vec::new();
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
let mut any_success = false;
|
let mut any_success = false;
|
||||||
@@ -164,12 +163,11 @@ impl ResultCollector {
|
|||||||
if start.elapsed() > timeout {
|
if start.elapsed() > timeout {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
tokio::task::yield_now().await;
|
tokio::time::sleep(poll_interval).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_first(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
fn get_first(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||||
// 🔧 修复:收集已提交的所有签名
|
|
||||||
let mut signatures = Vec::new();
|
let mut signatures = Vec::new();
|
||||||
let mut has_success = false;
|
let mut has_success = false;
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
@@ -190,11 +188,25 @@ impl ResultCollector {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
|
||||||
|
async fn wait_for_all_submitted(&self, timeout_secs: u64) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||||
|
let start = Instant::now();
|
||||||
|
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||||
|
let poll_interval = std::time::Duration::from_millis(50);
|
||||||
|
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||||
|
if start.elapsed() > timeout {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
tokio::time::sleep(poll_interval).await;
|
||||||
|
}
|
||||||
|
self.get_first()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
|
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
|
||||||
pub async fn execute_parallel(
|
pub async fn execute_parallel(
|
||||||
swqos_clients: Vec<Arc<SwqosClient>>,
|
swqos_clients: &[Arc<SwqosClient>],
|
||||||
payer: Arc<Keypair>,
|
payer: Arc<Keypair>,
|
||||||
rpc: Option<Arc<SolanaRpcClient>>,
|
rpc: Option<Arc<SolanaRpcClient>>,
|
||||||
instructions: Vec<Instruction>,
|
instructions: Vec<Instruction>,
|
||||||
@@ -207,6 +219,7 @@ pub async fn execute_parallel(
|
|||||||
wait_transaction_confirmed: bool,
|
wait_transaction_confirmed: bool,
|
||||||
with_tip: bool,
|
with_tip: bool,
|
||||||
gas_fee_strategy: GasFeeStrategy,
|
gas_fee_strategy: GasFeeStrategy,
|
||||||
|
use_core_affinity: bool,
|
||||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||||
let _exec_start = Instant::now();
|
let _exec_start = Instant::now();
|
||||||
|
|
||||||
@@ -223,10 +236,10 @@ pub async fn execute_parallel(
|
|||||||
return Err(anyhow!("No Rpc Default Swqos configured."));
|
return Err(anyhow!("No Rpc Default Swqos configured."));
|
||||||
}
|
}
|
||||||
|
|
||||||
let cores = core_affinity::get_core_ids().unwrap();
|
let cores = core_affinity::get_core_ids().unwrap_or_default();
|
||||||
let instructions = Arc::new(instructions);
|
let instructions = Arc::new(instructions);
|
||||||
|
|
||||||
// 预先计算所有有效的组合
|
// Precompute all valid (client, gas config) combinations
|
||||||
let task_configs: Vec<_> = swqos_clients
|
let task_configs: Vec<_> = swqos_clients
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
@@ -243,7 +256,7 @@ pub async fn execute_parallel(
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|config| config.0.eq(&swqos_client.get_swqos_type()))
|
.filter(|config| config.0.eq(&swqos_client.get_swqos_type()))
|
||||||
.filter(|config| {
|
.filter(|config| {
|
||||||
// 当需要 tip 且不是 Default 时,按 provider 最低小费进行筛选
|
// When tip required and not Default, filter by provider minimum tip
|
||||||
if with_tip && !matches!(config.0, SwqosType::Default) {
|
if with_tip && !matches!(config.0, SwqosType::Default) {
|
||||||
let min_tip = match config.0 {
|
let min_tip = match config.0 {
|
||||||
SwqosType::Jito => SWQOS_MIN_TIP_JITO,
|
SwqosType::Jito => SWQOS_MIN_TIP_JITO,
|
||||||
@@ -261,9 +274,9 @@ pub async fn execute_parallel(
|
|||||||
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
|
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
|
||||||
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
|
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
|
||||||
};
|
};
|
||||||
if config.2.tip < min_tip {
|
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
println!(
|
println!(
|
||||||
"⚠️ Config filtered: {:?} tip {} is below minimum required tip {}",
|
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
||||||
config.0, config.2.tip, min_tip
|
config.0, config.2.tip, min_tip
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -290,7 +303,8 @@ pub async fn execute_parallel(
|
|||||||
let _spawn_start = Instant::now();
|
let _spawn_start = Instant::now();
|
||||||
|
|
||||||
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
|
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
|
||||||
let core_id = cores[i % cores.len()];
|
let core_id = cores.get(i % cores.len().max(1)).copied();
|
||||||
|
let use_affinity = use_core_affinity;
|
||||||
let payer = payer.clone();
|
let payer = payer.clone();
|
||||||
let instructions = instructions.clone();
|
let instructions = instructions.clone();
|
||||||
let middleware_manager = middleware_manager.clone();
|
let middleware_manager = middleware_manager.clone();
|
||||||
@@ -305,10 +319,15 @@ pub async fn execute_parallel(
|
|||||||
let rpc = rpc.clone();
|
let rpc = rpc.clone();
|
||||||
let durable_nonce = durable_nonce.clone();
|
let durable_nonce = durable_nonce.clone();
|
||||||
let address_lookup_table_account = address_lookup_table_account.clone();
|
let address_lookup_table_account = address_lookup_table_account.clone();
|
||||||
|
let recent_blockhash_task = recent_blockhash.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _task_start = Instant::now();
|
let _task_start = Instant::now();
|
||||||
core_affinity::set_for_current(core_id);
|
if use_affinity {
|
||||||
|
if let Some(cid) = core_id {
|
||||||
|
core_affinity::set_for_current(cid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tip_amount = if with_tip { tip } else { 0.0 };
|
let tip_amount = if with_tip { tip } else { 0.0 };
|
||||||
|
|
||||||
@@ -318,9 +337,9 @@ pub async fn execute_parallel(
|
|||||||
rpc,
|
rpc,
|
||||||
unit_limit,
|
unit_limit,
|
||||||
unit_price,
|
unit_price,
|
||||||
instructions.as_ref().clone(),
|
instructions.as_ref(),
|
||||||
address_lookup_table_account,
|
address_lookup_table_account,
|
||||||
recent_blockhash,
|
recent_blockhash_task,
|
||||||
middleware_manager,
|
middleware_manager,
|
||||||
protocol_name,
|
protocol_name,
|
||||||
is_buy,
|
is_buy,
|
||||||
@@ -338,8 +357,8 @@ pub async fn execute_parallel(
|
|||||||
success: false,
|
success: false,
|
||||||
signature: Signature::default(),
|
signature: Signature::default(),
|
||||||
error: Some(e),
|
error: Some(e),
|
||||||
swqos_type, // 🔧 记录SWQOS类型
|
swqos_type,
|
||||||
landed_on_chain: false, // Build failed, tx never sent
|
landed_on_chain: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -349,6 +368,7 @@ pub async fn execute_parallel(
|
|||||||
|
|
||||||
let _send_start = Instant::now();
|
let _send_start = Instant::now();
|
||||||
let mut err: Option<anyhow::Error> = None;
|
let mut err: Option<anyhow::Error> = None;
|
||||||
|
#[allow(unused_assignments)]
|
||||||
let mut landed_on_chain = false;
|
let mut landed_on_chain = false;
|
||||||
let success = match swqos_client
|
let success = match swqos_client
|
||||||
.send_transaction(
|
.send_transaction(
|
||||||
@@ -371,28 +391,28 @@ pub async fn execute_parallel(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transaction sent
|
// Transaction sent: always submit a result so collector never has "no result" for this task.
|
||||||
|
// If transaction has no signatures (malformed), submit with default signature and success=false.
|
||||||
if let Some(signature) = transaction.signatures.first() {
|
let sig = transaction.signatures.first().copied().unwrap_or_default();
|
||||||
collector.submit(TaskResult {
|
collector.submit(TaskResult {
|
||||||
success,
|
success,
|
||||||
signature: *signature,
|
signature: sig,
|
||||||
error: err,
|
error: err,
|
||||||
swqos_type, // 🔧 记录SWQOS类型
|
swqos_type,
|
||||||
landed_on_chain, // 🔧 Whether tx landed (even if it failed)
|
landed_on_chain,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// All tasks spawned
|
// All tasks spawned
|
||||||
|
|
||||||
if !wait_transaction_confirmed {
|
if !wait_transaction_confirmed {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
const SUBMIT_TIMEOUT_SECS: u64 = 30;
|
||||||
if let Some(result) = collector.get_first() {
|
let (success, signatures, last_error) = collector
|
||||||
return Ok(result);
|
.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS)
|
||||||
}
|
.await
|
||||||
return Err(anyhow!("No transaction signature available"));
|
.unwrap_or((false, vec![], Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS))));
|
||||||
|
return Ok((success, signatures, last_error));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(result) = collector.wait_for_success().await {
|
if let Some(result) = collector.wait_for_success().await {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! 执行模块
|
//! Execution: instruction preprocessing, cache prefetch, branch hints.
|
||||||
|
//! 执行模块:指令预处理、缓存预取、分支提示。
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
@@ -12,7 +13,15 @@ use crate::perf::{
|
|||||||
simd::SIMDMemory,
|
simd::SIMDMemory,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// 预取工具
|
/// Solana account key size in bytes (Pubkey). 每个账户(Pubkey)的字节数。
|
||||||
|
pub const BYTES_PER_ACCOUNT: usize = 32;
|
||||||
|
|
||||||
|
/// Threshold above which we warn about large instruction count. 超过此次数会打 warning。
|
||||||
|
pub const MAX_INSTRUCTIONS_WARN: usize = 64;
|
||||||
|
|
||||||
|
/// Prefetch helper: triggers CPU prefetch for soon-to-be-accessed data to reduce cache-miss latency.
|
||||||
|
/// Call once on hot-path refs; no-op on non-x86_64. Safety: caller ensures valid read-only ref, no concurrent write.
|
||||||
|
/// 缓存预取:对即将访问的数据做 CPU 预取以降低 cache-miss;热路径上调用一次即可;非 x86_64 为 no-op。安全:调用方保证有效只读、无并发写。
|
||||||
pub struct Prefetch;
|
pub struct Prefetch;
|
||||||
|
|
||||||
impl Prefetch {
|
impl Prefetch {
|
||||||
@@ -21,20 +30,15 @@ impl Prefetch {
|
|||||||
if instructions.is_empty() {
|
if instructions.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Prefetch first/middle/last instruction into L1 for subsequent build_transaction access. 预取首/中/尾指令到 L1。
|
||||||
// 预取第一条指令
|
|
||||||
unsafe {
|
unsafe {
|
||||||
BranchOptimizer::prefetch_read_data(&instructions[0]);
|
BranchOptimizer::prefetch_read_data(&instructions[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预取中间指令
|
|
||||||
if instructions.len() > 2 {
|
if instructions.len() > 2 {
|
||||||
unsafe {
|
unsafe {
|
||||||
BranchOptimizer::prefetch_read_data(&instructions[instructions.len() / 2]);
|
BranchOptimizer::prefetch_read_data(&instructions[instructions.len() / 2]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预取最后一条指令
|
|
||||||
if instructions.len() > 1 {
|
if instructions.len() > 1 {
|
||||||
unsafe {
|
unsafe {
|
||||||
BranchOptimizer::prefetch_read_data(&instructions[instructions.len() - 1]);
|
BranchOptimizer::prefetch_read_data(&instructions[instructions.len() - 1]);
|
||||||
@@ -57,46 +61,40 @@ impl Prefetch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 内存操作
|
/// Memory operations (SIMD-accelerated where available). 内存操作(可用时走 SIMD 加速)。
|
||||||
pub struct MemoryOps;
|
pub struct MemoryOps;
|
||||||
|
|
||||||
impl MemoryOps {
|
impl MemoryOps {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn copy(dst: *mut u8, src: *const u8, len: usize) {
|
pub unsafe fn copy(dst: *mut u8, src: *const u8, len: usize) {
|
||||||
// 优先使用 AVX2 SIMD 加速
|
|
||||||
SIMDMemory::copy_avx2(dst, src, len);
|
SIMDMemory::copy_avx2(dst, src, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn compare(a: *const u8, b: *const u8, len: usize) -> bool {
|
pub unsafe fn compare(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||||
// 优先使用 AVX2 SIMD 比较
|
|
||||||
SIMDMemory::compare_avx2(a, b, len)
|
SIMDMemory::compare_avx2(a, b, len)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub unsafe fn zero(ptr: *mut u8, len: usize) {
|
pub unsafe fn zero(ptr: *mut u8, len: usize) {
|
||||||
// 优先使用 AVX2 SIMD 清零
|
|
||||||
SIMDMemory::zero_avx2(ptr, len);
|
SIMDMemory::zero_avx2(ptr, len);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 指令处理器
|
/// Instruction preprocessing and validation. 指令预处理与校验。
|
||||||
pub struct InstructionProcessor;
|
pub struct InstructionProcessor;
|
||||||
|
|
||||||
impl InstructionProcessor {
|
impl InstructionProcessor {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn preprocess(instructions: &[Instruction]) -> Result<()> {
|
pub fn preprocess(instructions: &[Instruction]) -> Result<()> {
|
||||||
// 分支预测: 大概率指令不为空
|
|
||||||
if BranchOptimizer::unlikely(instructions.is_empty()) {
|
if BranchOptimizer::unlikely(instructions.is_empty()) {
|
||||||
return Err(anyhow::anyhow!("Instructions empty"));
|
return Err(anyhow::anyhow!("Instructions empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 预取所有指令到缓存
|
|
||||||
Prefetch::instructions(instructions);
|
Prefetch::instructions(instructions);
|
||||||
|
|
||||||
// 分支预测: 大概率指令数量合理
|
if BranchOptimizer::unlikely(instructions.len() > MAX_INSTRUCTIONS_WARN) {
|
||||||
if BranchOptimizer::unlikely(instructions.len() > 64) {
|
tracing::warn!(target: "sol_trade_sdk", "Large instruction count: {}", instructions.len());
|
||||||
log::warn!("Large instruction count: {}", instructions.len());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -107,7 +105,7 @@ impl InstructionProcessor {
|
|||||||
let mut total_size = 0;
|
let mut total_size = 0;
|
||||||
|
|
||||||
for (i, instr) in instructions.iter().enumerate() {
|
for (i, instr) in instructions.iter().enumerate() {
|
||||||
// 预取下一条指令
|
// Prefetch next instruction; safe: same slice, read-only. 预取下一条指令;安全:同 slice、只读。
|
||||||
unsafe {
|
unsafe {
|
||||||
if let Some(next_instr) = instructions.get(i + 1) {
|
if let Some(next_instr) = instructions.get(i + 1) {
|
||||||
BranchOptimizer::prefetch_read_data(next_instr);
|
BranchOptimizer::prefetch_read_data(next_instr);
|
||||||
@@ -115,20 +113,19 @@ impl InstructionProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
total_size += instr.data.len();
|
total_size += instr.data.len();
|
||||||
total_size += instr.accounts.len() * 32; // 每个账户 32 字节
|
total_size += instr.accounts.len() * BYTES_PER_ACCOUNT;
|
||||||
}
|
}
|
||||||
|
|
||||||
total_size
|
total_size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 执行路径
|
/// Trade direction / execution path helpers. 交易方向与执行路径辅助。
|
||||||
pub struct ExecutionPath;
|
pub struct ExecutionPath;
|
||||||
|
|
||||||
impl ExecutionPath {
|
impl ExecutionPath {
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_buy(input_mint: &Pubkey) -> bool {
|
pub fn is_buy(input_mint: &Pubkey) -> bool {
|
||||||
// 分支预测: 大概率是买入
|
|
||||||
let is_buy = input_mint == &crate::constants::SOL_TOKEN_ACCOUNT
|
let is_buy = input_mint == &crate::constants::SOL_TOKEN_ACCOUNT
|
||||||
|| input_mint == &crate::constants::WSOL_TOKEN_ACCOUNT
|
|| input_mint == &crate::constants::WSOL_TOKEN_ACCOUNT
|
||||||
|| input_mint == &crate::constants::USD1_TOKEN_ACCOUNT
|
|| input_mint == &crate::constants::USD1_TOKEN_ACCOUNT
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ use solana_sdk::{
|
|||||||
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
||||||
signature::Keypair, signature::Signature,
|
signature::Keypair, signature::Signature,
|
||||||
};
|
};
|
||||||
use std::{sync::Arc, time::Instant};
|
use std::{sync::Arc, time::{Duration, Instant}};
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
use tracing::{info, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
|
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
|
||||||
perf::syscall_bypass::SystemCallBypassManager,
|
perf::syscall_bypass::SystemCallBypassManager,
|
||||||
|
swqos::common::poll_any_transaction_confirmation,
|
||||||
trading::core::{
|
trading::core::{
|
||||||
async_executor::execute_parallel,
|
async_executor::execute_parallel,
|
||||||
execution::{ExecutionPath, InstructionProcessor, Prefetch},
|
execution::{InstructionProcessor, Prefetch},
|
||||||
traits::TradeExecutor,
|
traits::TradeExecutor,
|
||||||
},
|
},
|
||||||
trading::MiddlewareManager,
|
trading::MiddlewareManager,
|
||||||
@@ -20,7 +23,9 @@ use once_cell::sync::Lazy;
|
|||||||
use crate::swqos::TradeType;
|
use crate::swqos::TradeType;
|
||||||
use super::{params::SwapParams, traits::InstructionBuilder};
|
use super::{params::SwapParams, traits::InstructionBuilder};
|
||||||
|
|
||||||
/// 🚀 全局系统调用绕过管理器
|
/// Global syscall bypass manager (reserved for future time/IO optimizations).
|
||||||
|
/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
|
||||||
|
#[allow(dead_code)]
|
||||||
static SYSCALL_BYPASS: Lazy<SystemCallBypassManager> = Lazy::new(|| {
|
static SYSCALL_BYPASS: Lazy<SystemCallBypassManager> = Lazy::new(|| {
|
||||||
use crate::perf::syscall_bypass::SyscallBypassConfig;
|
use crate::perf::syscall_bypass::SyscallBypassConfig;
|
||||||
SystemCallBypassManager::new(SyscallBypassConfig::default())
|
SystemCallBypassManager::new(SyscallBypassConfig::default())
|
||||||
@@ -45,27 +50,29 @@ impl GenericTradeExecutor {
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl TradeExecutor for GenericTradeExecutor {
|
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>)> {
|
||||||
let total_start = Instant::now();
|
// 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 {
|
||||||
|
Some(params.grpc_recv_us.unwrap_or_else(crate::common::clock::now_micros))
|
||||||
|
} else {
|
||||||
|
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;
|
||||||
|
|
||||||
// CPU 预取
|
|
||||||
Prefetch::keypair(¶ms.payer);
|
Prefetch::keypair(¶ms.payer);
|
||||||
|
|
||||||
// 构建指令
|
// Time build only when log_enabled to avoid cold-path syscalls. 仅 log_enabled 时计时,减少冷路径 syscall。
|
||||||
let build_start = Instant::now();
|
let build_start = params.log_enabled.then(Instant::now);
|
||||||
let instructions = if is_buy {
|
let instructions = if is_buy {
|
||||||
self.instruction_builder.build_buy_instructions(¶ms).await?
|
self.instruction_builder.build_buy_instructions(¶ms).await?
|
||||||
} else {
|
} else {
|
||||||
self.instruction_builder.build_sell_instructions(¶ms).await?
|
self.instruction_builder.build_sell_instructions(¶ms).await?
|
||||||
};
|
};
|
||||||
let build_elapsed = build_start.elapsed();
|
let build_elapsed = build_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
|
|
||||||
// 指令预处理
|
|
||||||
InstructionProcessor::preprocess(&instructions)?;
|
InstructionProcessor::preprocess(&instructions)?;
|
||||||
|
|
||||||
// 中间件处理
|
|
||||||
let final_instructions = match ¶ms.middleware_manager {
|
let final_instructions = match ¶ms.middleware_manager {
|
||||||
Some(middleware_manager) => middleware_manager
|
Some(middleware_manager) => middleware_manager
|
||||||
.apply_middlewares_process_protocol_instructions(
|
.apply_middlewares_process_protocol_instructions(
|
||||||
@@ -76,12 +83,10 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
None => instructions,
|
None => instructions,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 提交前耗时
|
let before_submit_elapsed = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
let before_submit_elapsed = total_start.elapsed();
|
|
||||||
|
|
||||||
// 如果是模拟模式,直接通过 RPC 模拟交易
|
|
||||||
if params.simulate {
|
if params.simulate {
|
||||||
let send_start = Instant::now();
|
let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
|
||||||
let result = simulate_transaction(
|
let result = simulate_transaction(
|
||||||
params.rpc,
|
params.rpc,
|
||||||
params.payer,
|
params.payer,
|
||||||
@@ -96,44 +101,23 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
params.gas_fee_strategy,
|
params.gas_fee_strategy,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let send_elapsed = send_start.elapsed();
|
let send_elapsed = send_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
let total_elapsed = total_start.elapsed();
|
let total_elapsed = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
|
|
||||||
// Get performance metrics using fast timestamp
|
if crate::common::sdk_log::sdk_log_enabled() {
|
||||||
let timestamp_ns = SYSCALL_BYPASS.fast_timestamp_nanos();
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
|
println!(" [SDK] {} timing(sim) build_instructions: {:.2}ms before_submit: {:.2}ms simulate: {:.2}ms total: {:.2}ms", dir, build_elapsed.as_secs_f64() * 1000.0, before_submit_elapsed.as_secs_f64() * 1000.0, send_elapsed.as_secs_f64() * 1000.0, total_elapsed.as_secs_f64() * 1000.0);
|
||||||
// Print all timing metrics at once to avoid blocking critical path
|
}
|
||||||
println!("[Timestamp] {}ns", timestamp_ns);
|
|
||||||
println!(
|
|
||||||
"[Build Instructions] Time: {:.3}ms ({:.0}μs)",
|
|
||||||
build_elapsed.as_micros() as f64 / 1000.0,
|
|
||||||
build_elapsed.as_micros()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"[Before Submit] {:.3}ms ({:.0}μs)",
|
|
||||||
before_submit_elapsed.as_micros() as f64 / 1000.0,
|
|
||||||
before_submit_elapsed.as_micros()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"[Simulate Transaction] Time: {:.3}ms ({:.0}μs)",
|
|
||||||
send_elapsed.as_micros() as f64 / 1000.0,
|
|
||||||
send_elapsed.as_micros()
|
|
||||||
);
|
|
||||||
println!(
|
|
||||||
"[Total Time] {:.3}ms ({:.0}μs)",
|
|
||||||
total_elapsed.as_micros() as f64 / 1000.0,
|
|
||||||
total_elapsed.as_micros()
|
|
||||||
);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 并行发送交易
|
let need_confirm = params.wait_transaction_confirmed;
|
||||||
let send_start = Instant::now();
|
let send_start = params.log_enabled.then(Instant::now);
|
||||||
let result = execute_parallel(
|
let result = execute_parallel(
|
||||||
params.swqos_clients.clone(),
|
¶ms.swqos_clients,
|
||||||
params.payer,
|
params.payer,
|
||||||
params.rpc,
|
params.rpc.clone(),
|
||||||
final_instructions,
|
final_instructions,
|
||||||
params.address_lookup_table_account,
|
params.address_lookup_table_account,
|
||||||
params.recent_blockhash,
|
params.recent_blockhash,
|
||||||
@@ -141,30 +125,67 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
params.middleware_manager,
|
params.middleware_manager,
|
||||||
self.protocol_name,
|
self.protocol_name,
|
||||||
is_buy,
|
is_buy,
|
||||||
params.wait_transaction_confirmed,
|
false, // submit only here; confirmation and log timing handled below
|
||||||
if is_buy { true } else { params.with_tip },
|
if is_buy { true } else { params.with_tip },
|
||||||
params.gas_fee_strategy,
|
params.gas_fee_strategy,
|
||||||
|
params.use_core_affinity,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let send_elapsed = send_start.elapsed();
|
let send_elapsed = send_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
let total_elapsed = total_start.elapsed();
|
|
||||||
|
|
||||||
// Get performance metrics using fast timestamp
|
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
#[cfg(feature = "perf-trace")]
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
{
|
let build_ms = build_elapsed.as_secs_f64() * 1000.0;
|
||||||
let timestamp_ns = SYSCALL_BYPASS.fast_timestamp_nanos();
|
let before_ms = before_submit_elapsed.as_secs_f64() * 1000.0;
|
||||||
log::trace!(
|
let send_ms = send_elapsed.as_secs_f64() * 1000.0;
|
||||||
"[Execute] timestamp_ns={} build_us={} before_submit_us={} send_us={} total_us={}",
|
if let Some(start_us) = timing_start_us {
|
||||||
timestamp_ns,
|
let now_us = crate::common::clock::now_micros();
|
||||||
build_elapsed.as_micros(),
|
let start_to_submit_us = (now_us - start_us).max(0);
|
||||||
before_submit_elapsed.as_micros(),
|
println!(" [SDK] {} timing(after_submit) build_instructions: {:.2}ms before_submit: {:.2}ms submit: {:.2}ms start_to_submit: {} μs", dir, build_ms, before_ms, send_ms, start_to_submit_us);
|
||||||
send_elapsed.as_micros(),
|
} else {
|
||||||
total_elapsed.as_micros()
|
println!(" [SDK] {} timing(after_submit) build_instructions: {:.2}ms before_submit: {:.2}ms submit: {:.2}ms", dir, build_ms, before_ms, send_ms);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "perf-trace"))]
|
let result = if need_confirm {
|
||||||
let _ = (build_elapsed, before_submit_elapsed, send_elapsed, total_elapsed);
|
let (ok, sigs, err) = match &result {
|
||||||
|
Ok((success, signatures, last_error)) => (
|
||||||
|
*success,
|
||||||
|
signatures.clone(),
|
||||||
|
last_error.as_ref().map(|e| anyhow::anyhow!("{}", e)),
|
||||||
|
),
|
||||||
|
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e))),
|
||||||
|
};
|
||||||
|
let confirm_result = if let Some(rpc) = params.rpc.as_ref() {
|
||||||
|
if sigs.is_empty() {
|
||||||
|
(ok, sigs, err)
|
||||||
|
} else {
|
||||||
|
let confirm_start = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled()).then(Instant::now);
|
||||||
|
let poll_res = poll_any_transaction_confirmation(rpc, &sigs, true).await;
|
||||||
|
let confirm_elapsed = confirm_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||||
|
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
|
let confirm_ms = confirm_elapsed.as_secs_f64() * 1000.0;
|
||||||
|
let total_ms = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO).as_secs_f64() * 1000.0;
|
||||||
|
println!(" [SDK] {} timing(after_confirm) confirm: {:.2}ms total: {:.2}ms", dir, confirm_ms, total_ms);
|
||||||
|
}
|
||||||
|
match poll_res {
|
||||||
|
Ok(_) => (true, sigs, None),
|
||||||
|
Err(e) => (false, sigs, Some(e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(ok, sigs, err)
|
||||||
|
};
|
||||||
|
Ok(confirm_result)
|
||||||
|
} else {
|
||||||
|
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
||||||
|
let total_ms = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO).as_secs_f64() * 1000.0;
|
||||||
|
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||||
|
println!(" [SDK] {} timing total: {:.2}ms", dir, total_ms);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
@@ -174,7 +195,8 @@ impl TradeExecutor for GenericTradeExecutor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔧 修复:Simulate模式返回Vec<Signature>(单个RPC模拟)
|
/// Simulate mode: single RPC simulation, returns Vec<Signature> for API consistency.
|
||||||
|
/// 模拟模式:单次 RPC 模拟,返回 Vec<Signature> 以与 API 一致。
|
||||||
async fn simulate_transaction(
|
async fn simulate_transaction(
|
||||||
rpc: Option<Arc<SolanaRpcClient>>,
|
rpc: Option<Arc<SolanaRpcClient>>,
|
||||||
payer: Arc<Keypair>,
|
payer: Arc<Keypair>,
|
||||||
@@ -215,7 +237,7 @@ async fn simulate_transaction(
|
|||||||
Some(rpc.clone()),
|
Some(rpc.clone()),
|
||||||
unit_limit,
|
unit_limit,
|
||||||
unit_price,
|
unit_price,
|
||||||
instructions,
|
&instructions,
|
||||||
address_lookup_table_account,
|
address_lookup_table_account,
|
||||||
recent_blockhash,
|
recent_blockhash,
|
||||||
middleware_manager,
|
middleware_manager,
|
||||||
@@ -256,12 +278,12 @@ async fn simulate_transaction(
|
|||||||
if let Some(err) = simulate_result.value.err {
|
if let Some(err) = simulate_result.value.err {
|
||||||
#[cfg(feature = "perf-trace")]
|
#[cfg(feature = "perf-trace")]
|
||||||
{
|
{
|
||||||
log::warn!("[Simulation Failed] error={:?} signature={:?}", err, signature);
|
warn!(target: "sol_trade_sdk", "[Simulation Failed] error={:?} signature={:?}", err, signature);
|
||||||
if let Some(logs) = &simulate_result.value.logs {
|
if let Some(logs) = &simulate_result.value.logs {
|
||||||
log::trace!("Transaction logs: {:?}", logs);
|
trace!(target: "sol_trade_sdk", "Transaction logs: {:?}", logs);
|
||||||
}
|
}
|
||||||
if let Some(units_consumed) = simulate_result.value.units_consumed {
|
if let Some(units_consumed) = simulate_result.value.units_consumed {
|
||||||
log::trace!("Compute Units Consumed: {}", units_consumed);
|
trace!(target: "sol_trade_sdk", "Compute Units Consumed: {}", units_consumed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err))));
|
return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err))));
|
||||||
@@ -270,12 +292,12 @@ async fn simulate_transaction(
|
|||||||
// Simulation succeeded
|
// Simulation succeeded
|
||||||
#[cfg(feature = "perf-trace")]
|
#[cfg(feature = "perf-trace")]
|
||||||
{
|
{
|
||||||
log::info!("[Simulation Succeeded] signature={:?}", signature);
|
info!(target: "sol_trade_sdk", "[Simulation Succeeded] signature={:?}", signature);
|
||||||
if let Some(units_consumed) = simulate_result.value.units_consumed {
|
if let Some(units_consumed) = simulate_result.value.units_consumed {
|
||||||
log::trace!("Compute Units Consumed: {}", units_consumed);
|
trace!(target: "sol_trade_sdk", "Compute Units Consumed: {}", units_consumed);
|
||||||
}
|
}
|
||||||
if let Some(logs) = &simulate_result.value.logs {
|
if let Some(logs) = &simulate_result.value.logs {
|
||||||
log::trace!("Transaction logs: {:?}", logs);
|
trace!(target: "sol_trade_sdk", "Transaction logs: {:?}", logs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ pub struct SwapParams {
|
|||||||
pub fixed_output_amount: Option<u64>,
|
pub fixed_output_amount: Option<u64>,
|
||||||
pub gas_fee_strategy: GasFeeStrategy,
|
pub gas_fee_strategy: GasFeeStrategy,
|
||||||
pub simulate: bool,
|
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,
|
||||||
|
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||||
|
pub grpc_recv_us: Option<i64>,
|
||||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||||
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
||||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||||
@@ -107,6 +113,8 @@ impl PumpFunParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||||
|
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||||
pub fn from_dev_trade(
|
pub fn from_dev_trade(
|
||||||
mint: Pubkey,
|
mint: Pubkey,
|
||||||
token_amount: u64,
|
token_amount: u64,
|
||||||
@@ -118,6 +126,7 @@ impl PumpFunParams {
|
|||||||
close_token_account_when_sell: Option<bool>,
|
close_token_account_when_sell: Option<bool>,
|
||||||
fee_recipient: Pubkey,
|
fee_recipient: Pubkey,
|
||||||
token_program: Pubkey,
|
token_program: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||||
@@ -127,6 +136,7 @@ impl PumpFunParams {
|
|||||||
max_sol_cost,
|
max_sol_cost,
|
||||||
creator,
|
creator,
|
||||||
is_mayhem_mode,
|
is_mayhem_mode,
|
||||||
|
is_cashback_coin,
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
bonding_curve: Arc::new(bonding_curve_account),
|
bonding_curve: Arc::new(bonding_curve_account),
|
||||||
@@ -137,6 +147,8 @@ impl PumpFunParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||||
|
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||||
pub fn from_trade(
|
pub fn from_trade(
|
||||||
bonding_curve: Pubkey,
|
bonding_curve: Pubkey,
|
||||||
associated_bonding_curve: Pubkey,
|
associated_bonding_curve: Pubkey,
|
||||||
@@ -150,6 +162,7 @@ impl PumpFunParams {
|
|||||||
close_token_account_when_sell: Option<bool>,
|
close_token_account_when_sell: Option<bool>,
|
||||||
fee_recipient: Pubkey,
|
fee_recipient: Pubkey,
|
||||||
token_program: Pubkey,
|
token_program: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||||
let bonding_curve = BondingCurveAccount::from_trade(
|
let bonding_curve = BondingCurveAccount::from_trade(
|
||||||
@@ -161,6 +174,7 @@ impl PumpFunParams {
|
|||||||
real_token_reserves,
|
real_token_reserves,
|
||||||
real_sol_reserves,
|
real_sol_reserves,
|
||||||
is_mayhem_mode,
|
is_mayhem_mode,
|
||||||
|
is_cashback_coin,
|
||||||
);
|
);
|
||||||
Self {
|
Self {
|
||||||
bonding_curve: Arc::new(bonding_curve),
|
bonding_curve: Arc::new(bonding_curve),
|
||||||
@@ -189,6 +203,7 @@ impl PumpFunParams {
|
|||||||
complete: account.0.complete,
|
complete: account.0.complete,
|
||||||
creator: account.0.creator,
|
creator: account.0.creator,
|
||||||
is_mayhem_mode: account.0.is_mayhem_mode,
|
is_mayhem_mode: account.0.is_mayhem_mode,
|
||||||
|
is_cashback_coin: account.0.is_cashback_coin,
|
||||||
};
|
};
|
||||||
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
||||||
&bonding_curve.account,
|
&bonding_curve.account,
|
||||||
@@ -243,6 +258,8 @@ pub struct PumpSwapParams {
|
|||||||
pub quote_token_program: Pubkey,
|
pub quote_token_program: Pubkey,
|
||||||
/// Whether the pool is in mayhem mode
|
/// Whether the pool is in mayhem mode
|
||||||
pub is_mayhem_mode: bool,
|
pub is_mayhem_mode: bool,
|
||||||
|
/// Whether the pool's coin has cashback enabled
|
||||||
|
pub is_cashback_coin: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PumpSwapParams {
|
impl PumpSwapParams {
|
||||||
@@ -274,6 +291,7 @@ impl PumpSwapParams {
|
|||||||
base_token_program,
|
base_token_program,
|
||||||
quote_token_program,
|
quote_token_program,
|
||||||
is_mayhem_mode,
|
is_mayhem_mode,
|
||||||
|
is_cashback_coin: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,6 +353,7 @@ impl PumpSwapParams {
|
|||||||
} else {
|
} else {
|
||||||
crate::constants::TOKEN_PROGRAM_2022
|
crate::constants::TOKEN_PROGRAM_2022
|
||||||
},
|
},
|
||||||
|
is_cashback_coin: pool_data.is_cashback_coin,
|
||||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||||
crate::constants::TOKEN_PROGRAM
|
crate::constants::TOKEN_PROGRAM
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user