Compare commits
86 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6db55c874 | |||
| 69b4f9bb9f | |||
| 532bc1195d | |||
| 8944c5455c | |||
| 0d846930ad | |||
| 703ae5e4db | |||
| 8e0210af97 | |||
| e605485ecf | |||
| 8dec2f8d8c | |||
| b505bebdac | |||
| 1cc051a874 | |||
| 0311a0b876 | |||
| b49c1c803e | |||
| ce8fb99626 | |||
| 49843d8a20 | |||
| 60d1db4755 | |||
| 7a8d2a9c17 | |||
| 75bfbf6d54 | |||
| 184f1cc583 | |||
| 55c13e2f25 | |||
| fda211ea87 | |||
| 85d2c602f3 | |||
| 02d939b3cf | |||
| e8ec9103ab | |||
| 0fe54f0e94 | |||
| 7ac07247a3 | |||
| 06ed710869 | |||
| d711346c55 | |||
| 06ef2fed84 | |||
| a391539000 | |||
| c75ca4b034 | |||
| b13b4fda0a | |||
| 4dec087ea6 | |||
| 9c9ecf3e5b | |||
| 4b451af5ff | |||
| 8f2f99f3d9 | |||
| 971ef41fad | |||
| 35bfa93516 | |||
| c8f9f9f6aa | |||
| 99846a21c2 | |||
| 062c5415c3 | |||
| eaa3214e4d | |||
| ae890ad976 | |||
| e10ee0de3b | |||
| 5e1fb1a66b | |||
| a5ea26afaf | |||
| ee4a8f685b | |||
| 322acf8baf | |||
| 378b8fc324 | |||
| 19f11aa620 | |||
| 61cea7546c | |||
| d52503b8de | |||
| 9e53b7694c | |||
| 07e45d136f | |||
| 20d053bab3 | |||
| 5d921f23ff | |||
| a187126554 | |||
| 6ea8c27824 | |||
| 8c4e3ee0c5 | |||
| 624b1843a2 | |||
| 7d2ecd57e9 | |||
| 667d6d2c5b | |||
| e957bc4bee | |||
| 274636abc5 | |||
| 2abcf3839e | |||
| 0ec826fcbd | |||
| 6607d276db | |||
| d7b0985844 | |||
| 2e39649ebc | |||
| 0201d3443a | |||
| 9872b1b4a7 | |||
| e16cd6620f | |||
| 9f79e865ab | |||
| e32e7ee6ab | |||
| 30c91a74af | |||
| 6c41e1cfe6 | |||
| a003fd4d2f | |||
| fd5af3ab61 | |||
| 7e2860d8de | |||
| 07487c06cb | |||
| 1142829394 | |||
| d2ce193e2c | |||
| 46564f1bb5 | |||
| 63afac7aea | |||
| d9b9ddc53f | |||
| d610745c7e |
+9
-27
@@ -1,30 +1,12 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
# Proto 生成工具和生成的代码(用户不需要)
|
||||
/proto/gen/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
# 预生成的 proto Rust 代码(提交到仓库,但用户不应修改)
|
||||
# /src/swqos/pb/serverpb.rs <- 这个文件已经提交,用户不应修改
|
||||
|
||||
# Build artifacts
|
||||
/target/
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# RustRover
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
.cargo/
|
||||
|
||||
tmp_*.rs
|
||||
tmp_*.log
|
||||
|
||||
|
||||
.claude/
|
||||
.serena/
|
||||
# Proto sources
|
||||
/proto/
|
||||
|
||||
+31
-22
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "3.5.6"
|
||||
version = "4.0.8"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
"wei <1415121722@qq.com>",
|
||||
]
|
||||
repository = "https://github.com/0xfnzero/sol-trade-sdk"
|
||||
description = "Rust SDK to interact with the dex trade Solana program."
|
||||
description = "A high-performance Rust SDK for Solana DEX trading."
|
||||
license = "MIT"
|
||||
keywords = ["solana", "memecoins", "pumpfun", "pumpswap", "raydium"]
|
||||
readme = "README.md"
|
||||
@@ -37,7 +37,7 @@ members = [
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -45,23 +45,26 @@ perf-trace = [] # 性能追踪特性,生产环境应禁用以获得最佳性
|
||||
|
||||
[dependencies]
|
||||
solana-sdk = "3.0.0"
|
||||
solana-client = "3.1.9"
|
||||
solana-client = "3.1.12"
|
||||
solana-program = "3.0.0"
|
||||
solana-rpc-client = "3.1.9"
|
||||
solana-rpc-client-api = "3.1.9"
|
||||
solana-transaction-status = "3.1.9"
|
||||
solana-account-decoder = "3.1.9"
|
||||
solana-rpc-client = "3.1.12"
|
||||
solana-rpc-client-api = "3.1.12"
|
||||
solana-transaction-status = "3.1.12"
|
||||
solana-account-decoder = "3.1.12"
|
||||
solana-hash = "3.0.0"
|
||||
solana-entry = "3.1.9"
|
||||
solana-rpc-client-nonce-utils = "3.1.9"
|
||||
solana-perf = "3.1.9"
|
||||
solana-metrics = "3.1.9"
|
||||
solana-nonce = "3.1.0"
|
||||
solana-address-lookup-table-interface = "3.0.1"
|
||||
solana-entry = "3.0.0"
|
||||
solana-rpc-client-nonce-utils = "3.1.12"
|
||||
solana-perf = "3.1.12"
|
||||
solana-metrics = "3.1.12"
|
||||
solana-tls-utils = "3.1.12"
|
||||
solana-nonce = "3.2.0"
|
||||
|
||||
solana-address-lookup-table-interface = "3.0.0"
|
||||
solana-message = "3.1.0"
|
||||
solana-compute-budget-interface = "3.0.0"
|
||||
solana-commitment-config = { version = "3.1.1", features = ["serde"] }
|
||||
solana-transaction-status-client-types = "3.1.9"
|
||||
solana-tls-utils = "3.1.9"
|
||||
solana-transaction-status-client-types = "3.1.12"
|
||||
solana-system-interface = { version = "3.0.0", features = ["bincode"] }
|
||||
|
||||
borsh = { version = "1.5.3", features = ["derive"] }
|
||||
isahc = "1.7.2"
|
||||
@@ -76,7 +79,7 @@ bincode = "1.3.3"
|
||||
anyhow = "1.0.90"
|
||||
reqwest = { version = "0.12.12", features = ["json", "multipart"] }
|
||||
tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]}
|
||||
tonic = { version = "0.14.2", features = ["transport"] }
|
||||
tonic = { version = "0.12", features = ["transport"] }
|
||||
rustls = { version = "0.23.23", features = ["ring"] }
|
||||
rustls-native-certs = "0.8.1"
|
||||
tokio-rustls = "0.26.1"
|
||||
@@ -86,10 +89,9 @@ regex = "1"
|
||||
tracing = "0.1.41"
|
||||
thiserror = "2.0.11"
|
||||
async-trait = "0.1.86"
|
||||
lazy_static = "1.5.0"
|
||||
once_cell = "1.20.3"
|
||||
prost = "0.14.1"
|
||||
prost-types = "0.14.1"
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
num_enum = "0.7.3"
|
||||
num-derive = "0.4.2"
|
||||
num-traits = "0.2.19"
|
||||
@@ -98,7 +100,7 @@ bytemuck = { version = "1.4.0" }
|
||||
arrayref = "0.3.6"
|
||||
borsh-derive = "1.5.5"
|
||||
indicatif = "0.18.0"
|
||||
solana-system-interface = { version = "2.0.0", features = ["bincode"] }
|
||||
|
||||
fnv = "1.0.7"
|
||||
dashmap = "6.1.0"
|
||||
clru = "0.6"
|
||||
@@ -107,7 +109,10 @@ parking_lot = "0.12"
|
||||
arc-swap = "1.7"
|
||||
sha2 = "0.10"
|
||||
tonic-prost = "0.14.2"
|
||||
quinn = {version = "0.11", default-features = false, features = ["rustls"]}
|
||||
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime found,QUIC(Speedlanding/Soyas)无法初始化
|
||||
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
|
||||
rcgen = "0.13"
|
||||
uuid = "1.11"
|
||||
|
||||
# Performance optimization dependencies
|
||||
crossbeam-queue = "0.3"
|
||||
@@ -133,6 +138,10 @@ incremental = true # 增量编译 - 大幅加速重新编译
|
||||
opt-level = 1 # 开发时适度优化
|
||||
overflow-checks = true # 开发时启用溢出检查
|
||||
|
||||
# 🚀 构建依赖
|
||||
# 注意:proto 代码已预生成在 src/swqos/pb/serverpb.rs
|
||||
# 开发者如需重新生成代码,请运行 gen_proto 目录下的工具
|
||||
|
||||
# 🚀 性能关键依赖的特殊优化
|
||||
[profile.release.package.solana-sdk]
|
||||
opt-level = 3
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
||||
</p>
|
||||
|
||||
> ☕ **Support This Project**
|
||||
>
|
||||
> This SDK is completely free and open source. However, maintaining and continuously updating it requires significant AI computing resources and token consumption. If this SDK helps with your trading development, consider making a monthly SOL donation — any amount is appreciated and helps keep this project alive!
|
||||
>
|
||||
> **Donation Wallet:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [✨ Features](#-features)
|
||||
@@ -48,10 +54,12 @@
|
||||
- [⚡ Trading Parameters](#-trading-parameters)
|
||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||
- [Astralane (Binary / Plain / QUIC)](#astralane-binary--plain--quic)
|
||||
- [🔧 Middleware System](#-middleware-system)
|
||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||
- [🔍 Nonce Cache](#-nonce-cache)
|
||||
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
|
||||
- [🔄 PumpFun V1 vs V2 Instructions](#-pumpfun-v1-vs-v2-instructions)
|
||||
- [🛡️ MEV Protection Services](#️-mev-protection-services)
|
||||
- [📁 Project Structure](#-project-structure)
|
||||
- [📄 License](#-license)
|
||||
@@ -60,9 +68,20 @@
|
||||
|
||||
---
|
||||
|
||||
## 📦 SDK Versions
|
||||
|
||||
This SDK is available in multiple languages:
|
||||
|
||||
| Language | Repository | Description |
|
||||
|----------|------------|-------------|
|
||||
| **Rust** | [sol-trade-sdk](https://github.com/0xfnzero/sol-trade-sdk) | Ultra-low latency with zero-copy optimization |
|
||||
| **Node.js** | [sol-trade-sdk-nodejs](https://github.com/0xfnzero/sol-trade-sdk-nodejs) | TypeScript/JavaScript for Node.js |
|
||||
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
|
||||
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
|
||||
|
||||
## ✨ Features
|
||||
|
||||
1. **PumpFun Trading**: Support for `buy` and `sell` operations
|
||||
1. **PumpFun Trading**: Support for `buy`, `sell`, `buy_exact_sol_in`, and the new unified `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` instructions (SOL + USDC)
|
||||
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
|
||||
3. **Bonk Trading**: Support for Bonk trading operations
|
||||
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
|
||||
@@ -89,14 +108,14 @@ Add the dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.6" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "3.5.6"
|
||||
sol-trade-sdk = "4.0.8"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -118,14 +137,23 @@ let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// Create TradeConfig instance
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
|
||||
// Optional: customize WSOL ATA and seed optimization
|
||||
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true - check & create WSOL ATA on init
|
||||
// .use_seed_optimize(true) // default: true - seed optimization for ATA ops
|
||||
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
|
||||
// .check_min_tip(false) // default: false - filter SWQOS below min tip
|
||||
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
|
||||
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
|
||||
// .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
@@ -243,7 +271,7 @@ let jito_config = SwqosConfig::Jito(
|
||||
);
|
||||
|
||||
// Using default regional endpoint (third parameter is None)
|
||||
let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
let temporal_config = SwqosConfig::Temporal(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::NewYork, // Will use the default endpoint for this region
|
||||
None // No custom URL, uses SwqosRegion
|
||||
@@ -257,6 +285,29 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||
|
||||
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||
|
||||
Astralane supports **Binary** HTTP (`/irisb`), **Plain** HTTP (`/iris`), and **QUIC** (`host:7000`, or `:9000` when global `mev_protection` is true). Pass `Some(AstralaneTransport::Plain)`, `Some(AstralaneTransport::Quic)`, or use `None` / omit for **Binary** (default). Global `mev_protection` adds `mev-protect=true` on HTTP or selects QUIC port 9000.
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
|
||||
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(AstralaneTransport::Quic),
|
||||
),
|
||||
];
|
||||
// Then create TradeConfig / TradingClient as usual with swqos_configs
|
||||
```
|
||||
|
||||
- **Binary** (default): `None` or `Some(AstralaneTransport::Binary)` — `/irisb`, bincode body.
|
||||
- **Plain**: `Some(AstralaneTransport::Plain)` — `/iris`.
|
||||
- **QUIC**: `Some(AstralaneTransport::Quic)` — regional `host:7000` / `:9000` (MEV); same API key.
|
||||
|
||||
---
|
||||
|
||||
### 🔧 Middleware System
|
||||
@@ -289,18 +340,84 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi
|
||||
- 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.
|
||||
|
||||
#### PumpFun: troubleshooting (on-chain errors)
|
||||
|
||||
For **Anchor 2006 / `NotAuthorized` (6000) / wrong token program / BuyZeroAmount (6020) / slippage (6042)** and related issues, see **[docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)** (Chinese). An English appendix may be added later.
|
||||
|
||||
#### PumpFun: Creator Rewards Sharing (creator_vault)
|
||||
|
||||
Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vault` can differ from the default derivation. If you reuse cached params from a **buy** when **selling**, you may see program error **2006 (seeds constraint violated)**. To avoid this:
|
||||
|
||||
- **From gRPC/events (no RPC needed)**: You can get both `creator` and `creator_vault` from parsed transaction events:
|
||||
- **sol-parser-sdk**: Before pushing events, the pipeline calls `fill_trade_accounts`, which fills `creator_vault` from the buy/sell instruction accounts (buy index 9, sell index 8). `creator` comes from the TradeEvent log. Use `PumpFunParams::from_trade(..., e.creator, e.creator_vault, ...)` or `from_dev_trade(..., e.creator, e.creator_vault, ...)` with the event `e`.
|
||||
- **solana-streamer**: Instruction parsers set `creator_vault` from accounts[9] (buy) or accounts[8] (sell); `creator` comes from the merged CPI TradeEvent log. Use the same `from_trade` / `from_dev_trade` with `e.creator` and `e.creator_vault`.
|
||||
- **Override after RPC**: If you get params via `PumpFunParams::from_mint_by_rpc` but later receive a newer `creator_vault` from gRPC, call `.with_creator_vault(latest_creator_vault)` on the params before selling.
|
||||
|
||||
The SDK does not fetch creator_vault from RPC on every sell (to avoid latency); pass the up-to-date vault from gRPC/events when available.
|
||||
|
||||
#### PumpFun V1 vs V2 Instructions
|
||||
|
||||
PumpFun has two instruction sets for bonding-curve trading:
|
||||
|
||||
| | V1 (default) | V2 (opt-in) |
|
||||
|---|---|---|
|
||||
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
|
||||
| Account metas | 18 | 27 |
|
||||
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
|
||||
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
|
||||
|
||||
**Default: V1** (`use_pumpfun_v2 = false`). The SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table.
|
||||
|
||||
**Key changes in v2 instructions:**
|
||||
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
|
||||
- 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts**
|
||||
- `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory
|
||||
- Same pricing and cost as legacy instructions for SOL-paired coins
|
||||
|
||||
**How to enable V2:**
|
||||
|
||||
**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet):
|
||||
|
||||
```rust
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.use_pumpfun_v2(true) // Switch all PumpFun trades to V2 instructions (27 accounts)
|
||||
.build();
|
||||
```
|
||||
|
||||
**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
// SOL-paired coin with v2 layout
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
|
||||
|
||||
// USDC-paired coin (requires v2)
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(USDC_TOKEN_ACCOUNT);
|
||||
```
|
||||
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
|
||||
#### PumpSwap: coin_creator_vault from events (no RPC)
|
||||
|
||||
For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_authority` are required in buy/sell instructions. Both are available from parsed events without RPC:
|
||||
|
||||
- **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`.
|
||||
- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event's `coin_creator_vault_ata` and `coin_creator_vault_authority`.
|
||||
|
||||
## 🛡️ MEV Protection Services
|
||||
|
||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||
|
||||
- **Jito**: High-performance block space
|
||||
- **ZeroSlot**: Zero-latency transactions
|
||||
- **Temporal**: Time-sensitive transactions
|
||||
- **Bloxroute**: Blockchain network acceleration
|
||||
- **FlashBlock**: High-speed transaction execution with API key authentication - [Official Documentation](https://doc.flashblock.trade/)
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication - [Official Documentation](https://blockrazor.gitbook.io/blockrazor/)
|
||||
- **Node1**: High-speed transaction execution with API key authentication - [Official Documentation](https://node1.me/docs.html)
|
||||
- **Astralane**: Blockchain network acceleration
|
||||
- **FlashBlock**: High-speed transaction execution with API key authentication
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC)
|
||||
- **SpeedLanding**: High-speed transaction execution with API key authentication
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
@@ -333,6 +450,10 @@ MIT License
|
||||
- Telegram Group: https://t.me/fnzero_group
|
||||
- Discord: https://discord.gg/vuazbGkqQE
|
||||
|
||||
## ⏱️ Timing metrics (v3.5.0+)
|
||||
|
||||
When `log_enabled` and SDK log are on, the executor prints `[SDK] Buy/Sell timing(...)`. **Semantics changed in v3.5.0**: `submit` is now only the send to SWQOS/RPC; `confirm` is separate; `start_to_submit` (when `grpc_recv_us` is set) is **end-to-end from gRPC event to submit**, so it is larger than in-process timings. See [docs/TIMING_METRICS.md](docs/TIMING_METRICS.md) for definitions and how to compare with older versions.
|
||||
|
||||
## ⚠️ Important Notes
|
||||
|
||||
1. Test thoroughly before using on mainnet
|
||||
|
||||
+124
-17
@@ -39,6 +39,12 @@
|
||||
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
||||
</p>
|
||||
|
||||
> ☕ **支持本项目**
|
||||
>
|
||||
> 本 SDK 完全免费且开源。但维护和持续更新需要消耗大量 AI 算力与 Token。如果这个 SDK 对您的开发有帮助,欢迎每月捐赠任意数量的 SOL,您的支持将帮助这个项目持续运行!
|
||||
>
|
||||
> **捐赠钱包:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [✨ 项目特性](#-项目特性)
|
||||
@@ -48,10 +54,12 @@
|
||||
- [⚡ 交易参数](#-交易参数)
|
||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||
- [Astralane(Binary / Plain / QUIC)](#astralanebinary--plain--quic)
|
||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||
- [🔍 地址查找表](#-地址查找表)
|
||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||
- [Pump.fun 常见链上错误与排错(文档)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)
|
||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||
- [📁 项目结构](#-项目结构)
|
||||
- [📄 许可证](#-许可证)
|
||||
@@ -60,15 +68,26 @@
|
||||
|
||||
---
|
||||
|
||||
## 📦 SDK 版本
|
||||
|
||||
本 SDK 提供多种语言版本:
|
||||
|
||||
| 语言 | 仓库 | 描述 |
|
||||
|------|------|------|
|
||||
| **Rust** | [sol-trade-sdk](https://github.com/0xfnzero/sol-trade-sdk) | 超低延迟,零拷贝优化 |
|
||||
| **Node.js** | [sol-trade-sdk-nodejs](https://github.com/0xfnzero/sol-trade-sdk-nodejs) | TypeScript/JavaScript,Node.js 支持 |
|
||||
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
|
||||
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
1. **PumpFun 交易**: 支持`购买`、`卖出`功能
|
||||
1. **PumpFun 交易**: 支持 `buy`、`sell`、`buy_exact_sol_in` 以及全新的统一化 `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` 指令(SOL + USDC)
|
||||
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
|
||||
3. **Bonk 交易**: 支持 Bonk 的交易操作
|
||||
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
|
||||
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
|
||||
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
|
||||
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane 等服务
|
||||
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
|
||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||
@@ -89,14 +108,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.5.6" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "3.5.6"
|
||||
sol-trade-sdk = "4.0.8"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -118,14 +137,22 @@ let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain(/iris)或 Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
|
||||
// 可选:自定义 WSOL ATA 与 Seed 优化
|
||||
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // 默认: true - 初始化时检查并创建 WSOL ATA
|
||||
// .use_seed_optimize(true) // 默认: true - ATA 操作启用 seed 优化
|
||||
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
|
||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
@@ -242,7 +269,7 @@ let jito_config = SwqosConfig::Jito(
|
||||
);
|
||||
|
||||
// 使用默认区域端点(第三个参数为 None)
|
||||
let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
let temporal_config = SwqosConfig::Temporal(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::NewYork, // 将使用该区域的默认端点
|
||||
None // 没有自定义 URL,使用 SwqosRegion
|
||||
@@ -256,6 +283,29 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
|
||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||
|
||||
#### Astralane(Binary / Plain / QUIC)
|
||||
|
||||
Astralane 支持 **Binary** HTTP(`/irisb`)、**Plain** HTTP(`/iris`)与 **QUIC**(`host:7000`,全局 `mev_protection` 为 true 时用 `:9000`)。第四个参数:`Some(AstralaneTransport::Plain)`、`Some(AstralaneTransport::Quic)`,或 `None` 表示 **Binary**(默认)。全局 `mev_protection` 会在 HTTP 上附加 `mev-protect=true`,或为 QUIC 选择 9000 端口。
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
|
||||
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(AstralaneTransport::Quic),
|
||||
),
|
||||
];
|
||||
// 然后照常使用 swqos_configs 创建 TradeConfig / TradingClient
|
||||
```
|
||||
|
||||
- **Binary**(默认):`None` 或 `Some(AstralaneTransport::Binary)` — `/irisb`,bincode 正文。
|
||||
- **Plain**:`Some(AstralaneTransport::Plain)` — `/iris`。
|
||||
- **QUIC**:`Some(AstralaneTransport::Quic)` — 按区域的 `host:7000` / `:9000`(MEV);同一 API key。
|
||||
|
||||
---
|
||||
|
||||
### 🔧 中间件系统说明
|
||||
@@ -288,18 +338,75 @@ PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还
|
||||
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。
|
||||
- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。
|
||||
|
||||
#### PumpFun:常见错误与排错思路
|
||||
|
||||
实盘集成时若遇 **Anchor 2006、`NotAuthorized`(6000)、Token program 不匹配、6020/6042** 等,请参阅专门文档:**[Pump.fun 常见链上错误与处理思路(中文)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)**。
|
||||
|
||||
#### PumpFun:Creator Rewards Sharing(creator_vault)
|
||||
|
||||
部分 PumpFun 代币启用了 **Creator Rewards Sharing**,链上 `creator_vault` 可能与默认推导结果不同。若在**卖出**时复用**买入**时缓存的 params,可能触发程序错误 **2006(seeds constraint violated)**。建议:
|
||||
|
||||
- **来自 gRPC/事件(无需 RPC)**:`creator` 与 `creator_vault` 均可从解析后的事件中直接拿到:
|
||||
- **sol-parser-sdk**:推送前会调用 `fill_trade_accounts`,从 buy/sell 指令账户补全 `creator_vault`(buy 索引 9,sell 索引 8);`creator` 来自 TradeEvent 日志。用 `PumpFunParams::from_trade(..., e.creator, e.creator_vault, ...)` 或 `from_dev_trade(..., e.creator, e.creator_vault, ...)` 即可。
|
||||
- **solana-streamer**:指令解析时从 accounts[9](buy)/ accounts[8](sell)写入 `creator_vault`;`creator` 来自合并后的 CPI TradeEvent 日志。同样用事件的 `e.creator`、`e.creator_vault` 调用 `from_trade` / `from_dev_trade`。
|
||||
- **RPC 后覆盖**:若通过 `PumpFunParams::from_mint_by_rpc` 得到 params,之后又从 gRPC 拿到更新的 `creator_vault`,在卖出前对 params 调用 `.with_creator_vault(latest_creator_vault)`。
|
||||
|
||||
SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟);请从 gRPC/事件中传入最新 vault。
|
||||
|
||||
#### PumpSwap:从事件拿 coin_creator_vault(无需 RPC)
|
||||
|
||||
**PumpSwap**(Pump AMM)的 buy/sell 指令需要 `coin_creator_vault_ata` 与 `coin_creator_vault_authority`,二者均可从解析事件中拿到,无需 RPC:
|
||||
|
||||
- **sol-parser-sdk**:指令解析从账户 17、18 写入;若事件来自日志,账户填充器也会从指令补全。用 `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` 即可。
|
||||
- **solana-streamer**:指令解析从 `accounts.get(17)`、`accounts.get(18)` 写入。同样用事件的 `coin_creator_vault_ata`、`coin_creator_vault_authority` 调用 `from_trade`。
|
||||
|
||||
### Pump.fun Bonding Curve v2(buy_v2 / sell_v2 / buy_exact_quote_in_v2)
|
||||
|
||||
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
|
||||
|
||||
**v2 指令关键变化:**
|
||||
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint
|
||||
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
|
||||
- `buyback_fee_recipient`、`sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
|
||||
- SOL 配对币的报价和成本与旧版一致,无额外开销
|
||||
|
||||
**使用方式:**
|
||||
|
||||
设置 `PumpFunParams` 的 `quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
// SOL 配对币 — 传包装 SOL mint
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
|
||||
|
||||
// USDC 配对币(即将开放 — 必须使用 v2)
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(USDC_TOKEN_ACCOUNT);
|
||||
|
||||
// 之后正常交易
|
||||
client.buy(buy_params).await?;
|
||||
client.sell(sell_params).await?;
|
||||
```
|
||||
|
||||
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|
||||
|-----------|-------------|---------|------|
|
||||
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
|
||||
| `WSOL_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
|
||||
| `USDC_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) |
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
||||
|
||||
- **Jito**: 高性能区块空间
|
||||
- **ZeroSlot**: 零延迟交易
|
||||
- **Temporal**: 时间敏感交易
|
||||
- **Bloxroute**: 区块链网络加速
|
||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://doc.flashblock.trade/)
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://blockrazor.gitbook.io/blockrazor/)
|
||||
- **Node1**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://node1.me/docs.html)
|
||||
- **Astralane**: 高速交易执行,支持 API 密钥认证
|
||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC)
|
||||
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
## sol-trade-sdk v4.0.2
|
||||
|
||||
This release focuses on QUIC reliability and low-latency submission stability for Astralane.
|
||||
|
||||
### Highlights
|
||||
|
||||
- Fixed Astralane QUIC address-family mismatch that could produce `invalid remote address` when DNS returned IPv6 first and local endpoint was IPv4-only.
|
||||
- Added remote-family-aware local QUIC bind selection:
|
||||
- IPv4 remote -> bind `0.0.0.0:0`
|
||||
- IPv6 remote -> bind `[::]:0`
|
||||
- Added Astralane direct-IP candidate support (official region IPs), with IPv4-first selection for better QUIC stability.
|
||||
- Added automatic endpoint failover and reconnect rotation across candidate addresses, reducing single-endpoint/DNS variance impact.
|
||||
- Kept existing SDK interfaces compatible while improving submit-path resiliency.
|
||||
|
||||
### Also included from recent updates
|
||||
|
||||
- BlockRazor gRPC endpoint fixes and gRPC default transport behavior improvements (v4.0.1).
|
||||
- SWQOS transport path hardening and Binary-Tx response handling improvements (v4.0.0).
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# sol-trade-sdk 代码审查报告
|
||||
|
||||
审查维度:**逻辑准确性**、**可读性**、**模块化**、**超低延迟**、**代码质量**、**安全性**。
|
||||
|
||||
---
|
||||
|
||||
## 1. 代码逻辑准确性
|
||||
|
||||
### 1.1 Instruction 与 IDL / 官方行为
|
||||
|
||||
| 模块 | 结论 | 说明 |
|
||||
|------|------|------|
|
||||
| PumpFun buy/sell | ✅ 一致 | 账户顺序、discriminator、track_volume 与 `idl/pump.json` 一致;cashback 时 remainingAccounts 顺序正确 |
|
||||
| PumpSwap buy/sell | ✅ 一致 | 与 `idl/pump_amm.json` 一致;sell cashback 使用 quote_mint ATA |
|
||||
| PDA 推导 | ✅ 一致 | bonding_curve_v2、pool_v2、user_volume_accumulator、creator_vault 等 seeds 与官方一致 |
|
||||
|
||||
### 1.2 需修正的逻辑/风格
|
||||
|
||||
- **`src/instruction/utils/pumpswap.rs` 约 258、291 行**:`let program_id: &Pubkey = &&accounts::AMM_PROGRAM` 为双重引用,易误导且多余。建议改为 `&accounts::AMM_PROGRAM`。
|
||||
|
||||
---
|
||||
|
||||
## 2. 代码可读性
|
||||
|
||||
### 2.1 命名与注释
|
||||
|
||||
- 多数模块有中英文注释,instruction 与 IDL 的对应关系有标注。
|
||||
- **建议**:`src/instruction/pumpfun.rs` 约 259 行 sell 的 discriminator 使用魔法数组 `[51, 230, 133, ...]`,建议改为 `SELL_DISCRIMINATOR` 常量(与 buy 路径一致)。
|
||||
|
||||
### 2.2 错误信息与文案
|
||||
|
||||
- **建议**:`src/lib.rs` 中 “Current version only support” 应为 “only supports”;类似拼写/语法可统一检查。
|
||||
|
||||
### 2.3 过长函数
|
||||
|
||||
- **建议**:`src/lib.rs` 的 `ensure_wsol_ata` 可拆为「入口 + 重试循环」与「单次尝试 + 结果判断」,便于单测和阅读。
|
||||
|
||||
---
|
||||
|
||||
## 3. 模块化
|
||||
|
||||
### 3.1 职责与分层
|
||||
|
||||
- **instruction**:按协议分(pumpfun / pumpswap / bonk / raydium_* 等),实现 `InstructionBuilder`,边界清晰。
|
||||
- **instruction/utils**:PDA、常量、类型、池子解析与上层「组指令」分工明确。
|
||||
- **swqos**:按提供商分模块,common 放序列化、确认轮询、HTTP 客户端,无循环依赖。
|
||||
- **结论**:分层合理,模块化良好。
|
||||
|
||||
---
|
||||
|
||||
## 4. 超低延迟
|
||||
|
||||
### 4.1 必须改:避免多余 clone
|
||||
|
||||
| 位置 | 问题 | 建议 |
|
||||
|------|------|------|
|
||||
| `src/instruction/pumpswap.rs` 约 232、429 行 | `Instruction { accounts: accounts.clone(), data }` 对已拥有的 `Vec<AccountMeta>` 做完整 clone | 改为直接移动:`Instruction { program_id, accounts, data }`,不再 clone |
|
||||
|
||||
### 4.2 建议
|
||||
|
||||
- 若多路 SWQOS 并发发**同一笔**交易,可在调用方序列化一次,再传 `&[u8]` 给各 client,减少重复 bincode 序列化。
|
||||
- 热点路径未见不必要的 `Mutex`/`RwLock` 竞争,当前设计可接受。
|
||||
|
||||
---
|
||||
|
||||
## 5. 代码质量
|
||||
|
||||
### 5.1 必须改:避免 panic 的 unwrap
|
||||
|
||||
以下 PDA 或关键 `Option` 使用 `.unwrap()`,在异常输入下会直接 panic,建议改为 `Result` 并向上传播错误:
|
||||
|
||||
| 文件 | 行号(约) | 说明 |
|
||||
|------|------------|------|
|
||||
| `src/instruction/pumpfun.rs` | 67, 101, 149, 221, 291, 295 | `get_bonding_curve_pda`、`get_user_volume_accumulator_pda`、`get_bonding_curve_v2_pda` |
|
||||
| `src/instruction/pumpswap.rs` | 184, 198, 385, 407 | `get_user_volume_accumulator_pda`、`get_pool_v2_pda` |
|
||||
| `src/instruction/utils/pumpfun.rs` | 229 | `DEFAULT_CREATOR_VAULT.unwrap()`(LazyLock 未初始化时可能 panic) |
|
||||
| `src/instruction/bonk.rs` | 多处 | `get_pool_pda`、`get_vault_pda`、`params.rpc.as_ref().unwrap()` |
|
||||
| `src/instruction/utils/bonk.rs` | 110–116, 148–152 | `checked_*` 链后 `.unwrap()`,数学假设不成立会 panic |
|
||||
| `src/instruction/raydium_cpmm.rs` | 46, 106, 189, 250 | PDA / 状态相关 unwrap |
|
||||
| `src/instruction/utils/raydium_cpmm.rs` | 83, 85, 141 | `get_vault_pda(...).unwrap()` |
|
||||
|
||||
**建议**:统一改为 `.ok_or_else(|| anyhow!("..."))?` 或返回 `Result`,在调用链顶层处理错误,避免进程退出。
|
||||
|
||||
### 5.2 建议
|
||||
|
||||
- **测试**:为 instruction 构建(或至少 PDA + discriminator/data 布局)增加单元测试,固定输入与预期 bytes/accounts 比对,便于 IDL 升级时回归。
|
||||
- **错误类型**:`claim_cashback_*` 等返回 `Option<Instruction>`;可考虑统一为 `Result<Instruction>` 并带“无法构建”原因,或在文档中明确 None 的语义。
|
||||
|
||||
---
|
||||
|
||||
## 6. 安全性
|
||||
|
||||
### 6.1 必须改:API key 不得写入日志
|
||||
|
||||
| 位置 | 问题 | 建议 |
|
||||
|------|------|------|
|
||||
| `src/swqos/astralane_quic.rs` 约 61 行 | `info!(..., "api_key as CN: {}", api_key)` | 移除 api_key 或改为占位(如 `***` / 仅长度) |
|
||||
| `src/swqos/astralane_quic.rs` 约 74 行 | `info!(..., "Connected at {} (api_key: {})", addr, api_key)` | 同上 |
|
||||
|
||||
### 6.2 必须改:SkipServerVerification 风险
|
||||
|
||||
| 位置 | 问题 | 建议 |
|
||||
|------|------|------|
|
||||
| `src/swqos/astralane_quic.rs` 约 179–181 行 | `with_custom_certificate_verifier(SkipServerVerification)` 完全跳过服务端证书校验 | 1)若服务端提供证书:用 `RootCertStore` 或固定证书做校验;2)若仅 dev/内网:用 feature 或配置限制,并在文档/日志中明确“仅受控环境使用”;3)默认/生产构建建议不跳过校验 |
|
||||
|
||||
### 6.3 建议
|
||||
|
||||
- **敏感配置**:确保生产环境从环境变量或安全配置读取 API key,并在文档中说明。
|
||||
- **依赖**:定期执行 `cargo audit` 与依赖升级。
|
||||
- **unsafe**:`perf/hardware_optimizations.rs`、`realtime_tuning.rs` 中的 `unsafe` 使用范围可控,需保持注释中的安全约定。
|
||||
|
||||
---
|
||||
|
||||
## 7. 三库联动与超低延迟检查(sol-trade-sdk / sol-parser-sdk / solana-streamer)
|
||||
|
||||
### 7.1 逻辑一致性(已确认)
|
||||
|
||||
| 检查项 | sol-parser-sdk | solana-streamer | 说明 |
|
||||
|--------|----------------|-----------------|------|
|
||||
| Pump buy 账户数 | 16(fill 用 get(9) 填 creator_vault) | 16,accounts[9]=creator_vault | 与 idl/pumpfun.json 一致 |
|
||||
| Pump sell 账户数 | 14(get(8)=creator_vault) | 14,accounts[8]=creator_vault | 一致 |
|
||||
| PumpSwap buy 17/18 | 指令解析 + fill_buy_accounts get(17)/get(18) | 指令解析 accounts.get(17)/get(18) | coin_creator_vault_ata/authority 正确 |
|
||||
| PumpSwap sell 17/18 | fill_sell_accounts 同左 | 同左 | 一致 |
|
||||
| 填充顺序 | 先 parse(log 或 instruction)→ fill_accounts → push | 指令解析直接写 17/18;无单独 fill 步骤 | 两者均保证事件带齐 creator_vault / coin_creator_vault |
|
||||
| find_instruction_invoke | 选「账户数最多」的 invoke,保证取到 outer buy/sell | N/A(按当前 instruction 解析) | 正确 |
|
||||
|
||||
### 7.2 版本化交易账户解析
|
||||
|
||||
- **sol-parser-sdk**:`get_instruction_account_getter` 正确支持 versioned tx:先 `account_keys`,再 `loaded_writable_addresses`,再 `loaded_readonly_addresses`,与 Solana 约定一致。
|
||||
- **solana-streamer**:指令的 `accounts` 为索引,通过 `accounts.get(idx as usize).copied()` 从完整 `accounts: &[Pubkey]` 解析;调用方需传入已包含 loaded 的完整账户列表,否则高索引会得到 `default()`。
|
||||
|
||||
### 7.3 超低延迟相关
|
||||
|
||||
| 项目 | 状态 / 建议 |
|
||||
|------|-------------|
|
||||
| solana-streamer 热路径 | 已改为**顺序执行** inner 解析与 swap_data 提取,去掉 `thread::scope` + 双 spawn/join,减少 μs 级开销。 |
|
||||
| sol-parser-sdk | log 与 instruction 并行(rayon::join);fill 仅在有 invoke 时做;`find_instruction_invoke` 为 O(invokes),单程序单 tx 下可接受。 |
|
||||
| sol-trade-sdk | 见上文第 4 节;PumpSwap instruction 构建避免 `accounts.clone()` 已列为必须改。 |
|
||||
|
||||
### 7.4 建议
|
||||
|
||||
- **solana-streamer**:若 gRPC 上游已提供完整 `accounts`(含 loaded),可避免对每笔 tx 做 `accounts.to_vec()`,仅在需要 resize 时克隆,进一步降低分配。
|
||||
- **三库**:保持 IDL 与账户索引注释同步(pumpfun.json / pump_amm.json),避免后续扩展时索引错位。
|
||||
|
||||
---
|
||||
|
||||
## 8. 汇总:必须改 vs 建议改
|
||||
|
||||
### 必须改(优先处理)
|
||||
|
||||
| 序号 | 项 | 位置 |
|
||||
|------|----|------|
|
||||
| 1 | 移除 astralane_quic 中 API key 的日志输出 | `src/swqos/astralane_quic.rs` 61、74 行 |
|
||||
| 2 | SkipServerVerification:改为证书校验或仅限 dev 并文档化 | `src/swqos/astralane_quic.rs` 179–181 行 |
|
||||
| 3 | instruction 中 PDA 等 `.unwrap()` 改为 `Result` 并传播错误 | pumpfun.rs、pumpswap.rs、pumpfun/utils、bonk、raydium_cpmm 等 |
|
||||
| 4 | PumpSwap 构建 instruction 时避免 `accounts.clone()`,改为移动 | `src/instruction/pumpswap.rs` 232、429 行 |
|
||||
|
||||
### 建议改(可分批)
|
||||
|
||||
| 序号 | 项 | 位置 |
|
||||
|------|----|------|
|
||||
| 5 | PDA 的 `program_id` 从 `&&AMM_PROGRAM` 改为 `&AMM_PROGRAM` | `src/instruction/utils/pumpswap.rs` 258、291 行 |
|
||||
| 6 | PumpFun sell discriminator 改为命名常量 | `src/instruction/pumpfun.rs` 约 259 行 |
|
||||
| 7 | `ensure_wsol_ata` 拆分;修正 “only support” 等文案 | `src/lib.rs` |
|
||||
| 8 | 为 instruction 构建与 PDA 增加单元测试 | 新建 tests 或模块下 |
|
||||
| 9 | 生产日志用 tracing 替代 println!/eprintln! | `src/swqos/astralane.rs` 等 |
|
||||
|
||||
---
|
||||
|
||||
*报告基于当前仓库与 IDL 的静态阅读;若官方 SDK 或链上程序有未公开变更,建议再与官方实现或链上行为做一次对照验证。*
|
||||
@@ -0,0 +1,208 @@
|
||||
# Pump.fun(Bonding Curve)常见链上错误与处理思路
|
||||
|
||||
本文档面向使用 **sol-trade-sdk** 组装 Pump.fun Program(`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`)买卖交易的集成方,汇总**实战中高频**的失败形态、日志特征,以及与 SDK 参数的对应关系和**推荐处理方式**。
|
||||
|
||||
> 自定义错误码以仓库内 `idl/pump.json`、`idl/pump_fees.json` 为准;**2006** 等部分错误来自 **Anchor 框架**,不在 Pump 自定义枚举里。
|
||||
|
||||
---
|
||||
|
||||
## 1. Anchor `2006` / `ConstraintSeeds`(`creator_vault`)
|
||||
|
||||
### 现象
|
||||
|
||||
- Solana Explorer / `simulateTransaction`:**Program Error: custom program error: 2006**,或 Anchor 文案 **A seeds constraint was violated**。
|
||||
- 日志里常见于账户 **`creator_vault`**:打印 **Left**(你传入的 pubkey)与 **Right**(程序按当前 curve 推导的 PDA)。
|
||||
|
||||
### 含义
|
||||
|
||||
Pump 校验 `creator_vault` 必须满足程序的 **PDAs seeds**(与 bonding curve 上记录的 **creator / fee-sharing 布局**一致)。传入地址与程序期望不一致即失败。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. **`bonding_curve.creator` 与链上不符**:事件/缓存里用的是 create 交易的 `creator` 字段、`user`,或陈旧快照,与 curve 账户内真实 creator 不一致;据此推导或缓存的 vault 会错。
|
||||
2. **买单侧「旧 vault」被沿用到卖单**:买入时 ix 里的 `creator_vault` 在后续 trade 语境下已不再与程序约束一致(例如 creator / sharing 语义在链上演进),卖单仍填旧地址 → **Left ≠ Right**。
|
||||
3. **Creator Rewards / pump-fees 分成布局**:部分 mint 使用 `sharing-config` 相关 seeds,单靠 `PDA(["creator-vault"], bonding_curve.creator)` 不足以覆盖全部历史状态;Stale 的 offline hint 也会把 resolve 引向错误 vault。
|
||||
4. **Phantom vault**:历史上若用 `Pubkey::default()` 推导出的占位 vault(SDK 常量 `phantom_default_creator_vault`),链上必定失败。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **买入 / 卖出指令**(同一套解析) | 使用 `resolve_creator_vault_for_ix_with_fee_sharing`(`src/instruction/utils/pumpfun.rs`):有 **非 default、非 phantom** 的 ix / 解析器回填 **`creator_vault` 时原样采用**(**不会**再根据 `creator` 做 `get_creator_vault_pda` 覆盖费分成等非传统布局);**未传 ix vault** 时依次:`fee_sharing_creator_vault_if_active` hint → `PDA(effective_creator)`。`PumpFunInstructionBuilder` 买卖均走此逻辑。 |
|
||||
| **权威对齐** | 低延迟路径外,可 **`PumpFunParams::from_mint_by_rpc`** 或由解析器 **`fill_trade_accounts`**(如 sol-parser-sdk)持续刷新 `creator` / `creator_vault`;必要时对 **fee-sharing** 使用 `fetch_fee_sharing_creator_vault_if_active` / `refresh_fee_sharing_creator_vault_from_rpc`。 |
|
||||
| **bonding_curve 账户地址** | 指令构建时使用 **`get_bonding_curve_pda(mint)`** 作为 canonical bonding curve pubkey,避免缓存中的曲线地址错位导致读到错误 **creator**。 |
|
||||
|
||||
集成方若在 **bot** 侧维护持仓快照,建议在**每笔**解析到的 Pump trade 后刷新缓存/仓位中的 `creator` 与 `creator_vault`,避免「只写一次建仓快照」。
|
||||
|
||||
---
|
||||
|
||||
## 2. Pump `6000` `NotAuthorized`(常见:`feeRecipient`)
|
||||
|
||||
### 现象
|
||||
|
||||
- 日志:`AnchorError thrown in programs/pump/src/fee_recipient.rs` 或 **`Error Code: NotAuthorized`**(与 Global 授权的 fee recipient 池有关)。
|
||||
|
||||
### 含义
|
||||
|
||||
账户 **#2 fee recipient** 不是当前 Pump **Global / 协议**允许使用的收款地址之一,或与 **Mayhem / 非 Mayhem** 池不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. 使用了**过期**或**错误池**的 fee recipient(静态列表落后于主网轮换)。
|
||||
2. **`mayhem_mode` 与 `fee_recipient` 不匹配**:声明 Mayhem 却传普通池地址,或相反(见 `reconcile_mayhem_mode_for_trade`,`src/instruction/utils/pumpfun.rs`)。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **优先事件** | 使用 gRPC / 解析器里的 **`tradeEvent.feeRecipient`** 或同笔 **create_v2 + buy** 观测到的 fee recipient。 |
|
||||
| **纠偏** | `PumpFunParams::from_trade` 会对 `mayhem_mode` 与 `fee_recipient` 做池一致性纠偏;发单前若事件缺省,可走 `pump_fun_fee_recipient_meta`(按 `is_mayhem_mode` 从静态池选)。 |
|
||||
| **提交前保留观测值** | 不要无条件把已观测的 `fee_recipient` 清成 default;否则会退回 SDK 内置静态池,静态池若落后于主网 Global 授权,仍可能触发 6000。只有缺少观测值时才让 builder 兜底。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. SPL:Token **`initializeAccount3`**——`incorrect program id for instruction`
|
||||
|
||||
### 现象
|
||||
|
||||
- 内联指令里 **`Token Program: initializeAccount3`**(或 Token-2022 等价指令)报错 **`incorrect program id for instruction`**。
|
||||
|
||||
### 含义
|
||||
|
||||
为 **Mint** 创建用户 ATA 时,使用的 **token program(Legacy SPL vs Token-2022)** 与 **Mint 的实际 owner(`mint.owner`)** 不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. Pump 新发多为 **Token-2022**,但代码写死 **`Tokenkeg…`**。
|
||||
2. 少数 Legacy mint(`Tokenkeg…`),却被强制按 Token-2022 建账。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::token_program`** 必须与非 default 的 **mint owner** 一致;从 **gRPC / 解析结果** 带入,**勿**在已明确 program 时再用「仅按 `.pump` 后缀猜 Token-2022」覆盖(业务层若做后缀启发,应仅在 `token_program == default` 时生效)。
|
||||
|
||||
---
|
||||
|
||||
## 4. Pump `6020` `BuyZeroAmount`
|
||||
|
||||
### 现象
|
||||
|
||||
- `buy` / `buy_exact_sol_in` 报 **Buy zero amount**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- `min_tokens_out == 0`(或等价路径算出可买 **0 枚**),协议直接拒绝。
|
||||
- 使用 **Create / Shred** 事件构造曲线时 **virtual / real 储备全 0**,本地定价算出 **0**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 对「首买 / 无储备」场景用 **`PumpFunParams::from_dev_trade`** 或按协议初值回填虚拟储备(与 `global_constants` 一致),再算 **`min_tokens_out`**。
|
||||
- 适当 **放宽买入滑点**(`slippage_basis_points`),避免估算代币量略小于链上。
|
||||
|
||||
---
|
||||
|
||||
## 5. Pump `6042` `BuySlippageBelowMinTokensOut`
|
||||
|
||||
### 现象
|
||||
|
||||
- 文案:**Slippage: Would buy less tokens than expected min_tokens_out**。
|
||||
|
||||
### 含义
|
||||
|
||||
链上实际可成交代币数量 **小于** 指令参数 **`min_tokens_out`**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 市价波动、SOL 竞价导致曲线状态与本地快照不一致。
|
||||
- 本地 **`get_buy_token_amount_from_sol_amount`** 所用 **creator / 费率假设**与链上 **pfee CPI**(动态费率)不一致,**预估偏多**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **提高滑点容忍**(或降低 **`min_tokens_out`**)。
|
||||
- 尽量用 **较新**的 **virtual/real reserves**(来自最近一次 trade 解析或简短 RPC)。
|
||||
- 若运行在 **狙击手**等极端延迟场景,需接受:**保守的 min_out**(更大滑点)换成功率。
|
||||
|
||||
---
|
||||
|
||||
## 6. Pump `6024` `Overflow` 与其它算术类错误
|
||||
|
||||
### 现象
|
||||
|
||||
- `6024 Overflow`、`6025 Truncation`、`6026 DivisionByZero`(见 IDL)。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 指令参数 **`amount` / SOL / lamports** 与曲线状态组合不合法(例如极端大卖、或为 0 与后续计算冲突)。
|
||||
- SDK 外传入了 **不合理的储备快照**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 校验 **买入/卖出金额 > 0**、与余额一致。
|
||||
- 使用 **`from_mint_by_rpc`** 或与链一致的储备后再算 **`min_sol_output` / `min_tokens_out`**。
|
||||
|
||||
---
|
||||
|
||||
## 7. Pump `6027` `NotEnoughRemainingAccounts`(返现等)
|
||||
|
||||
### 现象
|
||||
|
||||
- 返现代币等路径要求 **remaining accounts**(例如 `UserVolumeAccumulator`),数量不足。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- **`is_cashback_coin`(或等价标志)为 true**,但组装指令时 **未追加**所需账户。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::from_trade` / `from_dev_trade`** 传入正确的 **`is_cashback_coin`**(来自事件)。
|
||||
- README 中与 **Cashback** 章节一致:**事件路径必须带标志**,不能默认 false。
|
||||
|
||||
---
|
||||
|
||||
## 8. Pump `6022` `SellZeroAmount`
|
||||
|
||||
### 含义
|
||||
|
||||
卖出代币数量为 **0**。在业务层过滤即可。
|
||||
|
||||
---
|
||||
|
||||
## 9. 与 pump-fees / Creator 迁移相关的错误(`6049`–`6053` 等)
|
||||
|
||||
IDL 中例如:
|
||||
|
||||
- **`6049` `CreatorMigratedToSharingConfig`**
|
||||
- **`6050` `UnableToDistributeCreatorVaultMigratedToSharingConfig`**
|
||||
- **`6053` `BondingCurveAndSharingConfigCreatorMismatch`**
|
||||
|
||||
### 思路
|
||||
|
||||
这些是 **creator / fee-sharing** 生命周期中的**专用分支**,与一般买卖路径不同。若仿真或清算类指令触发:
|
||||
|
||||
- 以 **Pump / pump-fees 官方文档** 为准使用 **`distribute_creator_fees`**、**reset_fee_sharing_config** 等;
|
||||
- **`creator_vault` resolve** 需结合 **`fetch_fee_sharing_creator_vault_if_active`** 与链上 **`SharingConfig` 状态**,避免离线 deduce 过时。
|
||||
|
||||
---
|
||||
|
||||
## 10. 调试清单(推荐给集成方)
|
||||
|
||||
1. **记下失败指令索引** + **Simulation / explorer 展开的账户列表**,重点核对:**mint、bonding_curve、associated_bonding_curve、creator_vault、fee_recipient、token_program**。
|
||||
2. **对比 Anchor 日志里的 Left / Right**(针对 2006)与本地 `PumpFunParams` 打印是否一致。
|
||||
3. **`mint.owner`** 与 **`PumpFunParams.token_program`** 是否一致。
|
||||
4. **`bonding_curve` 地址**是否与 **`get_bonding_curve_pda(mint)`** 一致。
|
||||
5. **`mayhem_mode` ↔ `fee_recipient`** 是否同池。
|
||||
6. 低延迟不足以覆盖 **creator 演进** 时,是否在卖前引入了 **RPC 或最新 trade** 刷新。
|
||||
|
||||
---
|
||||
|
||||
## 参考代码入口(本仓库)
|
||||
|
||||
| 主题 | 路径 |
|
||||
|------|------|
|
||||
| Buy / Sell vault resolve(显式 ix `creator_vault` → `fee_sharing` hint → `PDA(effective_creator)`) | `src/instruction/utils/pumpfun.rs` — `resolve_creator_vault_for_ix_with_fee_sharing`;`effective_creator_for_trade` 见 `src/trading/core/params/pumpfun.rs` |
|
||||
| Fee recipient / Mayhem | `src/instruction/utils/pumpfun.rs` — `pump_fun_fee_recipient_meta`, `reconcile_mayhem_mode_for_trade` |
|
||||
| 指令构建 | `src/instruction/pumpfun.rs` — `PumpFunInstructionBuilder` |
|
||||
| Params | `src/trading/core/params/pumpfun.rs` — `PumpFunParams::{from_trade, from_dev_trade, from_mint_by_rpc, refresh_fee_sharing_creator_vault_from_rpc}` |
|
||||
|
||||
---
|
||||
|
||||
如需英文版或对 PumpSwap/其它 DEX 的同类文档,可在 `docs/` 下按相同结构扩展。
|
||||
@@ -30,7 +30,7 @@ The `TradeBuyParams` struct contains all parameters required for executing buy o
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | Address lookup table for transaction optimization |
|
||||
| `wait_transaction_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
|
||||
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
|
||||
| `create_input_token_ata` | `bool` | ✅ | Whether to create input token Associated Token Account |
|
||||
| `close_input_token_ata` | `bool` | ✅ | Whether to close input token ATA after transaction |
|
||||
| `create_mint_ata` | `bool` | ✅ | Whether to create token mint ATA |
|
||||
@@ -62,7 +62,7 @@ The `TradeSellParams` struct contains all parameters required for executing sell
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | Address lookup table for transaction optimization |
|
||||
| `wait_transaction_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
|
||||
| `wait_tx_confirmed` | `bool` | ✅ | Whether to wait for transaction confirmation |
|
||||
| `create_output_token_ata` | `bool` | ✅ | Whether to create output token Associated Token Account |
|
||||
| `close_output_token_ata` | `bool` | ✅ | Whether to close output token ATA after transaction |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | Durable nonce information containing nonce account and current nonce value |
|
||||
@@ -88,7 +88,7 @@ These parameters are essential for defining the basic trading operation:
|
||||
These parameters control how the transaction is processed:
|
||||
|
||||
- **slippage_basis_points**: Controls acceptable price slippage
|
||||
- **wait_transaction_confirmed**: Controls whether to wait for confirmation
|
||||
- **wait_tx_confirmed**: Controls whether to wait for confirmation
|
||||
|
||||
### 🔧 Account Management Parameters
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `address_lookup_table_account` | `Option<Pubkey>` | ❌ | 用于交易优化的地址查找表 |
|
||||
| `wait_transaction_confirmed` | `bool` | ✅ | 是否等待交易确认 |
|
||||
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
|
||||
| `create_input_token_ata` | `bool` | ✅ | 是否创建输入代币关联代币账户 |
|
||||
| `close_input_token_ata` | `bool` | ✅ | 交易后是否关闭输入代币 ATA |
|
||||
| `create_mint_ata` | `bool` | ✅ | 是否创建代币 mint ATA |
|
||||
@@ -62,7 +62,7 @@
|
||||
| 参数 | 类型 | 必需 | 描述 |
|
||||
|------|------|------|------|
|
||||
| `address_lookup_table_account` | `Option<AddressLookupTableAccount>` | ❌ | 用于交易优化的地址查找表 |
|
||||
| `wait_transaction_confirmed` | `bool` | ✅ | 是否等待交易确认 |
|
||||
| `wait_tx_confirmed` | `bool` | ✅ | 是否等待交易确认 |
|
||||
| `create_output_token_ata` | `bool` | ✅ | 是否创建输出代币关联代币账户 |
|
||||
| `close_output_token_ata` | `bool` | ✅ | 交易后是否关闭输出代币 ATA |
|
||||
| `durable_nonce` | `Option<DurableNonceInfo>` | ❌ | 持久 nonce 信息,包含 nonce 账户和当前 nonce 值 |
|
||||
@@ -88,7 +88,7 @@
|
||||
这些参数控制交易的处理方式:
|
||||
|
||||
- **slippage_basis_points**: 控制可接受的价格滑点
|
||||
- **wait_transaction_confirmed**: 控制是否等待确认
|
||||
- **wait_tx_confirmed**: 控制是否等待确认
|
||||
|
||||
### 🔧 账户管理参数
|
||||
|
||||
|
||||
@@ -16,7 +16,10 @@ use sol_trade_sdk::common::{GasFeeStrategy, TradeConfig};
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -63,7 +66,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
if let Some(event) = queue.pop() {
|
||||
let run = match &event {
|
||||
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
DexEvent::PumpFunBuy(e)
|
||||
| DexEvent::PumpFunSell(e)
|
||||
| DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
Some(e.clone())
|
||||
} else {
|
||||
@@ -104,7 +109,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -123,7 +135,9 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
|
||||
let lookup_table_key = Pubkey::from_str("use_your_lookup_table_key_here").unwrap();
|
||||
let address_lookup_table_account =
|
||||
fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key).await.ok();
|
||||
fetch_address_lookup_table_account(&client.infrastructure.rpc, &lookup_table_key)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let gas_fee_strategy = GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
@@ -150,6 +164,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
trade_info.is_cashback_coin,
|
||||
Some(trade_info.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -161,6 +176,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -10,7 +10,10 @@ use sol_trade_sdk::common::{
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{BonkParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{BonkParams, DexParamEnum},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -110,7 +113,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -127,14 +137,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(
|
||||
150000,
|
||||
150000,
|
||||
500000,
|
||||
500000,
|
||||
0.001,
|
||||
0.001,
|
||||
);
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from Bonk...");
|
||||
@@ -176,6 +179,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -3,7 +3,10 @@ use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{BonkParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{BonkParams, DexParamEnum},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -78,7 +81,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -95,14 +105,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(
|
||||
150000,
|
||||
150000,
|
||||
500000,
|
||||
500000,
|
||||
0.001,
|
||||
0.001,
|
||||
);
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
|
||||
let token_type = if trade_info.quote_token_mint == sol_trade_sdk::constants::USD1_TOKEN_ACCOUNT
|
||||
{
|
||||
@@ -144,6 +147,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -12,7 +12,8 @@ use sol_trade_sdk::{
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{
|
||||
BonkParams, PumpFunParams, PumpSwapParams, RaydiumAmmV4Params, RaydiumCpmmParams, DexParamEnum,
|
||||
BonkParams, DexParamEnum, PumpFunParams, PumpSwapParams, RaydiumAmmV4Params,
|
||||
RaydiumCpmmParams,
|
||||
},
|
||||
factory::DexType,
|
||||
},
|
||||
@@ -531,7 +532,7 @@ async fn handle_buy(
|
||||
|
||||
let client = initialize_real_client().await?;
|
||||
|
||||
let (create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals) =
|
||||
let (create_mint_ata, use_seed, owner_pubkey, _amount_f64, _decimals) =
|
||||
check_mint_ata(&client, mint).await?;
|
||||
|
||||
match dex {
|
||||
@@ -565,7 +566,7 @@ async fn handle_buy_rv4(
|
||||
slippage: Option<u64>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = initialize_real_client().await?;
|
||||
let (create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals) =
|
||||
let (create_mint_ata, use_seed, owner_pubkey, _amount_f64, _decimals) =
|
||||
check_mint_ata(&client, mint).await?;
|
||||
handle_buy_raydium_v4(mint, amm, sol_amount, slippage, create_mint_ata, use_seed, owner_pubkey)
|
||||
.await?;
|
||||
@@ -579,7 +580,7 @@ async fn handle_buy_rcpmm(
|
||||
slippage: Option<u64>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = initialize_real_client().await?;
|
||||
let (create_mint_ata, use_seed, owner_pubkey, amount_f64, decimals) =
|
||||
let (create_mint_ata, use_seed, owner_pubkey, _amount_f64, _decimals) =
|
||||
check_mint_ata(&client, mint).await?;
|
||||
handle_buy_raydium_cpmm(
|
||||
mint,
|
||||
@@ -599,8 +600,8 @@ async fn handle_buy_pumpfun(
|
||||
sol_amount: f64,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("🔥 BUY PUMPFUN COMMAND");
|
||||
println!(" Token Mint: {}", mint);
|
||||
@@ -635,6 +636,7 @@ async fn handle_buy_pumpfun(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -655,7 +657,7 @@ async fn handle_buy_pumpswap(
|
||||
sol_amount: f64,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = initialize_real_client().await?;
|
||||
@@ -691,6 +693,7 @@ async fn handle_buy_pumpswap(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -710,8 +713,8 @@ async fn handle_buy_bonk(
|
||||
sol_amount: f64,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = initialize_real_client().await?;
|
||||
println!("🔥 BUY BONK COMMAND");
|
||||
@@ -721,7 +724,8 @@ async fn handle_buy_bonk(
|
||||
println!(" Slippage: {}%", slippage.unwrap());
|
||||
}
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let param = BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
|
||||
let param =
|
||||
BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let sol_lamports = sol_str_to_lamports(sol_amount.to_string().as_str()).unwrap();
|
||||
|
||||
@@ -746,6 +750,7 @@ async fn handle_buy_bonk(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -766,8 +771,8 @@ async fn handle_buy_raydium_v4(
|
||||
sol_amount: f64,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = initialize_real_client().await?;
|
||||
println!("🔥 BUY RAYDIUM V4 COMMAND");
|
||||
@@ -780,7 +785,8 @@ async fn handle_buy_raydium_v4(
|
||||
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let amm_pubkey = Pubkey::from_str(amm)?;
|
||||
let param = RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
|
||||
let param =
|
||||
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let sol_lamports = sol_str_to_lamports(sol_amount.to_string().as_str()).unwrap();
|
||||
|
||||
@@ -805,6 +811,7 @@ async fn handle_buy_raydium_v4(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -825,8 +832,8 @@ async fn handle_buy_raydium_cpmm(
|
||||
sol_amount: f64,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = initialize_real_client().await?;
|
||||
println!("🔥 BUY RAYDIUM CPMM COMMAND");
|
||||
@@ -839,7 +846,9 @@ async fn handle_buy_raydium_cpmm(
|
||||
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let pool_pubkey = Pubkey::from_str(pool_address)?;
|
||||
let param = RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey).await?;
|
||||
let param =
|
||||
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey)
|
||||
.await?;
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let sol_lamports = sol_str_to_lamports(sol_amount.to_string().as_str()).unwrap();
|
||||
|
||||
@@ -864,6 +873,7 @@ async fn handle_buy_raydium_cpmm(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -992,11 +1002,11 @@ async fn handle_sell_pumpfun(
|
||||
mint: &str,
|
||||
token_amount: Option<f64>,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_create_mint_ata: bool,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
amount_f64: f64,
|
||||
decimals: u8,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
|
||||
@@ -1053,11 +1063,11 @@ async fn handle_sell_pumpswap(
|
||||
mint: &str,
|
||||
token_amount: Option<f64>,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_create_mint_ata: bool,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
amount_f64: f64,
|
||||
decimals: u8,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
println!("🔥 SELL PUMPSWAP COMMAND");
|
||||
@@ -1097,7 +1107,7 @@ async fn handle_sell_pumpswap(
|
||||
match client.sell(sell_params).await {
|
||||
Ok((_, signature, _)) => {
|
||||
println!(" ✅ Successfully sold tokens from PumpSwap!");
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
println!(" ✅ Transaction Signature: {:?}", signature);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(" ❌ Failed to sell tokens from PumpSwap: {}", e);
|
||||
@@ -1111,11 +1121,11 @@ async fn handle_sell_bonk(
|
||||
mint: &str,
|
||||
token_amount: Option<f64>,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_create_mint_ata: bool,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
amount_f64: f64,
|
||||
decimals: u8,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
println!("🔥 SELL PUMPSWAP COMMAND");
|
||||
@@ -1126,7 +1136,8 @@ async fn handle_sell_bonk(
|
||||
}
|
||||
let client = initialize_real_client().await?;
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let param = BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
|
||||
let param =
|
||||
BonkParams::from_mint_by_rpc(&client.infrastructure.rpc, &mint_pubkey, false).await?;
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
@@ -1170,11 +1181,11 @@ async fn handle_sell_raydium_v4(
|
||||
mint: &str,
|
||||
token_amount: Option<f64>,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_create_mint_ata: bool,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
amount_f64: f64,
|
||||
decimals: u8,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
println!("🔥 SELL RAYDIUM V4 COMMAND");
|
||||
@@ -1187,7 +1198,8 @@ async fn handle_sell_raydium_v4(
|
||||
let client = initialize_real_client().await?;
|
||||
let amm_pubkey = Pubkey::from_str(amm)?;
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let param = RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
|
||||
let param =
|
||||
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, amm_pubkey).await?;
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
@@ -1231,11 +1243,11 @@ async fn handle_sell_raydium_cpmm(
|
||||
pool_address: &str,
|
||||
token_amount: Option<f64>,
|
||||
slippage: Option<u64>,
|
||||
create_mint_ata: bool,
|
||||
use_seed: bool,
|
||||
owner_pubkey: Pubkey,
|
||||
_create_mint_ata: bool,
|
||||
_use_seed: bool,
|
||||
_owner_pubkey: Pubkey,
|
||||
amount_f64: f64,
|
||||
decimals: u8,
|
||||
_decimals: u8,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let amount = if token_amount.is_some() { token_amount.unwrap() } else { amount_f64 };
|
||||
println!("🔥 SELL RAYDIUM CPMM COMMAND");
|
||||
@@ -1248,7 +1260,9 @@ async fn handle_sell_raydium_cpmm(
|
||||
let client = initialize_real_client().await?;
|
||||
let pool_pubkey = Pubkey::from_str(pool_address)?;
|
||||
let mint_pubkey = Pubkey::from_str(mint)?;
|
||||
let param = RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey).await?;
|
||||
let param =
|
||||
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_pubkey)
|
||||
.await?;
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
@@ -1383,7 +1397,14 @@ async fn initialize_real_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = RPC_URL.to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(payer, trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType, common::{
|
||||
AnyResult, TradeConfig, fast_fn::get_associated_token_address_with_program_id_fast_use_seed
|
||||
}, swqos::SwqosConfig, trading::{core::params::{MeteoraDammV2Params, DexParamEnum}, factory::DexType}
|
||||
common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
|
||||
},
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{DexParamEnum, MeteoraDammV2Params},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -32,7 +38,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::MeteoraDammV2(
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
|
||||
.await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -44,6 +51,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -54,7 +62,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM;
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(&payer, &mint_pubkey, &program_id, client.use_seed_optimize);
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
println!("Token balance: {}", amount_token);
|
||||
@@ -67,7 +80,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::MeteoraDammV2(
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool).await?,
|
||||
MeteoraDammV2Params::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool)
|
||||
.await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -94,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -3,7 +3,8 @@ use sol_trade_sdk::{
|
||||
common::{AnyResult, TradeConfig},
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{PumpSwapParams, DexParamEnum}, factory::DexType,
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
factory::DexType,
|
||||
InstructionMiddleware, MiddlewareManager,
|
||||
},
|
||||
SolanaTrade, TradeTokenType,
|
||||
@@ -30,7 +31,7 @@ impl InstructionMiddleware for CustomMiddleware {
|
||||
fn process_protocol_instructions(
|
||||
&self,
|
||||
protocol_instructions: Vec<Instruction>,
|
||||
_protocol_name: String,
|
||||
_protocol_name: &str,
|
||||
_is_buy: bool,
|
||||
) -> Result<Vec<Instruction>> {
|
||||
// do anything you want here
|
||||
@@ -41,7 +42,7 @@ impl InstructionMiddleware for CustomMiddleware {
|
||||
fn process_full_instructions(
|
||||
&self,
|
||||
full_instructions: Vec<Instruction>,
|
||||
_protocol_name: String,
|
||||
_protocol_name: &str,
|
||||
_is_buy: bool,
|
||||
) -> Result<Vec<Instruction>> {
|
||||
// do anything you want here
|
||||
@@ -62,7 +63,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -91,7 +99,8 @@ async fn test_middleware() -> AnyResult<()> {
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpSwap(
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_address).await?,
|
||||
PumpSwapParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &pool_address)
|
||||
.await?,
|
||||
),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -103,6 +112,7 @@ async fn test_middleware() -> AnyResult<()> {
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -16,7 +16,10 @@ use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -62,7 +65,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
if let Some(event) = queue.pop() {
|
||||
let run = match &event {
|
||||
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
DexEvent::PumpFunBuy(e)
|
||||
| DexEvent::PumpFunSell(e)
|
||||
| DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
Some(e.clone())
|
||||
} else {
|
||||
@@ -103,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -148,6 +160,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
trade_info.is_cashback_coin,
|
||||
Some(trade_info.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -159,6 +172,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
//!
|
||||
//! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。
|
||||
|
||||
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use sol_parser_sdk::grpc::{
|
||||
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||
@@ -16,7 +19,10 @@ use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -65,7 +71,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
loop {
|
||||
if let Some(event) = queue.pop() {
|
||||
let run = match &event {
|
||||
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
DexEvent::PumpFunBuy(e)
|
||||
| DexEvent::PumpFunSell(e)
|
||||
| DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
Some(e.clone())
|
||||
} else {
|
||||
@@ -102,16 +110,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||
}
|
||||
|
||||
async fn pumpfun_copy_trade(
|
||||
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||
) -> AnyResult<()> {
|
||||
async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
let mint_pubkey = e.mint;
|
||||
let slippage_basis_points = Some(100u64);
|
||||
@@ -143,6 +157,7 @@ async fn pumpfun_copy_trade(
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -154,6 +169,7 @@ async fn pumpfun_copy_trade(
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -192,6 +208,7 @@ async fn pumpfun_copy_trade(
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
//! 监听创建者首次买入(Create 后同笔/首笔 Buy,is_created_buy == true),
|
||||
//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。
|
||||
|
||||
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
use sol_parser_sdk::grpc::{
|
||||
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||
@@ -16,7 +19,10 @@ use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{PumpFunParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpFunParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -94,16 +100,22 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let rpc_url = std::env::var("RPC_URL")
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||
}
|
||||
|
||||
async fn pumpfun_sniper_trade(
|
||||
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||
) -> AnyResult<()> {
|
||||
async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
let mint_pubkey = e.mint;
|
||||
let slippage_basis_points = Some(300u64);
|
||||
@@ -135,6 +147,7 @@ async fn pumpfun_sniper_trade(
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -146,6 +159,7 @@ async fn pumpfun_sniper_trade(
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -178,6 +192,7 @@ async fn pumpfun_sniper_trade(
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
use sol_trade_sdk::{
|
||||
SolanaTrade, TradeTokenType, common::{
|
||||
AnyResult, TradeConfig, fast_fn::get_associated_token_address_with_program_id_fast_use_seed
|
||||
}, swqos::SwqosConfig, trading::{core::params::{PumpSwapParams, DexParamEnum}, factory::DexType}
|
||||
common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
|
||||
},
|
||||
swqos::SwqosConfig,
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -44,6 +50,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -54,7 +61,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let program_id = sol_trade_sdk::constants::TOKEN_PROGRAM_2022;
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(&payer, &mint_pubkey, &program_id, client.use_seed_optimize);
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
@@ -93,7 +105,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{PumpSwapParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -130,7 +133,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -152,7 +162,9 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
trade_info.base_token_program,
|
||||
trade_info.quote_token_program,
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -181,7 +193,9 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
trade_info.base_token_program,
|
||||
trade_info.quote_token_program,
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -230,6 +244,7 @@ async fn pumpswap_trade_with_grpc(
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -244,7 +259,12 @@ async fn pumpswap_trade_with_grpc(
|
||||
} else {
|
||||
params.quote_token_program
|
||||
};
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(&payer, &mint_pubkey, &program_id, client.use_seed_optimize);
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&program_id,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
|
||||
@@ -2,7 +2,10 @@ use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id
|
||||
use sol_trade_sdk::{
|
||||
common::AnyResult,
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{RaydiumAmmV4Params, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, RaydiumAmmV4Params},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade,
|
||||
};
|
||||
use sol_trade_sdk::{
|
||||
@@ -106,7 +109,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -122,8 +132,12 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let amm_info = fetch_amm_info(&client.infrastructure.rpc, trade_info.amm).await?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(&client.infrastructure.rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
let (coin_reserve, pc_reserve) = get_multi_token_balances(
|
||||
&client.infrastructure.rpc,
|
||||
&amm_info.token_coin,
|
||||
&amm_info.token_pc,
|
||||
)
|
||||
.await?;
|
||||
let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
{
|
||||
@@ -142,14 +156,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
);
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(
|
||||
150000,
|
||||
150000,
|
||||
500000,
|
||||
500000,
|
||||
0.001,
|
||||
0.001,
|
||||
);
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from Raydium_amm_v4...");
|
||||
@@ -174,6 +181,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -194,7 +202,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let params = RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm).await?;
|
||||
let params =
|
||||
RaydiumAmmV4Params::from_amm_address_by_rpc(&client.infrastructure.rpc, trade_info.amm)
|
||||
.await?;
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::RaydiumAmmV4,
|
||||
output_token_type: if is_wsol { TradeTokenType::WSOL } else { TradeTokenType::USDC },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::constants::{WSOL_TOKEN_ACCOUNT, USDC_TOKEN_ACCOUNT};
|
||||
use sol_trade_sdk::trading::core::params::{RaydiumCpmmParams, DexParamEnum};
|
||||
use sol_trade_sdk::constants::{USDC_TOKEN_ACCOUNT, WSOL_TOKEN_ACCOUNT};
|
||||
use sol_trade_sdk::trading::core::params::{DexParamEnum, RaydiumCpmmParams};
|
||||
use sol_trade_sdk::trading::factory::DexType;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
use sol_trade_sdk::{common::AnyResult, swqos::SwqosConfig, SolanaTrade};
|
||||
@@ -107,7 +107,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -132,9 +139,12 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
|
||||
let buy_params =
|
||||
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &trade_info.pool_state).await?;
|
||||
|
||||
let buy_params = RaydiumCpmmParams::from_pool_address_by_rpc(
|
||||
&client.infrastructure.rpc,
|
||||
&trade_info.pool_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let is_wsol = trade_info.input_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| trade_info.output_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
|
||||
@@ -159,6 +169,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -173,8 +184,11 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
|
||||
let sell_params =
|
||||
RaydiumCpmmParams::from_pool_address_by_rpc(&client.infrastructure.rpc, &trade_info.pool_state).await?;
|
||||
let sell_params = RaydiumCpmmParams::from_pool_address_by_rpc(
|
||||
&client.infrastructure.rpc,
|
||||
&trade_info.pool_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
|
||||
@@ -3,7 +3,10 @@ use sol_trade_sdk::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, AnyResult, TradeConfig,
|
||||
},
|
||||
swqos::SwqosConfig,
|
||||
trading::{core::params::{PumpSwapParams, DexParamEnum}, factory::DexType},
|
||||
trading::{
|
||||
core::params::{DexParamEnum, PumpSwapParams},
|
||||
factory::DexType,
|
||||
},
|
||||
SolanaTrade, TradeTokenType,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
@@ -47,6 +50,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -104,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use sol_trade_sdk::{
|
||||
common::{AnyResult, InfrastructureConfig, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
TradingClient, TradingInfrastructure,
|
||||
AstralaneTransport, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -46,19 +46,22 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
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::Node1("your_api_token".to_string(), SwqosRegion::Frankfurt, None, None),
|
||||
SwqosConfig::BlockRazor("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Astralane("your_api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Astralane(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC;None / Some(Binary) / Some(Plain) 为 HTTP
|
||||
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
|
||||
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
|
||||
];
|
||||
|
||||
// Optional: Customize WSOL ATA and Seed optimization settings
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||
.with_wsol_ata_config(
|
||||
true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup
|
||||
true, // use_seed_optimize: Enable seed optimization for all ATA operations
|
||||
);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.create_wsol_ata_on_startup(true)
|
||||
.use_seed_optimize(true)
|
||||
.build();
|
||||
|
||||
// Creates new infrastructure internally
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
@@ -38,7 +38,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Example 2: Unwrap half of the WSOL back to SOL using seed account
|
||||
println!("\n🔄 Example 2: Unwrapping half of WSOL back to SOL using seed account");
|
||||
let unwrap_amount = wrap_amount / 2; // Half of the wrapped amount
|
||||
println!("Unwrapping {} lamports (0.0005 SOL) back to SOL using seed account...", unwrap_amount);
|
||||
println!(
|
||||
"Unwrapping {} lamports (0.0005 SOL) back to SOL using seed account...",
|
||||
unwrap_amount
|
||||
);
|
||||
|
||||
match solana_trade.wrap_wsol_to_sol(unwrap_amount).await {
|
||||
Ok(signature) => {
|
||||
@@ -81,7 +84,14 @@ async fn create_solana_trade_client() -> Result<SolanaTrade, Box<dyn std::error:
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
+2314
-2932
File diff suppressed because it is too large
Load Diff
+1
-46
@@ -688,21 +688,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "pool_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [112, 111, 111, 108, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "base_mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -1134,21 +1119,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "pool_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [112, 111, 111, 108, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "base_mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -3192,21 +3162,6 @@
|
||||
{
|
||||
"name": "fee_program",
|
||||
"address": "pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ"
|
||||
},
|
||||
{
|
||||
"name": "pool_v2",
|
||||
"pda": {
|
||||
"seeds": [
|
||||
{
|
||||
"kind": "const",
|
||||
"value": [112, 111, 111, 108, 45, 118, 50]
|
||||
},
|
||||
{
|
||||
"kind": "account",
|
||||
"path": "base_mint"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"args": [
|
||||
@@ -4880,7 +4835,7 @@
|
||||
},
|
||||
{
|
||||
"code": 6052,
|
||||
"name": "TokensInVaultLessThanCashbackEarned"
|
||||
"name": "CashbackEarnedDoesNotMatchTokenInVault"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
|
||||
+1463
-242
File diff suppressed because it is too large
Load Diff
@@ -1,40 +0,0 @@
|
||||
# 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
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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
|
||||
+1240
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,37 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use anyhow::Result;
|
||||
use solana_address_lookup_table_interface::state::AddressLookupTable;
|
||||
use solana_sdk::{message::AddressLookupTableAccount, pubkey::Pubkey};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub async fn fetch_address_lookup_table_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
lookup_table_address: &Pubkey,
|
||||
) -> Result<AddressLookupTableAccount, anyhow::Error> {
|
||||
let account = rpc.get_account(lookup_table_address).await?;
|
||||
let lookup_table = AddressLookupTable::deserialize(&account.data)?;
|
||||
let address_lookup_table_account = AddressLookupTableAccount {
|
||||
key: *lookup_table_address,
|
||||
addresses: lookup_table.addresses.to_vec(),
|
||||
};
|
||||
|
||||
// Parse address lookup table manually
|
||||
// Layout: 4 bytes (type) + 4 bytes (deactivation_slot) + 4 bytes (last_extended_slot) + 1 byte (last_extended_slot_start_index) + 1 byte (authority) + padding
|
||||
// Then addresses start at offset 56, each address is 32 bytes
|
||||
// First 4 bytes indicate if initialized (should be 1 or 2)
|
||||
|
||||
if account.data.len() < 56 {
|
||||
return Err(anyhow::anyhow!("Address lookup table account data too short"));
|
||||
}
|
||||
|
||||
// Read number of addresses (stored at offset 20 as u32, but we need to scan the bitmap)
|
||||
// Actually simpler: addresses start at offset 56, count from bitmap at offset 8-20
|
||||
let mut addresses = Vec::new();
|
||||
let mut offset = 56;
|
||||
while offset + 32 <= account.data.len() {
|
||||
let addr_bytes: [u8; 32] = account.data[offset..offset + 32].try_into()?;
|
||||
// Skip zero addresses (unused slots)
|
||||
if addr_bytes != [0u8; 32] {
|
||||
addresses.push(Pubkey::from(addr_bytes));
|
||||
}
|
||||
offset += 32;
|
||||
}
|
||||
|
||||
let address_lookup_table_account =
|
||||
AddressLookupTableAccount { key: *lookup_table_address, addresses };
|
||||
Ok(address_lookup_table_account)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,10 @@ static INSTRUCTION_CACHE: Lazy<DashMap<InstructionCacheKey, Arc<Vec<Instruction>
|
||||
/// Get cached instruction, compute and cache if not exists (lock-free)
|
||||
/// 🚀 返回 Arc 避免每次调用克隆整个 Vec
|
||||
#[inline]
|
||||
pub fn get_cached_instructions<F>(cache_key: InstructionCacheKey, compute_fn: F) -> Arc<Vec<Instruction>>
|
||||
pub fn get_cached_instructions<F>(
|
||||
cache_key: InstructionCacheKey,
|
||||
compute_fn: F,
|
||||
) -> Arc<Vec<Instruction>>
|
||||
where
|
||||
F: FnOnce() -> Vec<Instruction>,
|
||||
{
|
||||
@@ -63,10 +66,7 @@ where
|
||||
};
|
||||
|
||||
// Lock-free cache lookup with entry API
|
||||
INSTRUCTION_CACHE
|
||||
.entry(cache_key)
|
||||
.or_insert_with(|| Arc::new(compute_fn()))
|
||||
.clone()
|
||||
INSTRUCTION_CACHE.entry(cache_key).or_insert_with(|| Arc::new(compute_fn())).clone()
|
||||
}
|
||||
|
||||
// --------------------- Associated Token Account ---------------------
|
||||
@@ -141,7 +141,7 @@ pub fn _create_associated_token_account_idempotent_fast(
|
||||
}]
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
// 🚀 性能优化:尝试零开销解包 Arc,如果引用计数=1则直接移出,否则克隆
|
||||
Arc::try_unwrap(arc_instructions).unwrap_or_else(|arc| (*arc).clone())
|
||||
}
|
||||
@@ -154,6 +154,8 @@ pub enum PdaCacheKey {
|
||||
PumpFunUserVolume(Pubkey),
|
||||
PumpFunBondingCurve(Pubkey),
|
||||
PumpFunBondingCurveV2(Pubkey),
|
||||
/// Pump-fees program `sharing-config` PDA for a mint (`feeSharingConfigPda`).
|
||||
PumpFunFeeSharingConfig(Pubkey),
|
||||
PumpFunCreatorVault(Pubkey),
|
||||
BonkPool(Pubkey, Pubkey),
|
||||
BonkVault(Pubkey, Pubkey),
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
//!
|
||||
//! 使用 syscall_bypass 提供的快速时间戳避免频繁的系统调用
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::perf::syscall_bypass::SystemCallBypassManager;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// 全局快速时间提供器
|
||||
static FAST_TIMER: Lazy<FastTimer> = Lazy::new(|| FastTimer::new());
|
||||
@@ -26,11 +26,7 @@ impl FastTimer {
|
||||
let base_instant = Instant::now();
|
||||
let base_nanos = bypass_manager.fast_timestamp_nanos();
|
||||
|
||||
Self {
|
||||
bypass_manager,
|
||||
_base_instant: base_instant,
|
||||
_base_nanos: base_nanos,
|
||||
}
|
||||
Self { bypass_manager, _base_instant: base_instant, _base_nanos: base_nanos }
|
||||
}
|
||||
|
||||
/// 🚀 获取当前时间戳(纳秒) - 使用快速系统调用绕过
|
||||
@@ -107,10 +103,7 @@ impl FastStopwatch {
|
||||
/// 创建并启动计时器
|
||||
#[inline(always)]
|
||||
pub fn start(label: &'static str) -> Self {
|
||||
Self {
|
||||
start_nanos: fast_now_nanos(),
|
||||
label,
|
||||
}
|
||||
Self { start_nanos: fast_now_nanos(), label }
|
||||
}
|
||||
|
||||
/// 获取已耗时(纳秒)
|
||||
|
||||
+1
-1
@@ -2,11 +2,11 @@ pub mod address_lookup;
|
||||
pub mod bonding_curve;
|
||||
pub mod clock;
|
||||
pub mod fast_fn;
|
||||
pub mod sdk_log;
|
||||
pub mod fast_timing;
|
||||
pub mod gas_fee_strategy;
|
||||
pub mod global;
|
||||
pub mod nonce_cache;
|
||||
pub mod sdk_log;
|
||||
pub mod seed;
|
||||
pub mod spl_associated_token_account;
|
||||
pub mod spl_token;
|
||||
|
||||
+14
-14
@@ -1,8 +1,5 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_hash::Hash;
|
||||
use solana_nonce::state::State;
|
||||
use solana_nonce::versions::Versions;
|
||||
use solana_sdk::account_utils::StateMut;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use tracing::error;
|
||||
|
||||
@@ -21,18 +18,21 @@ pub async fn fetch_nonce_info(
|
||||
nonce_account: Pubkey,
|
||||
) -> Option<DurableNonceInfo> {
|
||||
match rpc.get_account(&nonce_account).await {
|
||||
Ok(account) => match account.state() {
|
||||
Ok(Versions::Current(state)) => {
|
||||
if let State::Initialized(data) = *state {
|
||||
let blockhash = data.durable_nonce.as_hash();
|
||||
return Some(DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(*blockhash),
|
||||
});
|
||||
}
|
||||
Ok(account) => {
|
||||
// Parse nonce account manually: first 4 bytes is version, then 4 bytes authority type
|
||||
// For initialized nonce: version=0, authority_type=0, then authority (32 bytes), then blockhash (32 bytes), then fee_calculator
|
||||
if account.data.len() >= 80 {
|
||||
// Skip version (4) + authority_type (4) + authority (32) = 40 bytes
|
||||
// Then blockhash is at offset 40
|
||||
let blockhash_bytes: [u8; 32] = account.data[40..72].try_into().ok()?;
|
||||
return Some(DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(Hash::from(blockhash_bytes)),
|
||||
});
|
||||
} else {
|
||||
error!("Nonce account data too short");
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get nonce account information: {:?}", e);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,56 @@
|
||||
//! Controlled by `TradeConfig::log_enabled`, set in `TradingClient::new`.
|
||||
//! All SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.) should check this before output.
|
||||
|
||||
use std::fmt;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Format duration for log: "97.9396 ms" or "13.936 µs", 4 decimal places, space before unit.
|
||||
fn format_elapsed(d: Duration) -> String {
|
||||
let secs = d.as_secs_f64();
|
||||
if secs < 0.001 {
|
||||
format!("{:.4} µs", secs * 1_000_000.0)
|
||||
} else {
|
||||
format!("{:.4} ms", secs * 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a short error message for SWQOS submission failed log.
|
||||
/// Tries JSON "message"/"data" and quoted string; on any parse failure returns original (no panic).
|
||||
fn extract_swqos_error_message(s: &str) -> String {
|
||||
let s = s.trim();
|
||||
if s.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
// Plain double-quoted string (no inner JSON): unquote
|
||||
if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 {
|
||||
let inner = &s[1..s.len() - 1];
|
||||
if !inner.contains('{') {
|
||||
return inner.replace("\\\"", "\"");
|
||||
}
|
||||
}
|
||||
// Try parse as JSON only when input looks like JSON (avoid parsing long non-JSON strings)
|
||||
if s.starts_with('{') {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(s) {
|
||||
let obj = v.get("error").and_then(|e| e.as_object()).or_else(|| v.as_object());
|
||||
if let Some(o) = obj {
|
||||
if let Some(m) = o.get("message").and_then(|x| x.as_str()) {
|
||||
return m.to_string();
|
||||
}
|
||||
if let Some(d) = o.get("data").and_then(|x| x.as_str()) {
|
||||
return d.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
s.to_string()
|
||||
}
|
||||
|
||||
static SDK_LOG_ENABLED: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
/// Width of [provider] label so SWQOS submit/confirm lines align (longest: Speedlanding).
|
||||
pub const SWQOS_LABEL_WIDTH: usize = 12;
|
||||
|
||||
/// Whether SDK logging is enabled (set from TradeConfig.log_enabled in TradingClient::new).
|
||||
#[inline(always)]
|
||||
pub fn sdk_log_enabled() -> bool {
|
||||
@@ -17,3 +63,99 @@ pub fn sdk_log_enabled() -> bool {
|
||||
pub fn set_sdk_log_enabled(enabled: bool) {
|
||||
SDK_LOG_ENABLED.store(enabled, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Aligned log: ` [Soyas ] Buy submitted: 13.936 µs`. Call only when sdk_log_enabled().
|
||||
#[inline]
|
||||
pub fn log_swqos_submitted(provider: &str, trade_type: impl fmt::Display, elapsed: Duration) {
|
||||
println!(
|
||||
" [{:width$}] {} submitted: {}",
|
||||
provider,
|
||||
trade_type,
|
||||
format_elapsed(elapsed),
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
/// Prints one SDK timing block (build_instructions, before_submit, per-channel submit_done).
|
||||
/// When confirm_us is Some, prints confirmed + total; when None, prints "confirmed: -, total: submit_ms".
|
||||
/// Call only when sdk_log_enabled().
|
||||
pub fn print_sdk_timing_block(
|
||||
dir: &str,
|
||||
start_us: Option<i64>,
|
||||
build_end_us: Option<i64>,
|
||||
before_submit_us: Option<i64>,
|
||||
submit_timings: &[(crate::swqos::SwqosType, i64)],
|
||||
confirm_us: Option<i64>,
|
||||
) {
|
||||
println!();
|
||||
let start_us = match start_us {
|
||||
Some(u) => u,
|
||||
None => return,
|
||||
};
|
||||
if let Some(end_us) = build_end_us {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
(end_us - start_us) as f64 / 1000.0,
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
if let Some(end_us) = before_submit_us {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
(end_us - start_us) as f64 / 1000.0,
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
if let Some(confirm_done_us) = confirm_us {
|
||||
let total_ms = (confirm_done_us - start_us) as f64 / 1000.0;
|
||||
for (swqos_type, submit_done_us) in submit_timings {
|
||||
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||
let confirmed_ms = (confirm_done_us - *submit_done_us).max(0) as f64 / 1000.0;
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
dir,
|
||||
submit_ms,
|
||||
confirmed_ms,
|
||||
total_ms,
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (swqos_type, submit_done_us) in submit_timings {
|
||||
let submit_ms = (*submit_done_us - start_us).max(0) as f64 / 1000.0;
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
dir,
|
||||
submit_ms,
|
||||
submit_ms,
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Aligned log: ` [Stellium ] Buy submission failed after 97.9396 ms: ...`. Call only when sdk_log_enabled().
|
||||
/// Error is normalized: JSON "message"/"data" or quoted string is shown; raw JSON is not.
|
||||
#[inline]
|
||||
pub fn log_swqos_submission_failed(
|
||||
provider: &str,
|
||||
trade_type: impl fmt::Display,
|
||||
elapsed: Duration,
|
||||
err: impl fmt::Display,
|
||||
) {
|
||||
let msg = extract_swqos_error_message(&format!("{}", err));
|
||||
eprintln!(
|
||||
" [{:width$}] {} submission failed after {}, error: {}",
|
||||
provider,
|
||||
trade_type,
|
||||
format_elapsed(elapsed),
|
||||
msg,
|
||||
width = SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
+33
-12
@@ -1,13 +1,13 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use anyhow::anyhow;
|
||||
use fnv::FnvHasher;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction::create_account_with_seed;
|
||||
use std::hash::Hasher;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use tokio::time::{sleep, Duration};
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::hash::Hasher;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
// 🚀 优化:使用 AtomicU64 替代 RwLock,性能提升 5-10x
|
||||
// u64::MAX 表示未初始化状态
|
||||
@@ -17,7 +17,7 @@ static SPL_TOKEN_2022_RENT: Lazy<AtomicU64> = Lazy::new(|| AtomicU64::new(u64::M
|
||||
/// 更新租金缓存(后台任务调用)
|
||||
pub async fn update_rents(client: &SolanaRpcClient) -> Result<(), anyhow::Error> {
|
||||
let rent = fetch_rent_for_token_account(client, false).await?;
|
||||
SPL_TOKEN_RENT.store(rent, Ordering::Release); // Release 确保其他线程可见
|
||||
SPL_TOKEN_RENT.store(rent, Ordering::Release); // Release 确保其他线程可见
|
||||
|
||||
let rent = fetch_rent_for_token_account(client, true).await?;
|
||||
SPL_TOKEN_2022_RENT.store(rent, Ordering::Release);
|
||||
@@ -25,6 +25,15 @@ pub async fn update_rents(client: &SolanaRpcClient) -> Result<(), anyhow::Error>
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 165 字节 Token 账户的典型租金(lamports),RPC 超时时用作回退
|
||||
const DEFAULT_TOKEN_ACCOUNT_RENT: u64 = 2_039_280;
|
||||
|
||||
/// 当 RPC 超时或不可用时设置默认租金,避免客户端创建卡死
|
||||
pub fn set_default_rents() {
|
||||
SPL_TOKEN_RENT.store(DEFAULT_TOKEN_ACCOUNT_RENT, Ordering::Release);
|
||||
SPL_TOKEN_2022_RENT.store(DEFAULT_TOKEN_ACCOUNT_RENT, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn start_rent_updater(client: Arc<SolanaRpcClient>) {
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -53,11 +62,15 @@ pub fn create_associated_token_account_use_seed(
|
||||
// Relaxed: 租金值不变,无需同步;Release/Acquire 在 update_rents 保证初始化可见性
|
||||
let rent = if is_2022_token {
|
||||
let v = SPL_TOKEN_2022_RENT.load(Ordering::Relaxed);
|
||||
if v == u64::MAX { return Err(anyhow!("Rent not initialized")); }
|
||||
if v == u64::MAX {
|
||||
return Err(anyhow!("Rent not initialized"));
|
||||
}
|
||||
v
|
||||
} else {
|
||||
let v = SPL_TOKEN_RENT.load(Ordering::Relaxed);
|
||||
if v == u64::MAX { return Err(anyhow!("Rent not initialized")); }
|
||||
if v == u64::MAX {
|
||||
return Err(anyhow!("Rent not initialized"));
|
||||
}
|
||||
v
|
||||
};
|
||||
|
||||
@@ -79,9 +92,17 @@ pub fn create_associated_token_account_use_seed(
|
||||
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
|
||||
|
||||
let len = 165;
|
||||
// 但账户的 owner 仍然使用正确的 token_program(Token 或 Token-2022)
|
||||
let create_acc =
|
||||
create_account_with_seed(payer, &ata_like, owner, seed, rent, len, token_program);
|
||||
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
|
||||
// 否则创建的账户地址与 ata_like 不匹配,导致 initializeAccount3 失败
|
||||
let create_acc = system_instruction::create_account_with_seed(
|
||||
payer,
|
||||
&ata_like,
|
||||
payer,
|
||||
seed,
|
||||
rent,
|
||||
len,
|
||||
token_program,
|
||||
);
|
||||
|
||||
let init_acc = if is_2022_token {
|
||||
crate::common::spl_token_2022::initialize_account3(&token_program, &ata_like, mint, owner)?
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
|
||||
+6
-13
@@ -1,6 +1,6 @@
|
||||
use solana_program::pubkey;
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
@@ -18,14 +18,11 @@ pub fn close_account(
|
||||
let mut data = Vec::with_capacity(1);
|
||||
data.push(9);
|
||||
let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len());
|
||||
accounts.push(solana_sdk::message::AccountMeta::new(*account_pubkey, false));
|
||||
accounts.push(solana_sdk::message::AccountMeta::new(*destination_pubkey, false));
|
||||
accounts.push(solana_sdk::message::AccountMeta::new_readonly(
|
||||
*owner_pubkey,
|
||||
signer_pubkeys.is_empty(),
|
||||
));
|
||||
accounts.push(AccountMeta::new(*account_pubkey, false));
|
||||
accounts.push(AccountMeta::new(*destination_pubkey, false));
|
||||
accounts.push(AccountMeta::new_readonly(*owner_pubkey, signer_pubkeys.is_empty()));
|
||||
for signer_pubkey in signer_pubkeys.iter() {
|
||||
accounts.push(solana_sdk::message::AccountMeta::new_readonly(**signer_pubkey, true));
|
||||
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
|
||||
}
|
||||
Ok(Instruction { program_id: *token_program_id, accounts, data })
|
||||
}
|
||||
@@ -52,11 +49,7 @@ pub fn transfer(
|
||||
accounts.push(AccountMeta::new_readonly(**signer, true));
|
||||
}
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: *token_program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
Ok(Instruction { program_id: *token_program_id, accounts, data })
|
||||
}
|
||||
|
||||
pub fn initialize_account3(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use solana_program::pubkey;
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
+141
-23
@@ -9,6 +9,12 @@ pub struct InfrastructureConfig {
|
||||
pub rpc_url: String,
|
||||
pub swqos_configs: Vec<SwqosConfig>,
|
||||
pub commitment: CommitmentConfig,
|
||||
/// When true, SWQOS sender threads use the *last* N cores instead of the first N. Reduces contention with main thread / default tokio workers that often use low-numbered cores. Default false.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC `:9000` or HTTP `mev-protect=true`, BlockRazor) use MEV-protected
|
||||
/// endpoints/modes. Default false.
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
|
||||
impl InfrastructureConfig {
|
||||
@@ -21,6 +27,8 @@ impl InfrastructureConfig {
|
||||
rpc_url,
|
||||
swqos_configs,
|
||||
commitment,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +38,8 @@ impl InfrastructureConfig {
|
||||
rpc_url: config.rpc_url.clone(),
|
||||
swqos_configs: config.swqos_configs.clone(),
|
||||
commitment: config.commitment.clone(),
|
||||
swqos_cores_from_end: config.swqos_cores_from_end,
|
||||
mev_protection: config.mev_protection,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +57,9 @@ impl Hash for InfrastructureConfig {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.rpc_url.hash(state);
|
||||
self.swqos_configs.hash(state);
|
||||
// Hash commitment level as string since CommitmentConfig doesn't impl Hash
|
||||
format!("{:?}", self.commitment).hash(state);
|
||||
self.swqos_cores_from_end.hash(state);
|
||||
self.mev_protection.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +68,8 @@ impl PartialEq for InfrastructureConfig {
|
||||
self.rpc_url == other.rpc_url
|
||||
&& self.swqos_configs == other.swqos_configs
|
||||
&& self.commitment == other.commitment
|
||||
&& self.swqos_cores_from_end == other.swqos_cores_from_end
|
||||
&& self.mev_protection == other.mev_protection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,52 +85,157 @@ pub struct TradeConfig {
|
||||
pub create_wsol_ata_on_startup: bool,
|
||||
/// Whether to use seed optimization for all ATA operations (default: true)
|
||||
pub use_seed_optimize: bool,
|
||||
/// Whether to pin parallel submit tasks to CPU cores (can reduce latency; set false in containers). Default true.
|
||||
pub use_core_affinity: bool,
|
||||
/// Whether to output all SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default true.
|
||||
pub log_enabled: bool,
|
||||
/// Whether to check minimum tip per SWQOS provider (filter out configs below min). Default false to save latency.
|
||||
pub check_min_tip: bool,
|
||||
/// When true, SWQOS uses the *last* N cores (instead of the first N). Use when main thread / tokio use low-numbered cores to reduce CPU contention. Default false.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
|
||||
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||
pub mev_protection: bool,
|
||||
/// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27/26-account metas, quote_mint support).
|
||||
/// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface.
|
||||
pub use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl TradeConfig {
|
||||
/// Create a new TradeConfig using the builder pattern.
|
||||
///
|
||||
/// # Available builder methods
|
||||
/// - `.create_wsol_ata_on_startup(bool)` — check & create WSOL ATA on init (default: true)
|
||||
/// - `.use_seed_optimize(bool)` — seed optimization for ATA ops (default: true)
|
||||
/// - `.log_enabled(bool)` — SDK timing/SWQOS logs (default: true)
|
||||
/// - `.check_min_tip(bool)` — filter SWQOS below min tip (default: false)
|
||||
/// - `.swqos_cores_from_end(bool)` — bind SWQOS to last N cores (default: false)
|
||||
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
/// .mev_protection(true)
|
||||
/// .check_min_tip(true)
|
||||
/// .log_enabled(false)
|
||||
/// .build();
|
||||
/// ```
|
||||
pub fn builder(
|
||||
rpc_url: String,
|
||||
swqos_configs: Vec<SwqosConfig>,
|
||||
commitment: CommitmentConfig,
|
||||
) -> TradeConfigBuilder {
|
||||
TradeConfigBuilder::new(rpc_url, swqos_configs, commitment)
|
||||
}
|
||||
|
||||
/// Shortcut: create a TradeConfig with all defaults. Equivalent to `builder(...).build()`.
|
||||
pub fn new(
|
||||
rpc_url: String,
|
||||
swqos_configs: Vec<SwqosConfig>,
|
||||
commitment: CommitmentConfig,
|
||||
) -> Self {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!("🔧 TradeConfig create_wsol_ata_on_startup default: true");
|
||||
println!("🔧 TradeConfig use_seed_optimize default: true");
|
||||
}
|
||||
Self::builder(rpc_url, swqos_configs, commitment).build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for [`TradeConfig`]. Created via [`TradeConfig::builder`].
|
||||
///
|
||||
/// All fields are optional and pre-filled with sensible defaults.
|
||||
/// Call `.build()` to produce the final [`TradeConfig`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TradeConfigBuilder {
|
||||
rpc_url: String,
|
||||
swqos_configs: Vec<SwqosConfig>,
|
||||
commitment: CommitmentConfig,
|
||||
create_wsol_ata_on_startup: bool,
|
||||
use_seed_optimize: bool,
|
||||
log_enabled: bool,
|
||||
check_min_tip: bool,
|
||||
swqos_cores_from_end: bool,
|
||||
mev_protection: bool,
|
||||
use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl TradeConfigBuilder {
|
||||
fn new(rpc_url: String, swqos_configs: Vec<SwqosConfig>, commitment: CommitmentConfig) -> Self {
|
||||
Self {
|
||||
rpc_url,
|
||||
swqos_configs,
|
||||
commitment,
|
||||
create_wsol_ata_on_startup: true, // default: check and create on startup
|
||||
use_seed_optimize: true, // default: use seed optimization
|
||||
use_core_affinity: true, // default: pin parallel submit tasks to cores
|
||||
log_enabled: true, // default: enable all SDK logs
|
||||
check_min_tip: false, // default: skip min tip check to reduce latency
|
||||
create_wsol_ata_on_startup: true,
|
||||
use_seed_optimize: true,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a TradeConfig with custom WSOL ATA settings
|
||||
pub fn with_wsol_ata_config(
|
||||
mut self,
|
||||
create_wsol_ata_on_startup: bool,
|
||||
use_seed_optimize: bool,
|
||||
) -> Self {
|
||||
self.create_wsol_ata_on_startup = create_wsol_ata_on_startup;
|
||||
self.use_seed_optimize = use_seed_optimize;
|
||||
/// Check and create WSOL ATA on SDK initialization. Default: `true`.
|
||||
pub fn create_wsol_ata_on_startup(mut self, v: bool) -> Self {
|
||||
self.create_wsol_ata_on_startup = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to check minimum tip per SWQOS (filter out configs below min). Default false for lower latency.
|
||||
pub fn with_check_min_tip(mut self, check_min_tip: bool) -> Self {
|
||||
self.check_min_tip = check_min_tip;
|
||||
/// Enable seed optimization for all ATA operations. Default: `true`.
|
||||
pub fn use_seed_optimize(mut self, v: bool) -> Self {
|
||||
self.use_seed_optimize = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default: `true`.
|
||||
pub fn log_enabled(mut self, v: bool) -> Self {
|
||||
self.log_enabled = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter out SWQOS providers whose tip is below their minimum requirement.
|
||||
/// Adds a small check on the hot path; disable for lowest latency. Default: `false`.
|
||||
pub fn check_min_tip(mut self, v: bool) -> Self {
|
||||
self.check_min_tip = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind SWQOS sender threads to the *last* N CPU cores instead of the first N.
|
||||
/// Useful when main thread / tokio workers occupy low-numbered cores. Default: `false`.
|
||||
pub fn swqos_cores_from_end(mut self, v: bool) -> Self {
|
||||
self.swqos_cores_from_end = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable global MEV protection. When `true`:
|
||||
/// - **Astralane QUIC** uses port `9000`; **Astralane HTTP** adds `mev-protect=true`
|
||||
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
|
||||
///
|
||||
/// May reduce landing speed. Default: `false`.
|
||||
pub fn mev_protection(mut self, v: bool) -> Self {
|
||||
self.mev_protection = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use PumpFun V2 instructions (`buy_v2` / `sell_v2`, 27-account metas, `quote_mint` support).
|
||||
/// Default: `false` (V1 — 18-account metas, legacy SOL-paired, smaller transaction).
|
||||
/// Set to `true` when PumpFun officially deploys V2 on mainnet.
|
||||
pub fn use_pumpfun_v2(mut self, v: bool) -> Self {
|
||||
self.use_pumpfun_v2 = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consume the builder and produce a [`TradeConfig`].
|
||||
pub fn build(self) -> TradeConfig {
|
||||
TradeConfig {
|
||||
rpc_url: self.rpc_url,
|
||||
swqos_configs: self.swqos_configs,
|
||||
commitment: self.commitment,
|
||||
create_wsol_ata_on_startup: self.create_wsol_ata_on_startup,
|
||||
use_seed_optimize: self.use_seed_optimize,
|
||||
log_enabled: self.log_enabled,
|
||||
check_min_tip: self.check_min_tip,
|
||||
swqos_cores_from_end: self.swqos_cores_from_end,
|
||||
mev_protection: self.mev_protection,
|
||||
use_pumpfun_v2: self.use_pumpfun_v2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
|
||||
|
||||
@@ -43,8 +43,7 @@ pub const USD1_TOKEN_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
};
|
||||
|
||||
// USDC (mainnet) mint and meta
|
||||
pub const USDC_TOKEN_ACCOUNT: Pubkey =
|
||||
pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
||||
pub const USDC_TOKEN_ACCOUNT: Pubkey = pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
|
||||
pub const USDC_TOKEN_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: USDC_TOKEN_ACCOUNT,
|
||||
@@ -58,3 +57,9 @@ pub const RENT_META: solana_sdk::instruction::AccountMeta =
|
||||
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
+254
-65
@@ -1,7 +1,6 @@
|
||||
use solana_program::pubkey;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
|
||||
pub const JITO_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("96gYZGLnJYVFmbjzopPSU6QiEV5fGqZNyN9nmNhvrZU5"),
|
||||
pubkey!("HFqU5x63VTqvQss8hp11i4wVV8bD44PvwucfZ2bU7gRe"),
|
||||
@@ -112,6 +111,7 @@ pub const BLOCKRAZOR_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"),
|
||||
];
|
||||
|
||||
/// Astralane tip wallets. Extended with new addresses for improved routing and reduced write-lock contention (see portal.astralane.io/blockline).
|
||||
pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF"),
|
||||
pubkey!("astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm"),
|
||||
@@ -121,6 +121,16 @@ pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"),
|
||||
pubkey!("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"),
|
||||
pubkey!("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"),
|
||||
// New tip wallets (2025) for improved performance and reduced write-lock delays
|
||||
pubkey!("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"),
|
||||
pubkey!("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"),
|
||||
pubkey!("AstrABAu8CBTyuPXpV4eSCJ5fePEPnxN8NqBaPKQ9fHR"),
|
||||
pubkey!("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"),
|
||||
pubkey!("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"),
|
||||
pubkey!("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"),
|
||||
pubkey!("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"),
|
||||
pubkey!("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"),
|
||||
pubkey!("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"),
|
||||
];
|
||||
|
||||
pub const STELLIUM_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
@@ -142,6 +152,12 @@ pub const SOYAS_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"),
|
||||
pubkey!("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"),
|
||||
pubkey!("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"),
|
||||
pubkey!("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"),
|
||||
pubkey!("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"),
|
||||
pubkey!("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"),
|
||||
pubkey!("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"),
|
||||
pubkey!("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"),
|
||||
pubkey!("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"),
|
||||
];
|
||||
|
||||
pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
@@ -155,162 +171,267 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
||||
];
|
||||
|
||||
// NewYork,
|
||||
// Frankfurt,
|
||||
// Amsterdam,
|
||||
// SLC,
|
||||
// Tokyo,
|
||||
// London,
|
||||
// LosAngeles,
|
||||
// Default,
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
// **地理就近(用户语义)**:当某枚举区域没有该服务商**独立公布**的 PoP 时,在**该服务商已出现的端点集合内**,按真实地理位置选**大圆距离最近**的一项作为填充;行尾注释说明依据。
|
||||
// **例外**:`SwqosRegion::Default`(下标 9)不表示地球上的点,表中为全局 URL 或文档默认枢纽,**不适用**地理就近,仅表示「未指定区域时的回退」。
|
||||
// 若某区域仅有一种「大区」级入口(例如全美只有一个美东 PoP),则地理上非最优但只能复用,注释会标明「受服务商可用区限制」。
|
||||
|
||||
pub const SWQOS_ENDPOINTS_JITO: [&str; 8] = [
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
/// Jito mainnet block engines (`https://<region>.mainnet.block-engine.jito.wtf`).
|
||||
/// There is no Los Angeles engine → use Salt Lake City for `LosAngeles`; `SwqosRegion::Default` uses the global mainnet URL.
|
||||
pub const SWQOS_ENDPOINTS_JITO: [&str; 10] = [
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
"https://frankfurt.mainnet.block-engine.jito.wtf",
|
||||
"https://amsterdam.mainnet.block-engine.jito.wtf",
|
||||
"https://dublin.mainnet.block-engine.jito.wtf",
|
||||
"https://slc.mainnet.block-engine.jito.wtf",
|
||||
"https://tokyo.mainnet.block-engine.jito.wtf",
|
||||
"https://singapore.mainnet.block-engine.jito.wtf",
|
||||
"https://london.mainnet.block-engine.jito.wtf",
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
"https://slc.mainnet.block-engine.jito.wtf", // LosAngeles: no LA PoP; nearest US-West is SLC
|
||||
"https://mainnet.block-engine.jito.wtf",
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 8] = [
|
||||
/// NextBlock regional HTTP hosts (see provider docs). `SwqosRegion` order; no dedicated LA PoP → SLC as US-West fallback.
|
||||
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 10] = [
|
||||
"http://ny.nextblock.io",
|
||||
"http://frankfurt.nextblock.io",
|
||||
"http://amsterdam.nextblock.io",
|
||||
"http://fra.nextblock.io",
|
||||
"http://ams.nextblock.io",
|
||||
"http://dublin.nextblock.io",
|
||||
"http://slc.nextblock.io",
|
||||
"http://tokyo.nextblock.io",
|
||||
"http://london.nextblock.io",
|
||||
"http://singapore.nextblock.io",
|
||||
"http://frankfurt.nextblock.io",
|
||||
"http://tokyo.nextblock.io",
|
||||
"http://sgp.nextblock.io",
|
||||
"http://london.nextblock.io",
|
||||
"http://slc.nextblock.io",
|
||||
"http://fra.nextblock.io", // Default: 非地理区域;服务商无「全局」主机名时用 EU 枢纽作未选区回退
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 10] = [
|
||||
"http://ny.0slot.trade",
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://ams.0slot.trade",
|
||||
"http://ny.0slot.trade",
|
||||
"http://ams.0slot.trade", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams(相对 de2 等)
|
||||
"http://la.0slot.trade", // SLC: no UT PoP; nearest US-West published host
|
||||
"http://jp.0slot.trade",
|
||||
"http://ams.0slot.trade",
|
||||
"http://jp.0slot.trade", // SG: 无本地 PoP;已公布 APAC 仅 jp,为表中离新加坡最近的大圆距离
|
||||
"http://ams.0slot.trade", // London: 无 UK 专用;已公布 EU 点中 ams 距伦敦最近之一
|
||||
"http://la.0slot.trade",
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://de2.0slot.trade", // Default: 非地理区域;EU 枢纽 de2 作未选区回退
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 8] = [
|
||||
"http://ewr1.nozomi.temporal.xyz",
|
||||
/// Nozomi Direct regions: ewr1, fra2, ams1, lon1, lax1, tyo1, sgp1, …
|
||||
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 10] = [
|
||||
"http://ewr1.nozomi.temporal.xyz", // NewYork → Newark
|
||||
"http://fra2.nozomi.temporal.xyz",
|
||||
"http://ams1.nozomi.temporal.xyz",
|
||||
"http://ewr1.nozomi.temporal.xyz",
|
||||
"http://lon1.nozomi.temporal.xyz", // Dublin: no IE host; UK nearest Direct PoP
|
||||
"http://lax1.nozomi.temporal.xyz", // SLC: US-West
|
||||
"http://tyo1.nozomi.temporal.xyz",
|
||||
"http://sgp1.nozomi.temporal.xyz",
|
||||
"http://pit1.nozomi.temporal.xyz",
|
||||
"http://fra2.nozomi.temporal.xyz",
|
||||
"http://lon1.nozomi.temporal.xyz",
|
||||
"http://lax1.nozomi.temporal.xyz",
|
||||
"http://fra2.nozomi.temporal.xyz", // Default: 非地理区域;EU Direct 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_BLOX: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOX: [&str; 10] = [
|
||||
"https://ny.solana.dex.blxrbdn.com",
|
||||
"https://germany.solana.dex.blxrbdn.com",
|
||||
"https://amsterdam.solana.dex.blxrbdn.com",
|
||||
"https://ny.solana.dex.blxrbdn.com",
|
||||
"https://uk.solana.dex.blxrbdn.com", // Dublin: IE/UK edge
|
||||
"https://la.solana.dex.blxrbdn.com", // SLC: no Mountain PoP; US-West LA
|
||||
"https://tokyo.solana.dex.blxrbdn.com",
|
||||
"https://tokyo.solana.dex.blxrbdn.com", // SG: 文档无 SGP 区域;已公布 APAC 仅 Tokyo,为距 SG 最近选项
|
||||
"https://uk.solana.dex.blxrbdn.com",
|
||||
"https://la.solana.dex.blxrbdn.com",
|
||||
"https://global.solana.dex.blxrbdn.com",
|
||||
"https://global.solana.dex.blxrbdn.com", // Default: 非地理区域;全球任播
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_NODE1: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_NODE1: [&str; 10] = [
|
||||
"http://ny.node1.me",
|
||||
"http://fra.node1.me",
|
||||
"http://ams.node1.me",
|
||||
"http://ny.node1.me",
|
||||
"http://lon.node1.me", // Dublin: 已公布中英爱区域用 lon(地理上近爱尔兰)
|
||||
"http://ny.node1.me", // SLC: 已公布美国仅 ny;美西无 PoP,受可用区限制复用美东
|
||||
"http://tk.node1.me",
|
||||
"http://tk.node1.me", // SG: 已公布 APAC 仅 tk;地理上为表中离 SG 最近
|
||||
"http://lon.node1.me",
|
||||
"http://ny.node1.me",
|
||||
"http://fra.node1.me",
|
||||
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
|
||||
"http://fra.node1.me", // Default: 非地理区域;与 QUIC 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
|
||||
/// Node1 QUIC: port 16666. Region order matches [`SwqosRegion`].
|
||||
/// server_name = host part (e.g. ny.node1.me). Auth: first bi stream = 16-byte UUID; each tx = new bi stream, bincode body.
|
||||
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 10] = [
|
||||
"ny.node1.me:16666",
|
||||
"fra.node1.me:16666",
|
||||
"ams.node1.me:16666",
|
||||
"lon.node1.me:16666",
|
||||
"ny.node1.me:16666",
|
||||
"tk.node1.me:16666",
|
||||
"tk.node1.me:16666",
|
||||
"lon.node1.me:16666",
|
||||
"ny.node1.me:16666",
|
||||
"fra.node1.me:16666", // Default: 非地理区域;与 HTTP 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
/// Published: ny, slc, ams, fra, singapore, london, tokyo (no IE/UK split → london for Dublin).
|
||||
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 10] = [
|
||||
"http://ny.flashblock.trade",
|
||||
"http://fra.flashblock.trade",
|
||||
"http://ams.flashblock.trade",
|
||||
"http://london.flashblock.trade", // Dublin: no IE host; UK nearest
|
||||
"http://slc.flashblock.trade",
|
||||
"http://tokyo.flashblock.trade",
|
||||
"http://singapore.flashblock.trade",
|
||||
"http://london.flashblock.trade",
|
||||
"http://ny.flashblock.trade",
|
||||
"http://ny.flashblock.trade",
|
||||
"http://slc.flashblock.trade",
|
||||
"http://fra.flashblock.trade", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
||||
/// 若 HTTP 返回 500,可尝试 HTTPS:https://<region>.solana.blockrazor.io/v2/sendTransaction(Frankfurt/NewYork/Tokyo),通过 custom_url 覆盖。
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 10] = [
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction", // Dublin: UK nearest published
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // SLC: 文档无美西;美国仅 NY,受可用区限制
|
||||
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction", // SG: 已公布 APAC 仅 Tokyo,为距 SG 最近
|
||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // LosAngeles: 无美西入口;美国仅 NY
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
|
||||
/// BlockRazor gRPC endpoints. Region order matches [`SwqosRegion`].
|
||||
/// Port 80 for gRPC protocol. Auth: apikey metadata in gRPC headers.
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 10] = [
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
||||
"http://amsterdam.solana-grpc.blockrazor.xyz:80",
|
||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80", // SLC: 与 HTTP 一致;美国仅 NY
|
||||
"http://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||
"http://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80", // LosAngeles: 与 HTTP 一致
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80", // Default: 非地理区域
|
||||
];
|
||||
|
||||
/// Plain HTTP API path (`/iris?api-key=…&method=…`).
|
||||
pub const ASTRALANE_PATH_IRIS: &str = "iris";
|
||||
/// Binary HTTP API path (`/irisb?api-key=…&method=sendTransaction`, raw bincode body).
|
||||
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
|
||||
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
|
||||
/// Astralane **Plain** HTTP gateways (`/iris`). Pair with [`ASTRALANE_PATH_IRIS`].
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_PLAIN: [&str; 10] = [
|
||||
"http://ny.gateway.astralane.io/iris",
|
||||
"http://fr.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams
|
||||
"http://la.gateway.astralane.io/iris",
|
||||
"http://jp.gateway.astralane.io/iris",
|
||||
"http://sg.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris", // London: 无 UK 专用;在已公布 EU 点中选距英国最近的 ams
|
||||
"http://la.gateway.astralane.io/iris",
|
||||
"https://edge.astralane.io/iris", // Default: 非地理区域;全局任播边缘
|
||||
];
|
||||
|
||||
/// Astralane **Binary** HTTP gateways (`/irisb`). Pair with [`ASTRALANE_PATH_IRISB`].
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_BINARY: [&str; 10] = [
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://fr.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb",
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb", // Dublin: 同 Plain
|
||||
"http://la.gateway.astralane.io/irisb",
|
||||
"http://jp.gateway.astralane.io/irisb",
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://lax.gateway.astralane.io/irisb",
|
||||
"http://lim.gateway.astralane.io/irisb",
|
||||
"http://sg.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb", // London: 同 Plain
|
||||
"http://la.gateway.astralane.io/irisb",
|
||||
"https://edge.astralane.io/irisb", // Default: 同 Plain
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
||||
/// Astralane QUIC endpoints (port 7000). Region order matches [`SwqosRegion`].
|
||||
/// See: https://github.com/Astralane/astralane-quic-client.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 10] = [
|
||||
"ny.gateway.astralane.io:7000",
|
||||
"fr.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // Dublin: 同 HTTP
|
||||
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
|
||||
"jp.gateway.astralane.io:7000",
|
||||
"sg.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // London: 同 HTTP
|
||||
"la.gateway.astralane.io:7000",
|
||||
"lim.gateway.astralane.io:7000", // Default: 非地理区域;全局边缘
|
||||
];
|
||||
|
||||
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 10] = [
|
||||
"ny.gateway.astralane.io:9000",
|
||||
"fr.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"la.gateway.astralane.io:9000",
|
||||
"jp.gateway.astralane.io:9000",
|
||||
"sg.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"la.gateway.astralane.io:9000",
|
||||
"lim.gateway.astralane.io:9000",
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 10] = [
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
"http://ams1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://lhr1.flashrpc.com", // Dublin: 已公布 UK 用 lhr;地理上近爱尔兰
|
||||
"http://ewr1.flashrpc.com", // SLC: 已公布美国仅 ewr;无美西 PoP,受可用区限制
|
||||
"http://tyo1.flashrpc.com",
|
||||
"http://tyo1.flashrpc.com", // SG: 表中无 SGP;APAC 仅 tyo,为距 SG 最近
|
||||
"http://lhr1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com", // LosAngeles: 同上,美国仅 ewr
|
||||
"http://fra1.flashrpc.com", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 10] = [
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"fra.landing.soyas.xyz:9000",
|
||||
"ams.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"lon.landing.soyas.xyz:9000", // Dublin: 已公布用 lon;地理近爱尔兰
|
||||
"nyc.landing.soyas.xyz:9000", // SLC: 已公布美国仅 nyc;无美西
|
||||
"tyo.landing.soyas.xyz:9000",
|
||||
"tyo.landing.soyas.xyz:9000", // SG: 表中 APAC 仅 tyo
|
||||
"lon.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"fra.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000", // LosAngeles: 同上
|
||||
"fra.landing.soyas.xyz:9000", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 10] = [
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778",
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778", // Dublin: 已公布 EU 点中 ams 地理近爱尔兰
|
||||
"nyc.speedlanding.trade:17778", // SLC: 表中美国仅 nyc;无美西 PoP,受可用区限制
|
||||
"tyo.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"sgp.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778", // London: 已公布 EU 中 ams 距英国最近之一
|
||||
"nyc.speedlanding.trade:17778", // LosAngeles: 同上,美国仅 nyc
|
||||
"fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
|
||||
/// Region order: NewYork(EWR), Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles(SG), Default(Global).
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"http://ewr-sender.helius-rpc.com/fast",
|
||||
"http://fra-sender.helius-rpc.com/fast",
|
||||
"http://ams-sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast", // Dublin: IE → UK/EU routing
|
||||
"http://slc-sender.helius-rpc.com/fast",
|
||||
"http://tyo-sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast",
|
||||
"http://sg-sender.helius-rpc.com/fast",
|
||||
"https://sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast",
|
||||
"http://slc-sender.helius-rpc.com/fast",
|
||||
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
|
||||
];
|
||||
|
||||
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
|
||||
@@ -331,3 +452,71 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
|
||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SWQOS_REGION_ENDPOINT_TABLES: &[&[&str]] = &[
|
||||
&SWQOS_ENDPOINTS_JITO,
|
||||
&SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
&SWQOS_ENDPOINTS_ZERO_SLOT,
|
||||
&SWQOS_ENDPOINTS_TEMPORAL,
|
||||
&SWQOS_ENDPOINTS_BLOX,
|
||||
&SWQOS_ENDPOINTS_NODE1,
|
||||
&SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||
&SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
&SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
&SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_BINARY,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_QUIC,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
&SWQOS_ENDPOINTS_STELLIUM,
|
||||
&SWQOS_ENDPOINTS_SOYAS,
|
||||
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_swqos_endpoint_tables_align_with_swqos_region() {
|
||||
const EXPECT: usize = 10;
|
||||
for (idx, table) in SWQOS_REGION_ENDPOINT_TABLES.iter().enumerate() {
|
||||
assert_eq!(
|
||||
table.len(),
|
||||
EXPECT,
|
||||
"SWQOS endpoint table index {} length must match SwqosRegion (10 variants)",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astralane_quic_hosts_match_mev_row_by_row() {
|
||||
for i in 0..10 {
|
||||
let base = SWQOS_ENDPOINTS_ASTRALANE_QUIC[i].trim_end_matches(":7000");
|
||||
let mev = SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[i].trim_end_matches(":9000");
|
||||
assert_eq!(base, mev, "Astralane QUIC vs MEV host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node1_http_host_matches_quic_without_port() {
|
||||
for i in 0..10 {
|
||||
let http_host =
|
||||
SWQOS_ENDPOINTS_NODE1[i].strip_prefix("http://").expect("NODE1 HTTP URL");
|
||||
let quic_host =
|
||||
SWQOS_ENDPOINTS_NODE1_QUIC[i].strip_suffix(":16666").expect("NODE1 QUIC endpoint");
|
||||
assert_eq!(http_host, quic_host, "Node1 HTTP vs QUIC host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astralane_plain_and_binary_same_origin_per_region() {
|
||||
for i in 0..10 {
|
||||
let plain = SWQOS_ENDPOINTS_ASTRALANE_PLAIN[i].trim_end_matches("/iris");
|
||||
let binary = SWQOS_ENDPOINTS_ASTRALANE_BINARY[i].trim_end_matches("/irisb");
|
||||
assert_eq!(plain, binary, "Astralane Plain vs Binary base URL mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ pub mod trade {
|
||||
pub const DEFAULT_SELL_TIP_FEE: f64 = 0.0001;
|
||||
pub const DEFAULT_RPC_UNIT_LIMIT: u32 = 150000;
|
||||
pub const DEFAULT_RPC_UNIT_PRICE: u64 = 500000;
|
||||
}
|
||||
}
|
||||
|
||||
+7
-23
@@ -4,12 +4,9 @@ use crate::{
|
||||
accounts, get_pool_pda, get_vault_pda, BUY_EXECT_IN_DISCRIMINATOR,
|
||||
SELL_EXECT_IN_DISCRIMINATOR,
|
||||
},
|
||||
trading::{
|
||||
common::utils::get_token_balance,
|
||||
core::{
|
||||
params::{BonkParams, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
trading::core::{
|
||||
params::{BonkParams, SwapParams},
|
||||
traits::InstructionBuilder,
|
||||
},
|
||||
utils::calc::bonk::{
|
||||
get_buy_token_amount_from_sol_amount, get_sell_sol_amount_from_token_amount,
|
||||
@@ -177,9 +174,10 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
if params.rpc.is_none() {
|
||||
return Err(anyhow!("RPC is not set"));
|
||||
}
|
||||
let amount = params
|
||||
.input_amount
|
||||
.filter(|&a| a > 0)
|
||||
.ok_or_else(|| anyhow!("Bonk sell requires input_amount (token amount to sell); fetch balance via RPC before calling build_sell"))?;
|
||||
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
@@ -189,20 +187,6 @@ impl InstructionBuilder for BonkInstructionBuilder {
|
||||
|
||||
let usd1_pool = protocol_params.global_config == accounts::USD1_GLOBAL_CONFIG;
|
||||
|
||||
let rpc = params.rpc.as_ref().unwrap().clone();
|
||||
|
||||
let mut amount = params.input_amount;
|
||||
if params.input_amount.is_none() || params.input_amount.unwrap_or(0) == 0 {
|
||||
let balance_u64 =
|
||||
get_token_balance(rpc.as_ref(), ¶ms.payer.pubkey(), ¶ms.input_mint).await?;
|
||||
amount = Some(balance_u64);
|
||||
}
|
||||
let amount = amount.unwrap_or(0);
|
||||
|
||||
if amount == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
let pool_state = if protocol_params.pool_state == Pubkey::default() {
|
||||
if usd1_pool {
|
||||
get_pool_pda(¶ms.input_mint, &crate::constants::USD1_TOKEN_ACCOUNT).unwrap()
|
||||
|
||||
@@ -27,10 +27,12 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<MeteoraDammV2Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?;
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for MeteoraDammV2"))?;
|
||||
|
||||
let is_wsol = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT || protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let is_wsol = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
||||
}
|
||||
@@ -38,7 +40,8 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let is_a_in = protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
@@ -84,7 +87,11 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
);
|
||||
@@ -135,14 +142,16 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<MeteoraDammV2Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?;
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for MeteoraDammV2"))?;
|
||||
|
||||
if params.input_amount.is_none() || params.input_amount.unwrap_or(0) == 0 {
|
||||
return Err(anyhow!("Token amount is not set"));
|
||||
}
|
||||
|
||||
let is_wsol = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT || protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let is_wsol = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_a_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
let is_usdc = protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_a_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
if !is_wsol && !is_usdc {
|
||||
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
||||
}
|
||||
@@ -150,7 +159,8 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT || protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let is_a_in = protocol_params.token_b_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.token_b_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let minimum_amount_out: u64 = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => return Err(anyhow!("fixed_output_amount must be set for MeteoraDammV2 swap")),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod bonk;
|
||||
pub mod raydium_cpmm;
|
||||
pub mod raydium_amm_v4;
|
||||
pub mod meteora_damm_v2;
|
||||
pub mod utils;
|
||||
pub mod pumpfun;
|
||||
pub(crate) mod pumpfun_ix_data;
|
||||
pub mod pumpswap;
|
||||
pub(crate) mod pumpswap_ix_data;
|
||||
pub mod raydium_amm_v4;
|
||||
pub mod raydium_cpmm;
|
||||
pub mod utils;
|
||||
|
||||
Executable → Regular
+812
-288
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` 与 **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`**
|
||||
//! 的 instruction data 栈上编码(热路径零堆分配)。
|
||||
//!
|
||||
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 在 ix 参数中为
|
||||
//! **1 字节 option tag + 1 字节值**(Anchor `Option<bool>` = 2 字节),共 26 字节 ix data。
|
||||
//! `*_v2` 指令无 `track_volume` 字节(见 [pump-public-docs](https://github.com/pump-fun/pump-public-docs))。
|
||||
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
||||
BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
/// 与官方 `getBuyInstructionInternal` 对齐:`OptionBool` 在 ix 中为 1 字节 option tag + 1 字节值。
|
||||
/// `track_volume` 在调用侧组合为 `(1u8, cashback as u8)`(Option tag + value),共 26 字节 ix data。
|
||||
pub const TRACK_VOLUME_OPTION_TAG: u8 = 1;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_ix_data(
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
track_volume_val: u8,
|
||||
) -> [u8; 26] {
|
||||
let mut d = [0u8; 26];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
d[24] = TRACK_VOLUME_OPTION_TAG;
|
||||
d[25] = track_volume_val;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_exact_sol_in_ix_data(
|
||||
spendable_sol_in: u64,
|
||||
min_tokens_out: u64,
|
||||
track_volume_val: u8,
|
||||
) -> [u8; 26] {
|
||||
let mut d = [0u8; 26];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
d[24] = TRACK_VOLUME_OPTION_TAG;
|
||||
d[25] = track_volume_val;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_sell_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
// --- v2 instruction data encoders (no track_volume arg — 2 args each, 24 bytes total) ---
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_v2_ix_data(amount: u64, max_sol_cost: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_V2_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_exact_quote_in_v2_ix_data(
|
||||
spendable_quote_in: u64,
|
||||
min_tokens_out: u64,
|
||||
) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_sell_v2_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_V2_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
d
|
||||
}
|
||||
+105
-91
@@ -1,9 +1,14 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::pumpswap_ix_data::{
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
|
||||
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
instruction::utils::pumpswap::{
|
||||
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,
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
|
||||
get_user_volume_accumulator_wsol_ata, warm_pumpswap_global_config,
|
||||
},
|
||||
trading::{
|
||||
common::wsol_manager,
|
||||
@@ -27,6 +32,7 @@ pub struct PumpSwapInstructionBuilder;
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
warm_pumpswap_global_config(params.rpc.as_ref()).await;
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
@@ -75,6 +81,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal(
|
||||
@@ -83,6 +90,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// base_amount_out, max_quote_amount_in
|
||||
@@ -94,6 +102,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// min_quote_amount_out, base_amount_in
|
||||
@@ -119,14 +128,13 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
// Determine fee recipient based on mayhem mode
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
|
||||
let is_mayhem_mode = protocol_params.is_mayhem_mode;
|
||||
let fee_recipient =
|
||||
if is_mayhem_mode { accounts::MAYHEM_FEE_RECIPIENT } else { accounts::FEE_RECIPIENT };
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
accounts::MAYHEM_FEE_RECIPIENT_META
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
accounts::FEE_RECIPIENT_META
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
@@ -136,8 +144,17 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if create_wsol_ata {
|
||||
// Determine wrap amount based on instruction type:
|
||||
// - buy_exact_quote_in: program spends exactly input_amount, wrap input_amount
|
||||
// - buy: program may spend up to max_quote, wrap max_quote
|
||||
let wrap_amount =
|
||||
if quote_is_wsol_or_usdc && params.use_exact_sol_amount.unwrap_or(true) {
|
||||
params.input_amount.unwrap_or(0)
|
||||
} else {
|
||||
sol_amount
|
||||
};
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), sol_amount));
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), wrap_amount));
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
@@ -153,7 +170,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let mut accounts = Vec::with_capacity(23);
|
||||
let mut accounts = Vec::with_capacity(28);
|
||||
accounts.extend([
|
||||
AccountMeta::new(pool, false), // pool_id
|
||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||
@@ -177,10 +194,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
]);
|
||||
if quote_is_wsol_or_usdc {
|
||||
accounts.push(accounts::GLOBAL_VOLUME_ACCUMULATOR_META);
|
||||
accounts.push(AccountMeta::new(
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey()).unwrap(),
|
||||
false,
|
||||
));
|
||||
let uva = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(uva, false));
|
||||
}
|
||||
accounts.push(accounts::FEE_CONFIG_META);
|
||||
accounts.push(accounts::FEE_PROGRAM_META);
|
||||
@@ -190,51 +206,51 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||
}
|
||||
}
|
||||
// Program upgrade: pool_v2 (readonly) at end of account list
|
||||
accounts.push(AccountMeta::new_readonly(
|
||||
get_pool_v2_pda(&base_mint).unwrap(),
|
||||
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts);
|
||||
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
}
|
||||
// Trailing accounts: GlobalConfig.buyback_fee_recipients 中任 pubkey + quote ATA(与 pump-swap-sdk 静态池对齐;轮换时需查链上)。
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
false,
|
||||
));
|
||||
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
if quote_is_wsol_or_usdc {
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
// buy_exact_quote_in(spendable_quote_in: u64, min_base_amount_out: u64)
|
||||
// Spend exactly the input SOL/quote amount, get at least min_base_amount_out
|
||||
let ix_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
data[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
// spendable_quote_in (exact SOL amount to spend)
|
||||
data[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
// min_base_amount_out (minimum tokens to receive)
|
||||
data[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
min_base_amount_out,
|
||||
track_volume,
|
||||
)
|
||||
} else {
|
||||
// buy(base_amount_out: u64, max_quote_amount_in: u64)
|
||||
// Buy exactly base_amount_out tokens, pay up to max_quote_amount_in
|
||||
data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
// base_amount_out
|
||||
data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
// max_quote_amount_in
|
||||
data[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
}
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
};
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else {
|
||||
data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
// base_amount_in
|
||||
data[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// min_quote_amount_out
|
||||
data[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
let ix_data = encode_pumpswap_sell_ix_data(sol_amount, token_amount);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
}
|
||||
|
||||
let buy_instruction = Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts: accounts.clone(),
|
||||
data: data.to_vec(),
|
||||
};
|
||||
|
||||
instructions.push(buy_instruction);
|
||||
if close_wsol_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
@@ -243,6 +259,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
warm_pumpswap_global_config(params.rpc.as_ref()).await;
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
@@ -291,6 +308,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal(
|
||||
@@ -299,6 +317,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// base_amount_in, min_quote_amount_out
|
||||
@@ -310,6 +329,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// max_quote_amount_in, base_amount_out
|
||||
@@ -320,14 +340,13 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
sol_amount = params.fixed_output_amount.unwrap();
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 10th = Mayhem fee recipient, 11th = WSOL ATA of Mayhem; use any one randomly)
|
||||
let is_mayhem_mode = protocol_params.is_mayhem_mode;
|
||||
let fee_recipient =
|
||||
if is_mayhem_mode { accounts::MAYHEM_FEE_RECIPIENT } else { accounts::FEE_RECIPIENT };
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
accounts::MAYHEM_FEE_RECIPIENT_META
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
accounts::FEE_RECIPIENT_META
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
@@ -356,7 +375,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
let mut accounts = Vec::with_capacity(23);
|
||||
let mut accounts = Vec::with_capacity(28);
|
||||
accounts.extend([
|
||||
AccountMeta::new(pool, false), // pool_id
|
||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||
@@ -380,52 +399,47 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
]);
|
||||
if !quote_is_wsol_or_usdc {
|
||||
accounts.push(accounts::GLOBAL_VOLUME_ACCUMULATOR_META);
|
||||
accounts.push(AccountMeta::new(
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey()).unwrap(),
|
||||
false,
|
||||
));
|
||||
let uva = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(uva, false));
|
||||
}
|
||||
accounts.push(accounts::FEE_CONFIG_META);
|
||||
accounts.push(accounts::FEE_PROGRAM_META);
|
||||
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator, remaining_accounts[1] = UserVolumeAccumulator PDA
|
||||
// Cashback sell: 官方 remainingAccounts = [accumulator 的 quote_mint ATA, accumulator PDA, poolV2](用 quote_mint 非固定 WSOL)
|
||||
if protocol_params.is_cashback_coin {
|
||||
if let (Some(wsol_ata), Some(accumulator)) = (
|
||||
get_user_volume_accumulator_wsol_ata(¶ms.payer.pubkey()),
|
||||
if let (Some(quote_ata), Some(accumulator)) = (
|
||||
get_user_volume_accumulator_quote_ata(
|
||||
¶ms.payer.pubkey(),
|
||||
"e_mint,
|
||||
"e_token_program,
|
||||
),
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey()),
|
||||
) {
|
||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||
accounts.push(AccountMeta::new(quote_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(),
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
}
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
false,
|
||||
));
|
||||
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
if quote_is_wsol_or_usdc {
|
||||
data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
// base_amount_in
|
||||
data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
// min_quote_amount_out
|
||||
data[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
|
||||
let ix_data = if quote_is_wsol_or_usdc {
|
||||
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
|
||||
} else {
|
||||
data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
// base_amount_out
|
||||
data[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// max_quote_amount_in
|
||||
data[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
}
|
||||
|
||||
let sell_instruction = Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts: accounts.clone(),
|
||||
data: data.to_vec(),
|
||||
encode_pumpswap_buy_two_args(sol_amount, token_amount)
|
||||
};
|
||||
|
||||
instructions.push(sell_instruction);
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
|
||||
if close_wsol_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
@@ -465,12 +479,12 @@ pub fn claim_cashback_pumpswap_instruction(
|
||||
// IDL order: user, user_volume_accumulator, quote_mint, quote_token_program,
|
||||
// user_volume_accumulator_wsol_token_account, user_wsol_token_account, system_program, event_authority, program
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*payer, true), // user (signer, writable)
|
||||
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
|
||||
AccountMeta::new(*payer, true), // user (signer, writable)
|
||||
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
|
||||
AccountMeta::new_readonly(quote_mint, false),
|
||||
AccountMeta::new_readonly(quote_token_program, false),
|
||||
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
|
||||
AccountMeta::new(user_wsol_ata, false), // writable
|
||||
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
|
||||
AccountMeta::new(user_wsol_ata, false), // writable
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::AMM_PROGRAM_META,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! PumpSwap AMM `buy` / `buy_exact_quote_in` / `sell` instruction **data**(栈数组、无 `Vec` 分配)。
|
||||
|
||||
use crate::instruction::utils::pumpswap::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_ix_data(
|
||||
base_amount_out: u64,
|
||||
max_quote_amount_in: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
spendable_quote_in: u64,
|
||||
min_base_amount_out: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_sell_ix_data(base_amount_in: u64, min_quote_amount_out: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_quote_amount_out.to_le_bytes());
|
||||
d
|
||||
}
|
||||
@@ -29,7 +29,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumAmmV4Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?;
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
|
||||
|
||||
let is_wsol = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.pc_mint == crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
@@ -44,7 +44,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
let is_base_in = protocol_params.coin_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.coin_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let amount_in: u64 = params.input_amount.unwrap_or(0);
|
||||
let swap_result = compute_swap_amount(
|
||||
@@ -62,7 +62,11 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
let user_source_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -144,7 +148,7 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<RaydiumAmmV4Params>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumCpmm"))?;
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for RaydiumAmmV4"))?;
|
||||
|
||||
if params.input_amount.is_none() || params.input_amount.unwrap_or(0) == 0 {
|
||||
return Err(anyhow!("Token amount is not set"));
|
||||
@@ -187,7 +191,11 @@ impl InstructionBuilder for RaydiumAmmV4InstructionBuilder {
|
||||
let user_destination_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
let is_base_in = protocol_params.base_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
|| protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
let mint_token_program = if is_base_in {
|
||||
protocol_params.quote_token_program
|
||||
@@ -84,7 +84,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
|
||||
let input_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -97,10 +101,15 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
|
||||
let input_vault_account = get_vault_account(
|
||||
&pool_state,
|
||||
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
protocol_params,
|
||||
);
|
||||
let output_vault_account = get_vault_account(&pool_state, ¶ms.output_mint, protocol_params);
|
||||
let output_vault_account =
|
||||
get_vault_account(&pool_state, ¶ms.output_mint, protocol_params);
|
||||
|
||||
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
|
||||
get_observation_state_pda(&pool_state).unwrap()
|
||||
@@ -136,13 +145,17 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new(pool_state, false), // Pool State
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
crate::constants::TOKEN_PROGRAM_META, // Input Token Program (readonly)
|
||||
AccountMeta::new_readonly(mint_token_program, false), // Output Token Program (readonly)
|
||||
if is_wsol { crate::constants::WSOL_TOKEN_ACCOUNT_META } else { crate::constants::USDC_TOKEN_ACCOUNT_META }, // Input token mint (readonly)
|
||||
if is_wsol {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
crate::constants::USDC_TOKEN_ACCOUNT_META
|
||||
}, // Input token mint (readonly)
|
||||
AccountMeta::new_readonly(params.output_mint, false), // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
];
|
||||
@@ -196,7 +209,7 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
|
||||
let is_usdc = protocol_params.base_mint == crate::constants::USDC_TOKEN_ACCOUNT
|
||||
|| protocol_params.quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
|
||||
if !is_wsol && !is_usdc {
|
||||
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
||||
}
|
||||
@@ -228,7 +241,11 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
|
||||
let output_token_account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
¶ms.payer.pubkey(),
|
||||
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
@@ -241,10 +258,15 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
|
||||
let output_vault_account = get_vault_account(
|
||||
&pool_state,
|
||||
if is_wsol { &crate::constants::WSOL_TOKEN_ACCOUNT } else { &crate::constants::USDC_TOKEN_ACCOUNT },
|
||||
if is_wsol {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::USDC_TOKEN_ACCOUNT
|
||||
},
|
||||
protocol_params,
|
||||
);
|
||||
let input_vault_account = get_vault_account(&pool_state, ¶ms.input_mint, protocol_params);
|
||||
let input_vault_account =
|
||||
get_vault_account(&pool_state, ¶ms.input_mint, protocol_params);
|
||||
|
||||
let observation_state_account = if protocol_params.observation_state == Pubkey::default() {
|
||||
get_observation_state_pda(&pool_state).unwrap()
|
||||
@@ -267,14 +289,18 @@ impl InstructionBuilder for RaydiumCpmmInstructionBuilder {
|
||||
accounts::AUTHORITY_META, // Authority (readonly)
|
||||
AccountMeta::new(protocol_params.amm_config, false), // Amm Config (readonly)
|
||||
AccountMeta::new(pool_state, false), // Pool State
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
AccountMeta::new(input_token_account, false), // Input Token Account
|
||||
AccountMeta::new(output_token_account, false), // Output Token Account
|
||||
AccountMeta::new(input_vault_account, false), // Input Vault Account
|
||||
AccountMeta::new(output_vault_account, false), // Output Vault Account
|
||||
AccountMeta::new_readonly(mint_token_program, false), // Input Token Program (readonly)
|
||||
crate::constants::TOKEN_PROGRAM_META, // Output Token Program (readonly)
|
||||
AccountMeta::new_readonly(params.input_mint, false), // Input token mint (readonly)
|
||||
if is_wsol { crate::constants::WSOL_TOKEN_ACCOUNT_META } else { crate::constants::USDC_TOKEN_ACCOUNT_META }, // Output token mint (readonly)
|
||||
if is_wsol {
|
||||
crate::constants::WSOL_TOKEN_ACCOUNT_META
|
||||
} else {
|
||||
crate::constants::USDC_TOKEN_ACCOUNT_META
|
||||
}, // Output token mint (readonly)
|
||||
AccountMeta::new(observation_state_account, false), // Observation State Account
|
||||
];
|
||||
// Create instruction data
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
pub mod bonk;
|
||||
pub mod meteora_damm_v2;
|
||||
pub mod pumpfun;
|
||||
pub mod pumpswap;
|
||||
pub mod raydium_amm_v4;
|
||||
pub mod raydium_cpmm;
|
||||
pub mod meteora_damm_v2;
|
||||
|
||||
// types
|
||||
pub mod bonk_types;
|
||||
pub mod meteora_damm_v2_types;
|
||||
pub mod pumpswap_types;
|
||||
pub mod raydium_amm_v4_types;
|
||||
pub mod raydium_cpmm_types;
|
||||
pub mod meteora_damm_v2_types;
|
||||
@@ -1,56 +1,54 @@
|
||||
//! Pump.fun bonding-curve utilities (flat module): PDAs, fee `#2`, pump-fees `SharingConfig`, cold RPC.
|
||||
//!
|
||||
//! Hot swap ix assembly stays sync; async helpers at file bottom. Layout matches `@pump-fun/pump-sdk`.
|
||||
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use borsh::BorshDeserialize;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
// --- seeds -------------------------------------------------------------
|
||||
|
||||
pub mod seeds {
|
||||
/// Seed for bonding curve PDAs
|
||||
|
||||
/// Seed for bonding curve PDAs (`["bonding-curve", mint]`).
|
||||
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)
|
||||
/// Seed for bonding curve v2 PDA (`["bonding-curve-v2", mint]`).
|
||||
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
|
||||
|
||||
/// Seed for creator vault PDAs
|
||||
/// Creator vault PDA seeds prefix (`["creator-vault", authority]`).
|
||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
||||
|
||||
/// Seed for metadata PDAs
|
||||
/// Metadata PDA seeds prefix.
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
|
||||
/// Seed for user volume accumulator PDAs
|
||||
/// User volume accumulator for cashback / bonding-curve UX.
|
||||
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
||||
|
||||
/// Seed for global volume accumulator PDAs
|
||||
/// Global volume accumulator.
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
||||
|
||||
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
||||
/// `feeSharingConfig` PDA under pump-fees (`feeSharingConfigPda`).
|
||||
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
|
||||
}
|
||||
|
||||
pub mod global_constants {
|
||||
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000;
|
||||
|
||||
pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000;
|
||||
|
||||
pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000;
|
||||
|
||||
pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000;
|
||||
|
||||
pub const FEE_BASIS_POINTS: u64 = 95;
|
||||
|
||||
pub const ENABLE_MIGRATE: bool = false;
|
||||
|
||||
pub const POOL_MIGRATION_FEE: u64 = 15_000_001;
|
||||
|
||||
pub const CREATOR_FEE: u64 = 30;
|
||||
pub const SCALE: u64 = 1_000_000;
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL;
|
||||
|
||||
pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals
|
||||
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports
|
||||
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL
|
||||
|
||||
/// Public key for the fee recipient
|
||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
@@ -59,8 +57,17 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey =
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS");
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"),
|
||||
pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"),
|
||||
pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"),
|
||||
pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"),
|
||||
pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"),
|
||||
pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"),
|
||||
];
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
pub const MAYHEM_FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: MAYHEM_FEE_RECIPIENT,
|
||||
@@ -68,7 +75,6 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Public key for the global PDA
|
||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
@@ -77,49 +83,57 @@ pub mod global_constants {
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
/// Public key for the authority
|
||||
pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF");
|
||||
|
||||
/// Public key for the withdraw authority
|
||||
pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg");
|
||||
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ");
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX");
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz");
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY");
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM");
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz");
|
||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
||||
// Pump.fun AMM: Protocol Fee 7
|
||||
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
|
||||
/// Buyback fee recipients (v2 account #9 in buy_v2/sell_v2).
|
||||
/// 对应官方 FEE_RECIPIENTS.md "Buyback (Applies to All)" 池,与主 fee_recipient 池互斥。
|
||||
pub const BUYBACK_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Public key for the Pump.fun program
|
||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
/// Public key for the MPL Token Metadata program
|
||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
||||
|
||||
/// Authority for program events
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
||||
|
||||
/// Associated Token Program ID
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
|
||||
pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
||||
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey =
|
||||
pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y");
|
||||
|
||||
pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt");
|
||||
|
||||
// META
|
||||
pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: PUMPFUN,
|
||||
@@ -156,17 +170,163 @@ pub mod accounts {
|
||||
};
|
||||
}
|
||||
|
||||
/// Instruction discriminators for PumpFun program
|
||||
// --- Anchor / layout constants ---------------------------------------
|
||||
|
||||
/// Minimum bonding curve account data length (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
pub struct Symbol;
|
||||
/// `buy_v2` — unified SOL/USDC quote interface ([pump-public-docs](https://github.com/pump-fun/pump-public-docs)).
|
||||
pub const BUY_V2_DISCRIMINATOR: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
|
||||
/// `sell_v2`
|
||||
pub const SELL_V2_DISCRIMINATOR: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
|
||||
/// `buy_exact_quote_in_v2` (native SOL spend for SOL-paired coins when `quote_mint` is WSOL)
|
||||
pub const BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
|
||||
|
||||
impl Symbol {
|
||||
pub const SOLANA: &'static str = "solana";
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
pub const SHARING_CONFIG_ACCOUNT_DISCRIMINATOR: [u8; 8] = [216, 74, 9, 0, 56, 140, 93, 75];
|
||||
|
||||
pub(crate) const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
|
||||
|
||||
// --- Fee recipient pools -----------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(pubkey)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
pubkey == &global_constants::PUMPFUN_AMM_FEE_1
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_2
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_3
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_4
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_5
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_6
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reconcile_mayhem_mode_for_trade(
|
||||
mayhem_from_event: Option<bool>,
|
||||
fee_recipient: &Pubkey,
|
||||
) -> bool {
|
||||
if *fee_recipient == Pubkey::default() {
|
||||
return mayhem_from_event.unwrap_or(false);
|
||||
}
|
||||
let fee_m = is_mayhem_fee_recipient(fee_recipient);
|
||||
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
|
||||
match mayhem_from_event {
|
||||
Some(log_m) => {
|
||||
if fee_m && !log_m {
|
||||
true
|
||||
} else if fee_s && log_m && !fee_m {
|
||||
false
|
||||
} else {
|
||||
log_m
|
||||
}
|
||||
}
|
||||
None => fee_m,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
|
||||
let is_m = is_mayhem_fee_recipient(pk);
|
||||
let is_s = is_standard_bonding_fee_recipient(pk);
|
||||
if is_mayhem_mode {
|
||||
!(is_s && !is_m)
|
||||
} else {
|
||||
!(is_m && !is_s)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::MAYHEM_FEE_RECIPIENTS[0]);
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
||||
const POOL: &[Pubkey] = &[
|
||||
global_constants::FEE_RECIPIENT,
|
||||
global_constants::PUMPFUN_AMM_FEE_1,
|
||||
global_constants::PUMPFUN_AMM_FEE_2,
|
||||
global_constants::PUMPFUN_AMM_FEE_3,
|
||||
global_constants::PUMPFUN_AMM_FEE_4,
|
||||
global_constants::PUMPFUN_AMM_FEE_5,
|
||||
global_constants::PUMPFUN_AMM_FEE_6,
|
||||
global_constants::PUMPFUN_AMM_FEE_7,
|
||||
];
|
||||
let recipient = *POOL.choose(&mut rand::rng()).unwrap_or(&global_constants::FEE_RECIPIENT);
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
/// Buyback fee recipient (#9 in buy_v2/sell_v2) — dedicated pool, distinct from protocol extra fee recipients.
|
||||
#[inline]
|
||||
pub fn get_buyback_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::BUYBACK_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::BUYBACK_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(
|
||||
observed_fee_recipient: Pubkey,
|
||||
is_mayhem_mode: bool,
|
||||
) -> AccountMeta {
|
||||
let trust_observation = observed_fee_recipient != Pubkey::default()
|
||||
&& fee_recipient_ok_for_bonding_curve_mode(&observed_fee_recipient, is_mayhem_mode);
|
||||
if trust_observation {
|
||||
AccountMeta { pubkey: observed_fee_recipient, is_signer: false, is_writable: true }
|
||||
} else if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
get_standard_fee_recipient_meta_random()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Extend bonding curve (cold path) --------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(
|
||||
bonding_curve: &Pubkey,
|
||||
user: &Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&EXTEND_ACCOUNT_DISCRIMINATOR,
|
||||
vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// --- Cached PDAs + creator_vault resolve ------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -174,13 +334,11 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_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)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// 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(
|
||||
@@ -188,8 +346,7 @@ pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
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)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -199,13 +356,11 @@ pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
|
||||
if creator_vault_pda.eq(&Pubkey::default()) {
|
||||
Pubkey::default()
|
||||
} else {
|
||||
// Fast check against cached default creator vault
|
||||
static DEFAULT_CREATOR_VAULT: std::sync::LazyLock<Option<Pubkey>> =
|
||||
std::sync::LazyLock::new(|| get_creator_vault_pda(&Pubkey::default()));
|
||||
if creator_vault_pda.eq(&DEFAULT_CREATOR_VAULT.unwrap()) {
|
||||
Pubkey::default()
|
||||
} else {
|
||||
*creator_vault_pda
|
||||
match DEFAULT_CREATOR_VAULT.as_ref() {
|
||||
Some(default) if creator_vault_pda.eq(default) => Pubkey::default(),
|
||||
_ => *creator_vault_pda,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,45 +372,90 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::CREATOR_VAULT_SEED, creator.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)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunFeeSharingConfig(*mint),
|
||||
|| {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn phantom_default_creator_vault() -> Pubkey {
|
||||
solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
|
||||
*pk == phantom_default_creator_vault()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Option<Pubkey> {
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
|
||||
}
|
||||
|
||||
/// Resolves Pump.fun bonding-curve buy/sell **account `#10` (`creator_vault`)** for ix assembly.
|
||||
///
|
||||
/// **Priority (highest first)**
|
||||
/// 1. **Explicit `creator_vault`** from ix / parser / cached observation when non-default and not the phantom
|
||||
/// sentinel — **always used as-is** (no remap to [`get_creator_vault_pda`] from `creator`);
|
||||
/// fee-sharing / multi-party layouts rely on upstream passing the vault the program expects (`pfee…` / `update_fee_shares`).
|
||||
/// 2. If ix vault missing: optional `fee_sharing_creator_vault_if_active` hint (non-default, non-phantom).
|
||||
/// 3. Else: [`get_creator_vault_pda`] from `creator` when `creator` is known.
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
_mint: &Pubkey,
|
||||
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
) -> Option<Pubkey> {
|
||||
let phantom = phantom_default_creator_vault();
|
||||
|
||||
if creator_vault_from_event != Pubkey::default() && creator_vault_from_event != phantom {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
|
||||
if let Some(v) = fee_sharing_creator_vault_if_active {
|
||||
if v != Pubkey::default() && v != phantom {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
if *creator == Pubkey::default() {
|
||||
return None;
|
||||
}
|
||||
|
||||
get_creator_vault_pda(creator)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunUserVolume(*user),
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let seed: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.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)
|
||||
Pubkey::try_find_program_address(seed, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
let bonding_curve =
|
||||
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_buy_price(
|
||||
amount: u64,
|
||||
@@ -275,3 +475,259 @@ pub fn get_buy_price(
|
||||
|
||||
s_u64.min(real_token_reserves)
|
||||
}
|
||||
|
||||
// --- RPC -------------------------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Option<Pubkey>, anyhow::Error> {
|
||||
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let acc = match rpc.get_account(&config_pda).await {
|
||||
Ok(a) => a,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
if acc.owner != accounts::FEE_PROGRAM {
|
||||
return Ok(None);
|
||||
}
|
||||
let d = acc.data.as_slice();
|
||||
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
|
||||
return Ok(None);
|
||||
}
|
||||
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
|
||||
return Ok(None);
|
||||
}
|
||||
let mint_on_chain = Pubkey::new_from_array(
|
||||
d[11..43].try_into().map_err(|_| anyhow!("SharingConfig mint slice"))?,
|
||||
);
|
||||
if mint_on_chain != *mint {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(get_creator_vault_pda(&config_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or_else(|| anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
// Use `deserialize` instead of `try_from_slice` so that extra trailing bytes
|
||||
// (from on-chain schema additions like new fields) are silently ignored.
|
||||
// `try_from_slice` requires the entire slice to be consumed, causing
|
||||
// "Not all bytes read" when the account has been extended.
|
||||
let mut bonding_curve = BondingCurveAccount::deserialize(&mut &account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to decode bonding curve account: {}", e))?;
|
||||
bonding_curve.account = bonding_curve_pda;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
global_constants, phantom_default_creator_vault, pump_fun_fee_recipient_meta,
|
||||
reconcile_mayhem_mode_for_trade, resolve_creator_vault_for_ix,
|
||||
resolve_creator_vault_for_ix_with_fee_sharing, *,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[test]
|
||||
fn pumpfun_discriminators_are_8_bytes() {
|
||||
assert_eq!(BUY_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_EXACT_SOL_IN_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(SELL_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_V2_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(SELL_V2_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_bonding_curve_and_v2_pda_differ_for_same_mint() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let pda = get_bonding_curve_pda(&mint).unwrap();
|
||||
let pda_v2 = get_bonding_curve_v2_pda(&mint).unwrap();
|
||||
assert_ne!(pda, pda_v2, "bonding_curve and bonding_curve_v2 PDAs must differ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpfun_creator_vault_pda_deterministic() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let a = get_creator_vault_pda(&creator).unwrap();
|
||||
let b = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_sharing_config_pda_deterministic() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let a = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let b = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_creator_yields_fixed_creator_vault() {
|
||||
let v = get_creator_vault_pda(&Pubkey::default()).unwrap();
|
||||
assert_eq!(
|
||||
v,
|
||||
phantom_default_creator_vault(),
|
||||
"phantom vault constant must match PDA(default creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_ix_vault_when_creator_borsh_is_default() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let ix_vault = Pubkey::new_unique();
|
||||
let resolved = resolve_creator_vault_for_ix(&Pubkey::default(), ix_vault, &mint);
|
||||
assert_eq!(resolved, Some(ix_vault));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_returns_none_when_creator_and_vault_missing() {
|
||||
let mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(&Pubkey::default(), Pubkey::default(), &mint),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() {
|
||||
let mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(
|
||||
&Pubkey::default(),
|
||||
phantom_default_creator_vault(),
|
||||
&mint,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_prefers_creator_pda_ix_vault_even_when_fee_sharing_hint_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, v_derived, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(v_derived),
|
||||
"observed ix uses PDA(creator); stale hint must not override → wrong vault / Anchor 2006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_trusts_ix_fee_sharing_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let creator_vault = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_ne!(vs, creator_vault);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(vs),
|
||||
"explicit ix creator_vault (e.g. fee-sharing #8) wins; no remap to PDA(creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_remaps_phantom_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let expected = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(&creator, phantom_default_creator_vault(), &mint),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_ix_vault_always_wins_when_non_default_even_if_creator_pda_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let ix_other = Pubkey::new_unique();
|
||||
assert_ne!(ix_other, v_derived);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, ix_other, &mint, None);
|
||||
assert_eq!(resolved, Some(ix_other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_fee_sharing_ix_vault_used_as_is_even_without_hint() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, None);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_falls_back_to_fee_sharing_hint_when_ix_vault_placeholder() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
Pubkey::default(),
|
||||
&mint,
|
||||
Some(vs),
|
||||
);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_false_but_fee_is_mayhem_pool() {
|
||||
let fee = global_constants::MAYHEM_FEE_RECIPIENTS[0];
|
||||
assert!(reconcile_mayhem_mode_for_trade(Some(false), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
let m = pump_fun_fee_recipient_meta(fee, true);
|
||||
assert!(
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(&m.pubkey),
|
||||
"expected fallback to mayhem pool, got {}",
|
||||
m.pubkey
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_uses_observed_standard_fee_for_standard_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_7;
|
||||
let m = pump_fun_fee_recipient_meta(fee, false);
|
||||
assert_eq!(m.pubkey, fee);
|
||||
assert!(m.is_writable);
|
||||
assert!(!m.is_signer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,20 @@ use crate::{
|
||||
common::{
|
||||
spl_associated_token_account::get_associated_token_address_with_program_id, SolanaRpcClient,
|
||||
},
|
||||
constants::TOKEN_PROGRAM,
|
||||
constants::{TOKEN_PROGRAM, WSOL_TOKEN_ACCOUNT},
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::warn;
|
||||
|
||||
// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22)
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
@@ -29,6 +37,10 @@ pub mod seeds {
|
||||
|
||||
/// Seed for pool v2 PDA (required by program upgrade, readonly at end of account list)
|
||||
pub const POOL_V2_SEED: &[u8] = b"pool-v2";
|
||||
/// Legacy pool PDA seed (used with index, creator, base_mint, quote_mint)
|
||||
pub const POOL_SEED: &[u8] = b"pool";
|
||||
/// Pump program: pool-authority PDA seed (creator for canonical pool)
|
||||
pub const POOL_AUTHORITY_SEED: &[u8] = b"pool-authority";
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
@@ -53,6 +65,8 @@ pub mod accounts {
|
||||
pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
|
||||
/// Pump Bonding Curve program(canonical pool 的 creator 来自此程序的 pool-authority PDA)
|
||||
pub const PUMP_PROGRAM_ID: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
pub const LP_FEE_BASIS_POINTS: u64 = 25;
|
||||
pub const PROTOCOL_FEE_BASIS_POINTS: u64 = 5;
|
||||
@@ -68,9 +82,33 @@ pub mod accounts {
|
||||
pub const DEFAULT_COIN_CREATOR_VAULT_AUTHORITY: Pubkey =
|
||||
pubkey!("8N3GDaZ2iwN65oxVatKTLPNooAVUJTbfiVJ1ahyqwjSk");
|
||||
|
||||
/// Mayhem fee recipient (for mayhem mode coins)
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey =
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS");
|
||||
/// Mayhem fee recipients (pump-public-docs: use any one randomly for throughput)
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
pubkey!("8SBKzEQU4nLSzcwF4a74F2iaUDQyTfjGndn6qUWBnrpR"),
|
||||
pubkey!("4UQeTP1T39KZ9Sfxzo3WR5skgsaP6NZa87BAkuazLEKH"),
|
||||
pubkey!("8sNeir4QsLsJdYpc9RZacohhK1Y5FLU3nC5LXgYB4aa6"),
|
||||
pubkey!("Fh9HmeLNUMVCvejxCtCL2DbYaRyBFVJ5xrWkLnMH6fdk"),
|
||||
pubkey!("463MEnMeGyJekNZFQSTUABBEbLnvMTALbT6ZmsxAbAdq"),
|
||||
pubkey!("6AUH3WEHucYZyC61hqpqYUWVto5qA5hjHuNQ32GNnNxA"),
|
||||
];
|
||||
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
|
||||
/// Buyback trailing fee recipients (`GlobalConfig.buyback_fee_recipients` on Pump AMM).
|
||||
/// Must match one of these for the pubkey passed after optional `pool-v2` (`@pump-fun/pump-swap-sdk` `getBuybackFeeRecipient`).
|
||||
/// Static mirror of pump-public-docs; if protocol rotates configs, decode global_config from RPC.
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
|
||||
// META
|
||||
|
||||
@@ -142,6 +180,182 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
const PUMPSWAP_GLOBAL_CONFIG_TTL: Duration = Duration::from_secs(90);
|
||||
const PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT: Duration = Duration::from_millis(180);
|
||||
|
||||
const PUBKEY_LEN: usize = 32;
|
||||
const U64_LEN: usize = 8;
|
||||
const U8_LEN: usize = 1;
|
||||
const BOOL_LEN: usize = 1;
|
||||
const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalConfig {
|
||||
pub protocol_fee_recipients: [Pubkey; 8],
|
||||
pub reserved_fee_recipient: Pubkey,
|
||||
pub reserved_fee_recipients: [Pubkey; 7],
|
||||
pub buyback_fee_recipients: [Pubkey; 8],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedGlobalConfig {
|
||||
fetched_at: Instant,
|
||||
config: GlobalConfig,
|
||||
}
|
||||
|
||||
static GLOBAL_CONFIG_CACHE: Lazy<RwLock<Option<CachedGlobalConfig>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
|
||||
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
|
||||
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
|
||||
Some(Pubkey::new_from_array(bytes.try_into().ok()?))
|
||||
}
|
||||
|
||||
fn read_pubkey_array<const N: usize>(data: &[u8], offset: usize) -> Option<[Pubkey; N]> {
|
||||
let mut keys = [Pubkey::default(); N];
|
||||
for (i, key) in keys.iter_mut().enumerate() {
|
||||
*key = read_pubkey(data, offset + i * PUBKEY_LEN)?;
|
||||
}
|
||||
Some(keys)
|
||||
}
|
||||
|
||||
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
|
||||
let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN;
|
||||
offset += PUBKEY_LEN; // admin
|
||||
offset += U64_LEN * 2; // lp_fee_basis_points + protocol_fee_basis_points
|
||||
offset += U8_LEN; // disable_flags
|
||||
|
||||
let protocol_fee_recipients = read_pubkey_array::<8>(data, offset)?;
|
||||
offset += PUBKEY_LEN * 8;
|
||||
offset += U64_LEN; // coin_creator_fee_basis_points
|
||||
offset += PUBKEY_LEN; // admin_set_coin_creator_authority
|
||||
offset += PUBKEY_LEN; // whitelist_pda
|
||||
|
||||
let reserved_fee_recipient = read_pubkey(data, offset)?;
|
||||
offset += PUBKEY_LEN;
|
||||
offset += BOOL_LEN; // mayhem_mode_enabled
|
||||
|
||||
let reserved_fee_recipients = read_pubkey_array::<7>(data, offset)?;
|
||||
offset += PUBKEY_LEN * 7;
|
||||
offset += BOOL_LEN; // is_cashback_enabled
|
||||
|
||||
let buyback_fee_recipients = read_pubkey_array::<8>(data, offset)?;
|
||||
|
||||
Some(GlobalConfig {
|
||||
protocol_fee_recipients,
|
||||
reserved_fee_recipient,
|
||||
reserved_fee_recipients,
|
||||
buyback_fee_recipients,
|
||||
})
|
||||
}
|
||||
|
||||
async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfig> {
|
||||
let account = match tokio::time::timeout(
|
||||
PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT,
|
||||
rpc.get_account(&accounts::GLOBAL_ACCOUNT),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(account)) => account,
|
||||
Ok(Err(e)) => {
|
||||
warn!(target: "pumpswap_global_config", "PumpSwap GlobalConfig 读取失败: {}", e);
|
||||
return None;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
timeout_ms = PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT.as_millis(),
|
||||
"PumpSwap GlobalConfig 读取超时"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(config) = decode_global_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
data_len = account.data.len(),
|
||||
"PumpSwap GlobalConfig 解析失败"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
*GLOBAL_CONFIG_CACHE.write() =
|
||||
Some(CachedGlobalConfig { fetched_at: Instant::now(), config: config.clone() });
|
||||
Some(config)
|
||||
}
|
||||
|
||||
pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
|
||||
let Some(rpc) = rpc else {
|
||||
return;
|
||||
};
|
||||
let stale = GLOBAL_CONFIG_CACHE
|
||||
.read()
|
||||
.as_ref()
|
||||
.map(|c| c.fetched_at.elapsed() > PUMPSWAP_GLOBAL_CONFIG_TTL)
|
||||
.unwrap_or(true);
|
||||
if stale {
|
||||
let _ = refresh_global_config_once(rpc.as_ref()).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_global_config() -> Option<GlobalConfig> {
|
||||
let guard = GLOBAL_CONFIG_CACHE.read();
|
||||
let cached = guard.as_ref()?;
|
||||
(cached.fetched_at.elapsed() <= PUMPSWAP_GLOBAL_CONFIG_TTL).then(|| cached.config.clone())
|
||||
}
|
||||
|
||||
fn choose_nonzero(keys: &[Pubkey]) -> Option<Pubkey> {
|
||||
let mut valid = [Pubkey::default(); 8];
|
||||
let mut len = 0;
|
||||
for key in keys.iter().copied() {
|
||||
if key == Pubkey::default() || len == valid.len() {
|
||||
continue;
|
||||
}
|
||||
valid[len] = key;
|
||||
len += 1;
|
||||
}
|
||||
valid[..len].choose(&mut rand::rng()).copied()
|
||||
}
|
||||
|
||||
/// Returns a random Mayhem fee recipient and its AccountMeta (pump-public-docs: use any one randomly).
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
|
||||
let recipient = cached_global_config()
|
||||
.and_then(|config| {
|
||||
let mut pool = [Pubkey::default(); 8];
|
||||
pool[0] = config.reserved_fee_recipient;
|
||||
pool[1..].copy_from_slice(&config.reserved_fee_recipients);
|
||||
choose_nonzero(&pool)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
*accounts::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0])
|
||||
});
|
||||
let meta = AccountMeta { pubkey: recipient, is_signer: false, is_writable: false };
|
||||
(recipient, meta)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_protocol_fee_recipient_random() -> Pubkey {
|
||||
cached_global_config()
|
||||
.and_then(|config| choose_nonzero(&config.protocol_fee_recipients))
|
||||
.unwrap_or(accounts::FEE_RECIPIENT)
|
||||
}
|
||||
|
||||
/// Random entry from [`accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (readonly; paired with [`fee_recipient_ata`] as last account).
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
cached_global_config()
|
||||
.and_then(|config| choose_nonzero(&config.buyback_fee_recipients))
|
||||
.unwrap_or_else(|| {
|
||||
*accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
})
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
@@ -152,6 +366,35 @@ pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||
Some(pda)
|
||||
}
|
||||
|
||||
/// Pump 程序上的 pool-authority PDA(canonical pool 的 creator),与 @pump-fun/pump-swap-sdk 一致。
|
||||
#[inline]
|
||||
pub fn get_pump_pool_authority_pda(mint: &Pubkey) -> Pubkey {
|
||||
Pubkey::find_program_address(
|
||||
&[seeds::POOL_AUTHORITY_SEED, mint.as_ref()],
|
||||
&accounts::PUMP_PROGRAM_ID,
|
||||
)
|
||||
.0
|
||||
}
|
||||
|
||||
/// Canonical Pump 池 PDA:index=0,creator=pumpPoolAuthorityPda(mint),base_mint=mint,quote_mint=WSOL。
|
||||
/// 与 @pump-fun/pump-swap-sdk 的 canonicalPumpPoolPda(mint) 一致,用于从 bonding curve 迁移后的标准池查找。
|
||||
#[inline]
|
||||
pub fn get_canonical_pool_pda(mint: &Pubkey) -> Pubkey {
|
||||
const CANONICAL_POOL_INDEX: u16 = 0;
|
||||
let authority = get_pump_pool_authority_pda(mint);
|
||||
let (pda, _) = Pubkey::find_program_address(
|
||||
&[
|
||||
seeds::POOL_SEED,
|
||||
&CANONICAL_POOL_INDEX.to_le_bytes(),
|
||||
authority.as_ref(),
|
||||
mint.as_ref(),
|
||||
WSOL_TOKEN_ACCOUNT.as_ref(),
|
||||
],
|
||||
&accounts::AMM_PROGRAM,
|
||||
);
|
||||
pda
|
||||
}
|
||||
|
||||
// Find a pool for a specific mint
|
||||
pub async fn find_pool(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<Pubkey, anyhow::Error> {
|
||||
let (pool_address, _) = find_by_mint(rpc, mint).await?;
|
||||
@@ -191,14 +434,14 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::PdaCacheKey::PumpSwapUserVolume(*user),
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[&seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let program_id: &Pubkey = &&accounts::AMM_PROGRAM;
|
||||
let program_id: &Pubkey = &accounts::AMM_PROGRAM;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// WSOL ATA of UserVolumeAccumulator for Pump AMM (used for cashback remaining accounts).
|
||||
/// WSOL ATA of UserVolumeAccumulator for Pump AMM (buy cashback: remaining_accounts[0] 官方用 NATIVE_MINT).
|
||||
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(
|
||||
@@ -208,9 +451,23 @@ pub fn get_user_volume_accumulator_wsol_ata(user: &Pubkey) -> Option<Pubkey> {
|
||||
))
|
||||
}
|
||||
|
||||
/// Quote-mint ATA of UserVolumeAccumulator(sell cashback 时官方用 quoteMint,非固定 WSOL).
|
||||
pub fn get_user_volume_accumulator_quote_ata(
|
||||
user: &Pubkey,
|
||||
quote_mint: &Pubkey,
|
||||
quote_token_program: &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,
|
||||
quote_mint,
|
||||
quote_token_program,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn get_global_volume_accumulator_pda() -> Option<Pubkey> {
|
||||
let seeds: &[&[u8]; 1] = &[&seeds::GLOBAL_VOLUME_ACCUMULATOR_SEED];
|
||||
let program_id: &Pubkey = &&accounts::AMM_PROGRAM;
|
||||
let program_id: &Pubkey = &accounts::AMM_PROGRAM;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
}
|
||||
@@ -227,19 +484,23 @@ pub async fn fetch_pool(
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn find_by_base_mint(
|
||||
/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022)
|
||||
const POOL_DATA_LEN_SPL: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_T22: u64 = 643;
|
||||
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel.
|
||||
async fn get_program_accounts_both_sizes(
|
||||
rpc: &SolanaRpcClient,
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// Use getProgramAccounts to find pools for the given mint
|
||||
let filters = vec![
|
||||
// solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool account size
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, &base_mint.to_bytes()),
|
||||
),
|
||||
];
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(filters),
|
||||
memcmp_offset: usize,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Vec<(Pubkey, solana_sdk::account::Account)>, anyhow::Error> {
|
||||
let make_config = |data_size: u64| solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
|
||||
),
|
||||
]),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
@@ -251,95 +512,114 @@ pub async fn find_by_base_mint(
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||
}
|
||||
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
|
||||
let mut pools: Vec<_> = accounts
|
||||
let (spl_result, t22_result) = tokio::join!(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)),
|
||||
);
|
||||
let mut all = spl_result.unwrap_or_default();
|
||||
all.extend(t22_result.unwrap_or_default());
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
fn decode_pool_accounts(
|
||||
accounts: Vec<(Pubkey, solana_sdk::account::Account)>,
|
||||
) -> Vec<(Pubkey, Pool)> {
|
||||
accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
// 🔧 修复:跳过8字节的discriminator
|
||||
if acc.data.len() > 8 {
|
||||
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
|
||||
if pools.is_empty() {
|
||||
return Err(anyhow!("No valid pool decoded for mint {} (found {} accounts but all decode failed)", base_mint, accounts_count));
|
||||
pub async fn find_by_base_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||
}
|
||||
let mut pools = decode_pool_accounts(accounts);
|
||||
if pools.is_empty() {
|
||||
return Err(anyhow!("No valid pool decoded for mint {}", base_mint));
|
||||
}
|
||||
|
||||
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
|
||||
let (address, pool) = pools[0].clone();
|
||||
Ok((address, pool))
|
||||
Ok((pools[0].0, pools[0].1.clone()))
|
||||
}
|
||||
|
||||
pub async fn find_by_quote_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// Use getProgramAccounts to find pools for the given mint
|
||||
let filters = vec![
|
||||
// solana_rpc_client_api::filter::RpcFilterType::DataSize(211), // Pool account size
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(75, "e_mint.to_bytes()),
|
||||
),
|
||||
];
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(filters),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
commitment: None,
|
||||
min_context_slot: None,
|
||||
},
|
||||
with_context: None,
|
||||
sort_results: None,
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||
// quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", quote_mint));
|
||||
}
|
||||
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
|
||||
let mut pools: Vec<_> = accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
// 🔧 修复:跳过8字节的discriminator
|
||||
if acc.data.len() > 8 {
|
||||
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
|
||||
let mut pools = decode_pool_accounts(accounts);
|
||||
if pools.is_empty() {
|
||||
return Err(anyhow!("No valid pool decoded for quote_mint {} (found {} accounts but all decode failed)", quote_mint, accounts_count));
|
||||
return Err(anyhow!("No valid pool decoded for quote_mint {}", quote_mint));
|
||||
}
|
||||
|
||||
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
|
||||
let (address, pool) = pools[0].clone();
|
||||
Ok((address, pool))
|
||||
Ok((pools[0].0, pools[0].1.clone()))
|
||||
}
|
||||
|
||||
/// 按 mint 查找 PumpSwap 池(本函数仅用于 PumpSwap,其他 DEX 勿用)。
|
||||
///
|
||||
/// 查找顺序(与 @pump-fun/pump-swap-sdk 一致):
|
||||
/// 1. Pool v2 PDA ["pool-v2", base_mint] — 一次 getAccount
|
||||
/// 2. Canonical pool PDA ["pool", 0, pumpPoolAuthority(mint), mint, WSOL] — 迁移后的标准池
|
||||
/// 3. getProgramAccounts 按 base_mint / quote_mint 过滤
|
||||
pub async fn find_by_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
if let Ok((address, pool)) = find_by_base_mint(rpc, mint).await {
|
||||
return Ok((address, pool));
|
||||
let mut diag = Vec::<String>::new();
|
||||
|
||||
// 1. PumpSwap v2 PDA(seeds: ["pool-v2", base_mint])
|
||||
if let Some(pool_address) = get_pool_v2_pda(mint) {
|
||||
diag.push(format!("PDA(v2)={}", pool_address));
|
||||
match fetch_pool(rpc, &pool_address).await {
|
||||
Ok(pool) if pool.base_mint == *mint => return Ok((pool_address, pool)),
|
||||
Ok(_) => diag.push("PDA(v2) 账户存在但 base_mint 不匹配".into()),
|
||||
Err(e) => diag.push(format!("PDA(v2) get_account/decode 失败: {}", e)),
|
||||
}
|
||||
}
|
||||
if let Ok((address, pool)) = find_by_quote_mint(rpc, mint).await {
|
||||
return Ok((address, pool));
|
||||
|
||||
// 2. Canonical pool PDA(与 pump-swap-sdk canonicalPumpPoolPda(mint) 一致)
|
||||
let canonical_address = get_canonical_pool_pda(mint);
|
||||
diag.push(format!("canonical={}", canonical_address));
|
||||
match fetch_pool(rpc, &canonical_address).await {
|
||||
Ok(pool) if pool.base_mint == *mint => return Ok((canonical_address, pool)),
|
||||
Ok(_) => diag.push("canonical 账户存在但 base_mint 不匹配".into()),
|
||||
Err(e) => diag.push(format!("canonical get_account/decode 失败: {}", e)),
|
||||
}
|
||||
Err(anyhow!("No pool found for mint {}", mint))
|
||||
|
||||
// 3. Fallback: getProgramAccounts by base_mint / quote_mint (with 3s timeout to avoid blocking)
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_base_mint(rpc, mint))
|
||||
.await
|
||||
{
|
||||
Ok(Ok((address, pool))) => return Ok((address, pool)),
|
||||
Ok(Err(e)) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
|
||||
Err(_) => diag.push("getProgramAccounts(base_mint): timed out (3s)".into()),
|
||||
}
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_quote_mint(rpc, mint))
|
||||
.await
|
||||
{
|
||||
Ok(Ok((address, pool))) => return Ok((address, pool)),
|
||||
Ok(Err(e)) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
|
||||
Err(_) => diag.push("getProgramAccounts(quote_mint): timed out (3s)".into()),
|
||||
}
|
||||
|
||||
let diag_str = diag.join("; ");
|
||||
eprintln!("[find_by_mint] {} failed: {}", mint, diag_str);
|
||||
Err(anyhow!("No pool found for mint {}. diag: {}", mint, diag_str))
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
@@ -362,3 +642,31 @@ pub fn get_fee_config_pda() -> Option<Pubkey> {
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[test]
|
||||
fn pumpswap_user_volume_accumulator_pda_deterministic() {
|
||||
let user = Pubkey::new_unique();
|
||||
let a = get_user_volume_accumulator_pda(&user).unwrap();
|
||||
let b = get_user_volume_accumulator_pda(&user).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpswap_global_volume_accumulator_matches_constant() {
|
||||
let pda = get_global_volume_accumulator_pda().unwrap();
|
||||
assert_eq!(pda, accounts::GLOBAL_VOLUME_ACCUMULATOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpswap_pool_v2_pda_deterministic() {
|
||||
let base_mint = Pubkey::new_unique();
|
||||
let a = get_pool_v2_pda(&base_mint).unwrap();
|
||||
let b = get_pool_v2_pda(&base_mint).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@ pub struct Pool {
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Whether this pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
/// Reserved for future fields (pump-public-docs: pool structure = 244 bytes total)
|
||||
pub _reserved: [u8; 7],
|
||||
}
|
||||
|
||||
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
|
||||
/// Borsh 解码用的 Pool 长度。链上池为 244 字节(pump-public-docs Breaking Change),与 POOL_SIZE 一致。
|
||||
pub const POOL_SIZE: usize = 244;
|
||||
|
||||
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||
if data.len() < POOL_SIZE {
|
||||
|
||||
@@ -135,9 +135,11 @@ pub fn get_vault_account(
|
||||
) -> Pubkey {
|
||||
if protocol_params.base_mint == *token_mint && protocol_params.base_vault != Pubkey::default() {
|
||||
protocol_params.base_vault
|
||||
} else if protocol_params.quote_mint == *token_mint && protocol_params.quote_vault != Pubkey::default() {
|
||||
} else if protocol_params.quote_mint == *token_mint
|
||||
&& protocol_params.quote_vault != Pubkey::default()
|
||||
{
|
||||
protocol_params.quote_vault
|
||||
} else {
|
||||
get_vault_pda(pool_state, token_mint).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+7
-985
@@ -1,3 +1,4 @@
|
||||
pub mod client;
|
||||
pub mod common;
|
||||
pub mod constants;
|
||||
pub mod instruction;
|
||||
@@ -5,989 +6,10 @@ pub mod perf;
|
||||
pub mod swqos;
|
||||
pub mod trading;
|
||||
pub mod utils;
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::sdk_log;
|
||||
use crate::common::GasFeeStrategy;
|
||||
use crate::common::{TradeConfig, InfrastructureConfig};
|
||||
#[cfg(feature = "perf-trace")]
|
||||
use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
|
||||
use crate::constants::SOL_TOKEN_ACCOUNT;
|
||||
use crate::constants::USD1_TOKEN_ACCOUNT;
|
||||
use crate::constants::USDC_TOKEN_ACCOUNT;
|
||||
use crate::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use crate::swqos::common::TradeError;
|
||||
use crate::swqos::SwqosClient;
|
||||
use crate::swqos::SwqosConfig;
|
||||
use crate::swqos::TradeType;
|
||||
use crate::trading::core::params::BonkParams;
|
||||
use crate::trading::core::params::MeteoraDammV2Params;
|
||||
use crate::trading::core::params::PumpFunParams;
|
||||
use crate::trading::core::params::PumpSwapParams;
|
||||
use crate::trading::core::params::RaydiumAmmV4Params;
|
||||
use crate::trading::core::params::RaydiumCpmmParams;
|
||||
use crate::trading::core::params::DexParamEnum;
|
||||
use crate::trading::factory::DexType;
|
||||
use crate::trading::MiddlewareManager;
|
||||
use crate::trading::SwapParams;
|
||||
use crate::trading::TradeFactory;
|
||||
use common::SolanaRpcClient;
|
||||
use parking_lot::Mutex;
|
||||
use rustls::crypto::{ring::default_provider, CryptoProvider};
|
||||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::message::AddressLookupTableAccount;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature};
|
||||
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
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub enum TradeTokenType {
|
||||
SOL,
|
||||
WSOL,
|
||||
USD1,
|
||||
USDC,
|
||||
}
|
||||
|
||||
/// Shared infrastructure components that can be reused across multiple wallets
|
||||
///
|
||||
/// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients)
|
||||
/// that are wallet-independent and can be shared when only the trading wallet changes.
|
||||
pub struct TradingInfrastructure {
|
||||
/// Shared RPC client for blockchain interactions
|
||||
pub rpc: Arc<SolanaRpcClient>,
|
||||
/// Shared SWQOS clients for transaction priority and routing
|
||||
pub swqos_clients: Vec<Arc<SwqosClient>>,
|
||||
/// Configuration used to create this infrastructure
|
||||
pub config: InfrastructureConfig,
|
||||
}
|
||||
|
||||
impl TradingInfrastructure {
|
||||
/// Create new shared infrastructure from configuration
|
||||
///
|
||||
/// This performs the expensive initialization:
|
||||
/// - Creates RPC client with connection pool
|
||||
/// - Creates SWQOS clients (each with their own HTTP client)
|
||||
/// - Initializes rent cache and starts background updater
|
||||
pub async fn new(config: InfrastructureConfig) -> Self {
|
||||
// Install crypto provider (idempotent)
|
||||
if CryptoProvider::get_default().is_none() {
|
||||
let _ = default_provider()
|
||||
.install_default()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
|
||||
}
|
||||
|
||||
// Create RPC client
|
||||
let rpc = Arc::new(SolanaRpcClient::new_with_commitment(
|
||||
config.rpc_url.clone(),
|
||||
config.commitment.clone(),
|
||||
));
|
||||
|
||||
// Initialize rent cache and start background updater
|
||||
common::seed::update_rents(&rpc).await.unwrap();
|
||||
common::seed::start_rent_updater(rpc.clone());
|
||||
|
||||
// Create SWQOS clients with blacklist checking
|
||||
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
|
||||
for swqos in &config.swqos_configs {
|
||||
// Check blacklist, skip disabled providers
|
||||
if swqos.is_blacklisted() {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match SwqosConfig::get_swqos_client(
|
||||
config.rpc_url.clone(),
|
||||
config.commitment.clone(),
|
||||
swqos.clone(),
|
||||
).await {
|
||||
Ok(swqos_client) => swqos_clients.push(swqos_client),
|
||||
Err(err) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(
|
||||
target: "sol_trade_sdk",
|
||||
"failed to create {:?} swqos client: {err}. Excluding from swqos list",
|
||||
swqos.swqos_type()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self {
|
||||
rpc,
|
||||
swqos_clients,
|
||||
config,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Main trading client for Solana DeFi protocols
|
||||
///
|
||||
/// `SolTradingSDK` provides a unified interface for trading across multiple Solana DEXs
|
||||
/// including PumpFun, PumpSwap, Bonk, Raydium AMM V4, and Raydium CPMM.
|
||||
/// It manages RPC connections, transaction signing, and SWQOS (Solana Web Quality of Service) settings.
|
||||
pub struct TradingClient {
|
||||
/// The keypair used for signing all transactions
|
||||
pub payer: Arc<Keypair>,
|
||||
/// Shared infrastructure (RPC client, SWQOS clients)
|
||||
/// Can be shared across multiple TradingClient instances with different wallets
|
||||
pub infrastructure: Arc<TradingInfrastructure>,
|
||||
/// Optional middleware manager for custom transaction processing
|
||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
/// Whether to use seed optimization for all ATA operations (default: true)
|
||||
/// Applies to all token account creations across buy and sell operations
|
||||
pub use_seed_optimize: bool,
|
||||
/// Whether to pin parallel submit tasks to CPU cores (from TradeConfig.use_core_affinity). Default true.
|
||||
pub use_core_affinity: bool,
|
||||
/// Whether to output all SDK logs (from TradeConfig.log_enabled).
|
||||
pub log_enabled: bool,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
}
|
||||
|
||||
static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
|
||||
|
||||
/// 🔄 向后兼容:SolanaTrade 别名
|
||||
pub type SolanaTrade = TradingClient;
|
||||
|
||||
impl Clone for TradingClient {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
payer: self.payer.clone(),
|
||||
infrastructure: self.infrastructure.clone(),
|
||||
middleware_manager: self.middleware_manager.clone(),
|
||||
use_seed_optimize: self.use_seed_optimize,
|
||||
use_core_affinity: self.use_core_affinity,
|
||||
log_enabled: self.log_enabled,
|
||||
check_min_tip: self.check_min_tip,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for executing buy orders across different DEX protocols
|
||||
///
|
||||
/// Contains all necessary configuration for purchasing tokens, including
|
||||
/// protocol-specific settings, account management options, and transaction preferences.
|
||||
#[derive(Clone)]
|
||||
pub struct TradeBuyParams {
|
||||
// Trading configuration
|
||||
/// The DEX protocol to use for the trade
|
||||
pub dex_type: DexType,
|
||||
/// Type of the token to buy
|
||||
pub input_token_type: TradeTokenType,
|
||||
/// Public key of the token to purchase
|
||||
pub mint: Pubkey,
|
||||
/// Amount of tokens to buy (in smallest token units)
|
||||
pub input_token_amount: u64,
|
||||
/// Optional slippage tolerance in basis points (e.g., 100 = 1%)
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
/// Recent blockhash for transaction validity
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
/// Protocol-specific parameters (PumpFun, Raydium, etc.)
|
||||
pub extension_params: DexParamEnum,
|
||||
// Extended configuration
|
||||
/// Optional address lookup table for transaction size optimization
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
/// Whether to wait for transaction confirmation before returning
|
||||
pub wait_transaction_confirmed: bool,
|
||||
/// Whether to create input token associated token account
|
||||
pub create_input_token_ata: bool,
|
||||
/// Whether to close input token associated token account after trade
|
||||
pub close_input_token_ata: bool,
|
||||
/// Whether to create token mint associated token account
|
||||
pub create_mint_ata: bool,
|
||||
/// Durable nonce information
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
|
||||
pub fixed_output_token_amount: Option<u64>,
|
||||
/// Gas fee strategy
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
/// Whether to simulate the transaction instead of executing it
|
||||
pub simulate: bool,
|
||||
/// 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(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.
|
||||
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
|
||||
///
|
||||
/// Contains all necessary configuration for selling tokens, including
|
||||
/// protocol-specific settings, tip preferences, account management options, and transaction preferences.
|
||||
#[derive(Clone)]
|
||||
pub struct TradeSellParams {
|
||||
// Trading configuration
|
||||
/// The DEX protocol to use for the trade
|
||||
pub dex_type: DexType,
|
||||
/// Type of the token to sell
|
||||
pub output_token_type: TradeTokenType,
|
||||
/// Public key of the token to sell
|
||||
pub mint: Pubkey,
|
||||
/// Amount of tokens to sell (in smallest token units)
|
||||
pub input_token_amount: u64,
|
||||
/// Optional slippage tolerance in basis points (e.g., 100 = 1%)
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
/// Recent blockhash for transaction validity
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
/// Whether to include tip for transaction priority
|
||||
pub with_tip: bool,
|
||||
/// Protocol-specific parameters (PumpFun, Raydium, etc.)
|
||||
pub extension_params: DexParamEnum,
|
||||
// Extended configuration
|
||||
/// Optional address lookup table for transaction size optimization
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
/// Whether to wait for transaction confirmation before returning
|
||||
pub wait_transaction_confirmed: bool,
|
||||
/// Whether to create output token associated token account
|
||||
pub create_output_token_ata: bool,
|
||||
/// Whether to close output token associated token account after trade
|
||||
pub close_output_token_ata: bool,
|
||||
/// Whether to close mint token associated token account after trade
|
||||
pub close_mint_token_ata: bool,
|
||||
/// Durable nonce information
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
/// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
|
||||
pub fixed_output_token_amount: Option<u64>,
|
||||
/// Gas fee strategy
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
/// Whether to simulate the transaction instead of executing it
|
||||
pub simulate: bool,
|
||||
/// 可选:事件收到时间(微秒,与 sol-parser-sdk clock 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点。
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
}
|
||||
|
||||
impl TradingClient {
|
||||
/// Create a TradingClient from shared infrastructure (fast path)
|
||||
///
|
||||
/// This is the preferred method when multiple wallets share the same infrastructure.
|
||||
/// It only performs wallet-specific initialization (fast_init) without the expensive
|
||||
/// RPC/SWQOS client creation.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `payer` - The keypair used for signing transactions
|
||||
/// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients)
|
||||
/// * `use_seed_optimize` - Whether to use seed optimization for ATA operations
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns a configured `TradingClient` instance ready for trading operations
|
||||
pub fn from_infrastructure(
|
||||
payer: Arc<Keypair>,
|
||||
infrastructure: Arc<TradingInfrastructure>,
|
||||
use_seed_optimize: bool,
|
||||
) -> Self {
|
||||
// Initialize wallet-specific caches (fast, synchronous)
|
||||
crate::common::fast_fn::fast_init(&payer.pubkey());
|
||||
|
||||
Self {
|
||||
payer,
|
||||
infrastructure,
|
||||
middleware_manager: None,
|
||||
use_seed_optimize,
|
||||
use_core_affinity: true,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a TradingClient from shared infrastructure with optional WSOL ATA setup
|
||||
///
|
||||
/// Same as `from_infrastructure` but also handles WSOL ATA creation if requested.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `payer` - The keypair used for signing transactions
|
||||
/// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients)
|
||||
/// * `use_seed_optimize` - Whether to use seed optimization for ATA operations
|
||||
/// * `create_wsol_ata` - Whether to check/create WSOL ATA
|
||||
pub async fn from_infrastructure_with_wsol_setup(
|
||||
payer: Arc<Keypair>,
|
||||
infrastructure: Arc<TradingInfrastructure>,
|
||||
use_seed_optimize: bool,
|
||||
create_wsol_ata: bool,
|
||||
) -> Self {
|
||||
crate::common::fast_fn::fast_init(&payer.pubkey());
|
||||
|
||||
if create_wsol_ata {
|
||||
// 在后台异步创建 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 {
|
||||
payer,
|
||||
infrastructure,
|
||||
middleware_manager: None,
|
||||
use_seed_optimize,
|
||||
use_core_affinity: true,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to ensure WSOL ATA exists for a wallet
|
||||
async fn ensure_wsol_ata(payer: &Arc<Keypair>, rpc: &Arc<SolanaRpcClient>) {
|
||||
let wsol_ata =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&payer.pubkey(),
|
||||
&WSOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
match rpc.get_account(&wsol_ata).await {
|
||||
Ok(_) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
info!(target: "sol_trade_sdk", "✅ WSOL ATA already exists: {}", wsol_ata);
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(_) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
info!(target: "sol_trade_sdk", "🔨 Creating WSOL ATA: {}", wsol_ata);
|
||||
}
|
||||
let create_ata_ixs =
|
||||
crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey());
|
||||
|
||||
if !create_ata_ixs.is_empty() {
|
||||
use solana_sdk::transaction::Transaction;
|
||||
|
||||
// 重试逻辑:最多尝试3次,每次超时10秒
|
||||
const MAX_RETRIES: usize = 3;
|
||||
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;
|
||||
}
|
||||
|
||||
let recent_blockhash = match rpc.get_latest_blockhash().await {
|
||||
Ok(hash) => hash,
|
||||
Err(e) => {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
warn!(target: "sol_trade_sdk", "⚠️ Failed to get latest blockhash: {}", e);
|
||||
}
|
||||
last_error = Some(format!("Failed to get blockhash: {}", e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
if sdk_log::sdk_log_enabled() {
|
||||
info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA already exists (no need to create)");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new SolTradingSDK instance with the specified configuration
|
||||
///
|
||||
/// This function initializes the trading system with RPC connection, SWQOS settings,
|
||||
/// and sets up necessary components for trading operations.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `payer` - The keypair used for signing transactions
|
||||
/// * `trade_config` - Trading configuration including RPC URL, SWQOS settings, etc.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns a configured `SolTradingSDK` instance ready for trading operations
|
||||
#[inline]
|
||||
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
|
||||
let infra_config = InfrastructureConfig::from_trade_config(&trade_config);
|
||||
let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
|
||||
|
||||
// Initialize wallet-specific caches
|
||||
crate::common::fast_fn::fast_init(&payer.pubkey());
|
||||
|
||||
// Handle WSOL ATA creation if configured
|
||||
if trade_config.create_wsol_ata_on_startup {
|
||||
Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await;
|
||||
}
|
||||
|
||||
let instance = Self {
|
||||
payer,
|
||||
infrastructure,
|
||||
middleware_manager: None,
|
||||
use_seed_optimize: trade_config.use_seed_optimize,
|
||||
use_core_affinity: trade_config.use_core_affinity,
|
||||
log_enabled: trade_config.log_enabled,
|
||||
check_min_tip: trade_config.check_min_tip,
|
||||
};
|
||||
|
||||
let mut current = INSTANCE.lock();
|
||||
*current = Some(Arc::new(instance.clone()));
|
||||
|
||||
instance
|
||||
}
|
||||
|
||||
/// Adds a middleware manager to the SolanaTrade instance
|
||||
///
|
||||
/// Middleware managers can be used to implement custom logic that runs before or after trading operations,
|
||||
/// such as logging, monitoring, or custom validation.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `middleware_manager` - The middleware manager to attach
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the modified SolanaTrade instance with middleware manager attached
|
||||
pub fn with_middleware_manager(mut self, middleware_manager: MiddlewareManager) -> Self {
|
||||
self.middleware_manager = Some(Arc::new(middleware_manager));
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets the RPC client instance for direct Solana blockchain interactions
|
||||
///
|
||||
/// This provides access to the underlying Solana RPC client that can be used
|
||||
/// for custom blockchain operations outside of the trading framework.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns a reference to the Arc-wrapped SolanaRpcClient instance
|
||||
pub fn get_rpc(&self) -> &Arc<SolanaRpcClient> {
|
||||
&self.infrastructure.rpc
|
||||
}
|
||||
|
||||
/// Gets the current globally shared SolanaTrade instance
|
||||
///
|
||||
/// This provides access to the singleton instance that was created with `new()`.
|
||||
/// Useful for accessing the trading instance from different parts of the application.
|
||||
///
|
||||
/// # Returns
|
||||
/// Returns the Arc-wrapped SolanaTrade instance
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if no instance has been initialized yet. Make sure to call `new()` first.
|
||||
pub fn get_instance() -> Arc<Self> {
|
||||
let instance = INSTANCE.lock();
|
||||
instance
|
||||
.as_ref()
|
||||
.expect("SolanaTrade instance not initialized. Please call new() first.")
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Execute a buy order for a specified token
|
||||
///
|
||||
/// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
|
||||
/// - bool: 是否至少有一个交易成功
|
||||
/// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
|
||||
/// - Option<TradeError>: 最后一个错误(如果全部失败)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - Buy trade parameters containing all necessary trading configuration
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok((bool, Vec<Signature>, Option<TradeError>))` with success flag and all transaction signatures,
|
||||
/// or an error if the transaction fails.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - Invalid protocol parameters are provided for the specified DEX type
|
||||
/// - The transaction fails to execute
|
||||
/// - Network or RPC errors occur
|
||||
/// - Insufficient SOL balance for the purchase
|
||||
/// - Required accounts cannot be created or accessed
|
||||
#[inline]
|
||||
pub async fn buy(
|
||||
&self,
|
||||
params: TradeBuyParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||
#[cfg(feature = "perf-trace")]
|
||||
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||
debug!(
|
||||
target: "sol_trade_sdk",
|
||||
"slippage_basis_points is none, use default slippage basis points: {}",
|
||||
DEFAULT_SLIPPAGE
|
||||
);
|
||||
}
|
||||
if params.input_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk {
|
||||
return Err(anyhow::anyhow!(
|
||||
" Current version only support USD1 trading on Bonk protocols"
|
||||
));
|
||||
}
|
||||
let input_token_mint = if params.input_token_type == TradeTokenType::SOL {
|
||||
SOL_TOKEN_ACCOUNT
|
||||
} else if params.input_token_type == TradeTokenType::WSOL {
|
||||
WSOL_TOKEN_ACCOUNT
|
||||
} else if params.input_token_type == TradeTokenType::USDC {
|
||||
USDC_TOKEN_ACCOUNT
|
||||
} else {
|
||||
USD1_TOKEN_ACCOUNT
|
||||
};
|
||||
let executor = TradeFactory::create_executor(params.dex_type.clone());
|
||||
let protocol_params = params.extension_params;
|
||||
let buy_params = SwapParams {
|
||||
rpc: Some(self.infrastructure.rpc.clone()),
|
||||
payer: self.payer.clone(),
|
||||
trade_type: TradeType::Buy,
|
||||
input_mint: input_token_mint,
|
||||
output_mint: params.mint,
|
||||
input_token_program: None,
|
||||
output_token_program: None,
|
||||
input_amount: Some(params.input_token_amount),
|
||||
slippage_basis_points: params.slippage_basis_points,
|
||||
address_lookup_table_account: params.address_lookup_table_account,
|
||||
recent_blockhash: params.recent_blockhash,
|
||||
wait_transaction_confirmed: params.wait_transaction_confirmed,
|
||||
protocol_params: protocol_params.clone(),
|
||||
open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置
|
||||
swqos_clients: self.infrastructure.swqos_clients.clone(),
|
||||
middleware_manager: self.middleware_manager.clone(),
|
||||
durable_nonce: params.durable_nonce,
|
||||
with_tip: true,
|
||||
create_input_mint_ata: params.create_input_token_ata,
|
||||
close_input_mint_ata: params.close_input_token_ata,
|
||||
create_output_mint_ata: params.create_mint_ata,
|
||||
close_output_mint_ata: false,
|
||||
fixed_output_amount: params.fixed_output_token_amount,
|
||||
gas_fee_strategy: params.gas_fee_strategy,
|
||||
simulate: params.simulate,
|
||||
log_enabled: self.log_enabled,
|
||||
use_core_affinity: self.use_core_affinity,
|
||||
check_min_tip: self.check_min_tip,
|
||||
grpc_recv_us: params.grpc_recv_us,
|
||||
use_exact_sol_amount: params.use_exact_sol_amount,
|
||||
};
|
||||
|
||||
if !validate_protocol_params(params.dex_type, &protocol_params) {
|
||||
return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
|
||||
}
|
||||
|
||||
let swap_result = executor.swap(buy_params).await;
|
||||
let result =
|
||||
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Execute a sell order for a specified token
|
||||
///
|
||||
/// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
|
||||
/// - bool: 是否至少有一个交易成功
|
||||
/// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
|
||||
/// - Option<TradeError>: 最后一个错误(如果全部失败)
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - Sell trade parameters containing all necessary trading configuration
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok((bool, Vec<Signature>, Option<TradeError>))` with success flag and all transaction signatures,
|
||||
/// or an error if the transaction fails.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - Invalid protocol parameters are provided for the specified DEX type
|
||||
/// - The transaction fails to execute
|
||||
/// - Network or RPC errors occur
|
||||
/// - Insufficient token balance for the sale
|
||||
/// - Token account doesn't exist or is not properly initialized
|
||||
/// - Required accounts cannot be created or accessed
|
||||
#[inline]
|
||||
pub async fn sell(
|
||||
&self,
|
||||
params: TradeSellParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||
#[cfg(feature = "perf-trace")]
|
||||
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||
debug!(
|
||||
target: "sol_trade_sdk",
|
||||
"slippage_basis_points is none, use default slippage basis points: {}",
|
||||
DEFAULT_SLIPPAGE
|
||||
);
|
||||
}
|
||||
if params.output_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk {
|
||||
return Err(anyhow::anyhow!(
|
||||
" Current version only support USD1 trading on Bonk protocols"
|
||||
));
|
||||
}
|
||||
let executor = TradeFactory::create_executor(params.dex_type.clone());
|
||||
let protocol_params = params.extension_params;
|
||||
let output_token_mint = if params.output_token_type == TradeTokenType::SOL {
|
||||
SOL_TOKEN_ACCOUNT
|
||||
} else if params.output_token_type == TradeTokenType::WSOL {
|
||||
WSOL_TOKEN_ACCOUNT
|
||||
} else if params.output_token_type == TradeTokenType::USDC {
|
||||
USDC_TOKEN_ACCOUNT
|
||||
} else {
|
||||
USD1_TOKEN_ACCOUNT
|
||||
};
|
||||
let sell_params = SwapParams {
|
||||
rpc: Some(self.infrastructure.rpc.clone()),
|
||||
payer: self.payer.clone(),
|
||||
trade_type: TradeType::Sell,
|
||||
input_mint: params.mint,
|
||||
output_mint: output_token_mint,
|
||||
input_token_program: None,
|
||||
output_token_program: None,
|
||||
input_amount: Some(params.input_token_amount),
|
||||
slippage_basis_points: params.slippage_basis_points,
|
||||
address_lookup_table_account: params.address_lookup_table_account,
|
||||
recent_blockhash: params.recent_blockhash,
|
||||
wait_transaction_confirmed: params.wait_transaction_confirmed,
|
||||
protocol_params: protocol_params.clone(),
|
||||
with_tip: params.with_tip,
|
||||
open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置
|
||||
swqos_clients: self.infrastructure.swqos_clients.clone(),
|
||||
middleware_manager: self.middleware_manager.clone(),
|
||||
durable_nonce: params.durable_nonce,
|
||||
create_input_mint_ata: false,
|
||||
close_input_mint_ata: params.close_mint_token_ata,
|
||||
create_output_mint_ata: params.create_output_token_ata,
|
||||
close_output_mint_ata: params.close_output_token_ata,
|
||||
fixed_output_amount: params.fixed_output_token_amount,
|
||||
gas_fee_strategy: params.gas_fee_strategy,
|
||||
simulate: params.simulate,
|
||||
log_enabled: self.log_enabled,
|
||||
use_core_affinity: self.use_core_affinity,
|
||||
check_min_tip: self.check_min_tip,
|
||||
grpc_recv_us: params.grpc_recv_us,
|
||||
use_exact_sol_amount: None,
|
||||
};
|
||||
|
||||
if !validate_protocol_params(params.dex_type, &protocol_params) {
|
||||
return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
|
||||
}
|
||||
|
||||
let swap_result = executor.swap(sell_params).await;
|
||||
let result =
|
||||
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Execute a sell order for a percentage of the specified token amount
|
||||
///
|
||||
/// This is a convenience function that calculates the exact amount to sell based on
|
||||
/// a percentage of the total token amount and then calls the `sell` function.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - Sell trade parameters (will be modified with calculated token amount)
|
||||
/// * `amount_token` - Total amount of tokens available (in smallest token units)
|
||||
/// * `percent` - Percentage of tokens to sell (1-100, where 100 = 100%)
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns `Ok(Signature)` with the transaction signature if the sell order is successfully executed,
|
||||
/// or an error if the transaction fails.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - `percent` is 0 or greater than 100
|
||||
/// - Invalid protocol parameters are provided for the specified DEX type
|
||||
/// - The transaction fails to execute
|
||||
/// - Network or RPC errors occur
|
||||
/// - Insufficient token balance for the calculated sale amount
|
||||
/// - Token account doesn't exist or is not properly initialized
|
||||
/// - Required accounts cannot be created or accessed
|
||||
pub async fn sell_by_percent(
|
||||
&self,
|
||||
mut params: TradeSellParams,
|
||||
amount_token: u64,
|
||||
percent: u64,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
let amount = amount_token * percent / 100;
|
||||
params.input_token_amount = amount;
|
||||
self.sell(params).await
|
||||
}
|
||||
|
||||
/// Wraps native SOL into wSOL (Wrapped SOL) for use in SPL token operations
|
||||
///
|
||||
/// This function creates a wSOL associated token account (if it doesn't exist),
|
||||
/// transfers the specified amount of SOL to that account, and then syncs the native
|
||||
/// token balance to make SOL usable as an SPL token in trading operations.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - The amount of SOL to wrap (in lamports)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` - Transaction signature if successful
|
||||
/// * `Err(anyhow::Error)` - If the transaction fails to execute
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - Insufficient SOL balance for the wrap operation
|
||||
/// - wSOL associated token account creation fails
|
||||
/// - Transaction fails to execute or confirm
|
||||
/// - Network or RPC errors occur
|
||||
pub async fn wrap_sol_to_wsol(&self, amount: u64) -> Result<String, anyhow::Error> {
|
||||
use crate::trading::common::wsol_manager::handle_wsol;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let instructions = handle_wsol(&self.payer.pubkey(), amount);
|
||||
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())
|
||||
}
|
||||
/// Closes the wSOL associated token account and unwraps remaining balance to native SOL
|
||||
///
|
||||
/// This function closes the wSOL associated token account, which automatically
|
||||
/// transfers any remaining wSOL balance back to the account owner as native SOL.
|
||||
/// This is useful for cleaning up wSOL accounts and recovering wrapped SOL after trading operations.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` - Transaction signature if successful
|
||||
/// * `Err(anyhow::Error)` - If the transaction fails to execute
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - wSOL associated token account doesn't exist
|
||||
/// - Account closure fails due to insufficient permissions
|
||||
/// - Transaction fails to execute or confirm
|
||||
/// - Network or RPC errors occur
|
||||
pub async fn close_wsol(&self) -> Result<String, anyhow::Error> {
|
||||
use crate::trading::common::wsol_manager::close_wsol;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let instructions = close_wsol(&self.payer.pubkey());
|
||||
let mut transaction =
|
||||
Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
|
||||
transaction.sign(&[&*self.payer], recent_blockhash);
|
||||
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
|
||||
Ok(signature.to_string())
|
||||
}
|
||||
|
||||
/// Creates a wSOL associated token account (ATA) without wrapping any SOL
|
||||
///
|
||||
/// This function only creates the wSOL associated token account for the payer
|
||||
/// without transferring any SOL into it. This is useful when you want to set up
|
||||
/// the account infrastructure in advance without committing funds yet.
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` - Transaction signature if successful
|
||||
/// * `Err(anyhow::Error)` - If the transaction fails to execute
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if:
|
||||
/// - wSOL ATA account already exists (idempotent, will succeed silently)
|
||||
/// - Transaction fails to execute or confirm
|
||||
/// - Network or RPC errors occur
|
||||
/// - Insufficient SOL for transaction fees
|
||||
pub async fn create_wsol_ata(&self) -> Result<String, anyhow::Error> {
|
||||
use crate::trading::common::wsol_manager::create_wsol_ata;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
|
||||
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
let instructions = create_wsol_ata(&self.payer.pubkey());
|
||||
|
||||
// If instructions are empty, ATA already exists
|
||||
if instructions.is_empty() {
|
||||
return Err(anyhow::anyhow!("wSOL ATA already exists or no instructions needed"));
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/// 将 WSOL 转换为 SOL,使用 seed 账户
|
||||
///
|
||||
/// 这个函数实现以下步骤:
|
||||
/// 1. 使用 super::seed::create_associated_token_account_use_seed 创建 WSOL seed 账号
|
||||
/// 2. 使用 get_associated_token_address_with_program_id_use_seed 获取该账号的 ATA 地址
|
||||
/// 3. 添加从用户 WSOL ATA 转账到该 seed ATA 账号的指令
|
||||
/// 4. 添加关闭 WSOL seed 账号的指令
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `amount` - 要转换的 WSOL 数量(以 lamports 为单位)
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(String)` - 交易签名
|
||||
/// * `Err(anyhow::Error)` - 如果交易执行失败
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 此函数在以下情况下会返回错误:
|
||||
/// - 用户 WSOL ATA 中余额不足
|
||||
/// - seed 账户创建失败
|
||||
/// - 转账指令执行失败
|
||||
/// - 交易执行或确认失败
|
||||
/// - 网络或 RPC 错误
|
||||
pub async fn wrap_wsol_to_sol(&self, amount: u64) -> Result<String, anyhow::Error> {
|
||||
use crate::trading::common::wsol_manager::{wrap_wsol_to_sol as wrap_wsol_to_sol_internal, wrap_wsol_to_sol_without_create};
|
||||
use crate::common::seed::get_associated_token_address_with_program_id_use_seed;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
|
||||
// 检查临时seed账户是否已存在
|
||||
let seed_ata_address = get_associated_token_address_with_program_id_use_seed(
|
||||
&self.payer.pubkey(),
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
)?;
|
||||
|
||||
let account_exists = self.infrastructure.rpc.get_account(&seed_ata_address).await.is_ok();
|
||||
|
||||
let instructions = if account_exists {
|
||||
// 如果账户已存在,使用不创建账户的版本
|
||||
wrap_wsol_to_sol_without_create(&self.payer.pubkey(), amount)?
|
||||
} else {
|
||||
// 如果账户不存在,使用创建账户的版本
|
||||
wrap_wsol_to_sol_internal(&self.payer.pubkey(), amount)?
|
||||
};
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
// Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode)
|
||||
pub use crate::swqos::{AstralaneTransport, SwqosTransport};
|
||||
pub use client::{
|
||||
find_pool_by_mint, recommended_sender_thread_core_indices, SolanaTrade, TradeBuyParams,
|
||||
TradeSellParams, TradeTokenType, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! 🚀 编译器级性能优化 - 极致编译时优化
|
||||
//!
|
||||
//!
|
||||
//! 实现编译时的极致性能优化,包括:
|
||||
//! - 编译器标志优化配置
|
||||
//! - 编译时代码生成
|
||||
@@ -137,100 +137,110 @@ impl CompilerOptimizer {
|
||||
stats: CompilerOptimizationStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 生成超高性能编译配置
|
||||
pub fn generate_ultra_performance_config(&self) -> Result<CompilerConfig> {
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Generating ultra-performance compiler configuration...");
|
||||
|
||||
|
||||
let mut rustflags = Vec::new();
|
||||
|
||||
|
||||
// 基础优化标志
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push("opt-level=3".to_string()); // 最高优化级别
|
||||
|
||||
|
||||
// 链接时优化
|
||||
if self.optimization_flags.enable_lto {
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push("lto=fat".to_string()); // 胖LTO获得最佳优化
|
||||
}
|
||||
|
||||
|
||||
// 目标CPU优化
|
||||
if !self.optimization_flags.target_cpu.is_empty() {
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push(format!("target-cpu={}", self.optimization_flags.target_cpu));
|
||||
}
|
||||
|
||||
|
||||
// 目标特性
|
||||
if !self.optimization_flags.target_features.is_empty() {
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push(format!("target-feature={}", self.optimization_flags.target_features.join(",")));
|
||||
rustflags.push(format!(
|
||||
"target-feature={}",
|
||||
self.optimization_flags.target_features.join(",")
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// 代码模型
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push(format!("code-model={:?}", self.optimization_flags.code_model).to_lowercase());
|
||||
|
||||
rustflags
|
||||
.push(format!("code-model={:?}", self.optimization_flags.code_model).to_lowercase());
|
||||
|
||||
// 恐慌处理
|
||||
if self.codegen_config.panic_abort {
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push("panic=abort".to_string());
|
||||
}
|
||||
|
||||
|
||||
// 溢出检查
|
||||
if !self.codegen_config.overflow_checks {
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push("overflow-checks=no".to_string());
|
||||
}
|
||||
|
||||
|
||||
// 代码生成单元
|
||||
if let Some(units) = self.optimization_flags.codegen_units {
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push(format!("codegen-units={}", units));
|
||||
}
|
||||
|
||||
|
||||
// 内联阈值
|
||||
rustflags.push("-C".to_string());
|
||||
rustflags.push(format!("inline-threshold={}", self.inline_strategy.inline_threshold));
|
||||
|
||||
|
||||
// 额外的性能优化标志
|
||||
rustflags.extend([
|
||||
"-C".to_string(), "embed-bitcode=no".to_string(), // 不嵌入位码以减少体积
|
||||
"-C".to_string(), "debuginfo=0".to_string(), // 禁用调试信息
|
||||
"-C".to_string(), "rpath=no".to_string(), // 禁用rpath
|
||||
"-C".to_string(), "force-frame-pointers=no".to_string(), // 禁用帧指针
|
||||
"-C".to_string(),
|
||||
"embed-bitcode=no".to_string(), // 不嵌入位码以减少体积
|
||||
"-C".to_string(),
|
||||
"debuginfo=0".to_string(), // 禁用调试信息
|
||||
"-C".to_string(),
|
||||
"rpath=no".to_string(), // 禁用rpath
|
||||
"-C".to_string(),
|
||||
"force-frame-pointers=no".to_string(), // 禁用帧指针
|
||||
]);
|
||||
|
||||
|
||||
let config = CompilerConfig {
|
||||
rustflags,
|
||||
env_vars: self.generate_env_vars(),
|
||||
cargo_config: self.generate_cargo_config(),
|
||||
};
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","✅ Ultra-performance compiler configuration generated");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
|
||||
/// 生成环境变量配置
|
||||
fn generate_env_vars(&self) -> HashMap<String, String> {
|
||||
let mut env_vars = HashMap::new();
|
||||
|
||||
|
||||
// CPU特定优化
|
||||
env_vars.insert("CARGO_CFG_TARGET_FEATURE".to_string(),
|
||||
self.optimization_flags.target_features.join(","));
|
||||
|
||||
env_vars.insert(
|
||||
"CARGO_CFG_TARGET_FEATURE".to_string(),
|
||||
self.optimization_flags.target_features.join(","),
|
||||
);
|
||||
|
||||
// 启用不稳定特性
|
||||
env_vars.insert("RUSTC_BOOTSTRAP".to_string(), "1".to_string());
|
||||
|
||||
|
||||
// 编译缓存设置
|
||||
if self.optimization_flags.incremental {
|
||||
env_vars.insert("CARGO_INCREMENTAL".to_string(), "1".to_string());
|
||||
} else {
|
||||
env_vars.insert("CARGO_INCREMENTAL".to_string(), "0".to_string());
|
||||
}
|
||||
|
||||
|
||||
env_vars
|
||||
}
|
||||
|
||||
|
||||
/// 生成Cargo配置
|
||||
fn generate_cargo_config(&self) -> CargoConfig {
|
||||
CargoConfig {
|
||||
@@ -244,17 +254,21 @@ impl CompilerOptimizer {
|
||||
debug_assertions: false,
|
||||
rpath: false,
|
||||
strip: true, // 去除符号表
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 获取统计信息
|
||||
pub fn get_stats(&self) -> CompilerOptimizationStats {
|
||||
CompilerOptimizationStats {
|
||||
inlined_functions: AtomicU64::new(self.stats.inlined_functions.load(Ordering::Relaxed)),
|
||||
constant_folding: AtomicU64::new(self.stats.constant_folding.load(Ordering::Relaxed)),
|
||||
dead_code_elimination: AtomicU64::new(self.stats.dead_code_elimination.load(Ordering::Relaxed)),
|
||||
loop_optimizations: AtomicU64::new(self.stats.loop_optimizations.load(Ordering::Relaxed)),
|
||||
dead_code_elimination: AtomicU64::new(
|
||||
self.stats.dead_code_elimination.load(Ordering::Relaxed),
|
||||
),
|
||||
loop_optimizations: AtomicU64::new(
|
||||
self.stats.loop_optimizations.load(Ordering::Relaxed),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,12 +293,12 @@ impl OptimizationFlags {
|
||||
Self {
|
||||
opt_level: OptLevel::Aggressive,
|
||||
enable_lto: true,
|
||||
enable_pgo: false, // PGO需要多阶段构建
|
||||
enable_pgo: false, // PGO需要多阶段构建
|
||||
target_cpu: "native".to_string(), // 使用本机CPU特性
|
||||
target_features,
|
||||
code_model: CodeModel::Small,
|
||||
debug_info: false,
|
||||
incremental: false, // 发布版本禁用增量编译
|
||||
incremental: false, // 发布版本禁用增量编译
|
||||
codegen_units: Some(1), // 单个代码生成单元获得最佳优化
|
||||
}
|
||||
}
|
||||
@@ -294,7 +308,7 @@ impl CodegenConfig {
|
||||
/// 超高性能配置
|
||||
pub fn ultra_performance() -> Self {
|
||||
Self {
|
||||
panic_abort: true, // 恐慌即中止,避免展开开销
|
||||
panic_abort: true, // 恐慌即中止,避免展开开销
|
||||
overflow_checks: false, // 生产环境禁用溢出检查
|
||||
fat_lto: true,
|
||||
enable_simd: true,
|
||||
@@ -353,14 +367,14 @@ macro_rules! compile_time_optimize {
|
||||
(const $expr:expr) => {
|
||||
const { $expr }
|
||||
};
|
||||
|
||||
|
||||
// 强制内联热路径
|
||||
(inline_hot $fn_name:ident) => {
|
||||
#[inline(always)]
|
||||
#[hot]
|
||||
$fn_name
|
||||
};
|
||||
|
||||
|
||||
// 标记冷路径
|
||||
(cold $fn_name:ident) => {
|
||||
#[inline(never)]
|
||||
@@ -372,10 +386,10 @@ macro_rules! compile_time_optimize {
|
||||
/// 🚀 零成本抽象特征
|
||||
pub trait ZeroCostAbstraction {
|
||||
type Output;
|
||||
|
||||
|
||||
/// 编译时计算
|
||||
fn compute_at_compile_time(&self) -> Self::Output;
|
||||
|
||||
|
||||
/// 内联操作
|
||||
#[inline(always)]
|
||||
fn inline_operation(&self) -> Self::Output {
|
||||
@@ -399,35 +413,35 @@ impl CompileTimeOptimizedEventProcessor {
|
||||
route_table: Self::precompute_route_table(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 编译时预计算哈希表
|
||||
const fn precompute_hash_table() -> [u64; 256] {
|
||||
let mut table = [0u64; 256];
|
||||
let mut i = 0;
|
||||
|
||||
|
||||
while i < 256 {
|
||||
// 使用编译时常量计算哈希值
|
||||
table[i] = Self::const_hash(i as u8);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
|
||||
/// 编译时预计算路由表
|
||||
const fn precompute_route_table() -> [u32; 1024] {
|
||||
let mut table = [0u32; 1024];
|
||||
let mut i = 0;
|
||||
|
||||
|
||||
while i < 1024 {
|
||||
// 预计算路由信息
|
||||
table[i] = (i as u32) % 16; // 16个工作线程
|
||||
i += 1;
|
||||
}
|
||||
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
|
||||
/// 编译时常量哈希函数
|
||||
const fn const_hash(input: u8) -> u64 {
|
||||
// 使用简单的编译时常量哈希
|
||||
@@ -437,16 +451,14 @@ impl CompileTimeOptimizedEventProcessor {
|
||||
hash ^= hash << 17;
|
||||
hash
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 零开销事件路由
|
||||
#[inline(always)]
|
||||
pub fn route_event_zero_cost(&self, event_id: u8) -> u32 {
|
||||
// 编译时优化:直接数组访问,无边界检查
|
||||
unsafe {
|
||||
*self.route_table.get_unchecked((event_id as usize) & 1023)
|
||||
}
|
||||
unsafe { *self.route_table.get_unchecked((event_id as usize) & 1023) }
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 编译时优化的哈希查找
|
||||
#[inline(always)]
|
||||
pub fn hash_lookup_optimized(&self, key: u8) -> u64 {
|
||||
@@ -464,28 +476,28 @@ impl SIMDCompileTimeOptimizer {
|
||||
#[target_feature(enable = "avx2")]
|
||||
pub unsafe fn vectorized_sum_compile_time(data: &[u64]) -> u64 {
|
||||
use std::arch::x86_64::*;
|
||||
|
||||
|
||||
if data.len() < 4 {
|
||||
return data.iter().sum();
|
||||
}
|
||||
|
||||
|
||||
let chunks = data.len() / 4;
|
||||
let mut sum_vec = _mm256_setzero_si256();
|
||||
|
||||
|
||||
for i in 0..chunks {
|
||||
let ptr = data.as_ptr().add(i * 4) as *const __m256i;
|
||||
let vec = _mm256_loadu_si256(ptr);
|
||||
sum_vec = _mm256_add_epi64(sum_vec, vec);
|
||||
}
|
||||
|
||||
|
||||
// 水平求和
|
||||
let mut result = [0u64; 4];
|
||||
_mm256_storeu_si256(result.as_mut_ptr() as *mut __m256i, sum_vec);
|
||||
let partial_sum: u64 = result.iter().sum();
|
||||
|
||||
|
||||
// 处理剩余元素
|
||||
let remaining: u64 = data[chunks * 4..].iter().sum();
|
||||
|
||||
|
||||
partial_sum + remaining
|
||||
}
|
||||
|
||||
@@ -523,7 +535,8 @@ fn main() {
|
||||
println!("cargo:rustc-link-arg=-fprofile-use");
|
||||
}
|
||||
}
|
||||
"#.to_string()
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// 🚀 生成.cargo/config.toml
|
||||
@@ -576,56 +589,58 @@ rustflags = [
|
||||
rustflags = [
|
||||
"-C", "target-feature=+sse4.2,+avx,+avx2,+fma,+bmi1,+bmi2,+lzcnt,+popcnt",
|
||||
]
|
||||
"#.to_string()
|
||||
"#
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_compiler_optimizer_creation() {
|
||||
let optimizer = CompilerOptimizer::new();
|
||||
assert!(optimizer.optimization_flags.enable_lto);
|
||||
assert_eq!(optimizer.optimization_flags.opt_level as u8, OptLevel::Aggressive as u8);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_compile_time_processor() {
|
||||
const PROCESSOR: CompileTimeOptimizedEventProcessor = CompileTimeOptimizedEventProcessor::new();
|
||||
|
||||
const PROCESSOR: CompileTimeOptimizedEventProcessor =
|
||||
CompileTimeOptimizedEventProcessor::new();
|
||||
|
||||
let route = PROCESSOR.route_event_zero_cost(42);
|
||||
assert!(route < 16); // 应该路由到16个工作线程之一
|
||||
|
||||
|
||||
let hash = PROCESSOR.hash_lookup_optimized(100);
|
||||
assert!(hash > 0); // 哈希值应该非零
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_ultra_performance_config() {
|
||||
let flags = OptimizationFlags::ultra_performance();
|
||||
assert!(flags.enable_lto);
|
||||
assert_eq!(flags.target_cpu, "native");
|
||||
assert!(!flags.target_features.is_empty());
|
||||
|
||||
|
||||
let codegen = CodegenConfig::ultra_performance();
|
||||
assert!(codegen.panic_abort);
|
||||
assert!(!codegen.overflow_checks);
|
||||
assert!(codegen.enable_simd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
#[test]
|
||||
fn test_compiler_config_generation() {
|
||||
let optimizer = CompilerOptimizer::new();
|
||||
let config = optimizer.generate_ultra_performance_config().unwrap();
|
||||
|
||||
|
||||
assert!(!config.rustflags.is_empty());
|
||||
assert!(config.rustflags.contains(&"-C".to_string()));
|
||||
assert!(config.rustflags.contains(&"opt-level=3".to_string()));
|
||||
|
||||
|
||||
assert!(config.env_vars.contains_key("CARGO_INCREMENTAL"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_simd_compile_time_optimization() {
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
@@ -642,7 +657,7 @@ mod tests {
|
||||
assert_eq!(sum, 36); // 1+2+3+4+5+6+7+8 = 36
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_build_script_generation() {
|
||||
let build_script = generate_build_script();
|
||||
@@ -650,7 +665,7 @@ mod tests {
|
||||
assert!(build_script.contains("TARGET_FEATURE"));
|
||||
assert!(build_script.contains("lld"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_cargo_config_generation() {
|
||||
let config = generate_cargo_config_toml();
|
||||
@@ -659,4 +674,4 @@ mod tests {
|
||||
assert!(config.contains("target-cpu=native"));
|
||||
assert!(config.contains("panic = \"abort\""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Hardware-oriented optimizations: cache-line alignment, prefetch, SIMD, branch hints, memory barriers.
|
||||
//! 硬件级优化:缓存行对齐与预取、SIMD、分支提示、内存屏障。
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use anyhow::Result;
|
||||
use crossbeam_utils::CachePadded;
|
||||
use std::mem::size_of;
|
||||
use std::ptr;
|
||||
use crossbeam_utils::CachePadded;
|
||||
use anyhow::Result;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
/// Typical CPU cache line size in bytes. 典型 CPU 缓存行大小(字节)。
|
||||
pub const CACHE_LINE_SIZE: usize = 64;
|
||||
@@ -32,7 +32,7 @@ impl SIMDMemoryOps {
|
||||
_ => Self::memcpy_avx512_or_fallback(dst, src, len),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Copy 1–8 bytes (scalar / small word). 小数据拷贝(1–8 字节)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcpy_small(dst: *mut u8, src: *const u8, len: usize) {
|
||||
@@ -53,52 +53,52 @@ impl SIMDMemoryOps {
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Copy 9–16 bytes using SSE (128-bit). SSE 拷贝(9–16 字节)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcpy_sse(dst: *mut u8, src: *const u8, len: usize) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::{__m128i, _mm_loadu_si128, _mm_storeu_si128};
|
||||
|
||||
|
||||
if len <= 16 {
|
||||
let chunk = _mm_loadu_si128(src as *const __m128i);
|
||||
_mm_storeu_si128(dst as *mut __m128i, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
ptr::copy_nonoverlapping(src, dst, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Copy 17–32 bytes using AVX (256-bit). AVX 拷贝(17–32 字节)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcpy_avx(dst: *mut u8, src: *const u8, len: usize) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_storeu_si256};
|
||||
|
||||
|
||||
if len <= 32 {
|
||||
let chunk = _mm256_loadu_si256(src as *const __m256i);
|
||||
_mm256_storeu_si256(dst as *mut __m256i, chunk);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
ptr::copy_nonoverlapping(src, dst, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Copy 33–64 bytes using AVX2 (256-bit, two chunks). AVX2 拷贝(33–64 字节,两段)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcpy_avx2(dst: *mut u8, src: *const u8, len: usize) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_storeu_si256};
|
||||
|
||||
|
||||
let chunk1 = _mm256_loadu_si256(src as *const __m256i);
|
||||
_mm256_storeu_si256(dst as *mut __m256i, chunk1);
|
||||
if len > 32 {
|
||||
@@ -109,52 +109,52 @@ impl SIMDMemoryOps {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
ptr::copy_nonoverlapping(src, dst, len);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Copy >64 bytes: AVX-512 64-byte chunks when available, else AVX2 32-byte chunks. >64 字节:有 AVX512 用 64 字节块,否则 AVX2 32 字节块。
|
||||
#[inline(always)]
|
||||
unsafe fn memcpy_avx512_or_fallback(dst: *mut u8, src: *const u8, len: usize) {
|
||||
#[cfg(all(target_arch = "x86_64", target_feature = "avx512f"))]
|
||||
{
|
||||
use std::arch::x86_64::{__m512i, _mm512_loadu_si512, _mm512_storeu_si512};
|
||||
|
||||
|
||||
let chunks = len / 64;
|
||||
let mut offset = 0;
|
||||
|
||||
|
||||
for _ in 0..chunks {
|
||||
let chunk = _mm512_loadu_si512(src.add(offset) as *const __m512i);
|
||||
_mm512_storeu_si512(dst.add(offset) as *mut __m512i, chunk);
|
||||
offset += 64;
|
||||
}
|
||||
|
||||
|
||||
let remaining = len % 64;
|
||||
if remaining > 0 {
|
||||
Self::memcpy_avx2(dst.add(offset), src.add(offset), remaining);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(all(target_arch = "x86_64", target_feature = "avx512f")))]
|
||||
{
|
||||
let chunks = len / 32;
|
||||
let mut offset = 0;
|
||||
|
||||
|
||||
for _ in 0..chunks {
|
||||
Self::memcpy_avx2(dst.add(offset), src.add(offset), 32);
|
||||
offset += 32;
|
||||
}
|
||||
|
||||
|
||||
let remaining = len % 32;
|
||||
if remaining > 0 {
|
||||
Self::memcpy_avx(dst.add(offset), src.add(offset), remaining);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// SIMD-optimized byte equality; dispatches by length (small / SSE / AVX2 / large). SIMD 加速的内存比较,按长度分派。
|
||||
#[inline(always)]
|
||||
pub unsafe fn memcmp_simd_optimized(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
@@ -166,109 +166,108 @@ impl SIMDMemoryOps {
|
||||
_ => Self::memcmp_large(a, b, len),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Compare 1–8 bytes (scalar). 小数据比较(1–8 字节)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcmp_small(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
match len {
|
||||
1 => *a == *b,
|
||||
2 => *(a as *const u16) == *(b as *const u16),
|
||||
3 => {
|
||||
*(a as *const u16) == *(b as *const u16) &&
|
||||
*a.add(2) == *b.add(2)
|
||||
}
|
||||
3 => *(a as *const u16) == *(b as *const u16) && *a.add(2) == *b.add(2),
|
||||
4 => *(a as *const u32) == *(b as *const u32),
|
||||
5..=8 => *(a as *const u64) == *(b as *const u64),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Compare 9–16 bytes using SSE. SSE 比较(9–16 字节)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcmp_sse(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::{__m128i, _mm_loadu_si128, _mm_cmpeq_epi8, _mm_movemask_epi8};
|
||||
|
||||
use std::arch::x86_64::{__m128i, _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8};
|
||||
|
||||
let chunk_a = _mm_loadu_si128(a as *const __m128i);
|
||||
let chunk_b = _mm_loadu_si128(b as *const __m128i);
|
||||
let cmp_result = _mm_cmpeq_epi8(chunk_a, chunk_b);
|
||||
let mask = _mm_movemask_epi8(cmp_result) as u32;
|
||||
|
||||
|
||||
let valid_mask = if len >= 16 { 0xFFFF } else { (1u32 << len) - 1 };
|
||||
(mask & valid_mask) == valid_mask
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
(0..len).all(|i| *a.add(i) == *b.add(i))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Compare 17–32 bytes using AVX2. AVX2 比较(17–32 字节)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcmp_avx2(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::{__m256i, _mm256_loadu_si256, _mm256_cmpeq_epi8, _mm256_movemask_epi8};
|
||||
|
||||
use std::arch::x86_64::{
|
||||
__m256i, _mm256_cmpeq_epi8, _mm256_loadu_si256, _mm256_movemask_epi8,
|
||||
};
|
||||
|
||||
let chunk_a = _mm256_loadu_si256(a as *const __m256i);
|
||||
let chunk_b = _mm256_loadu_si256(b as *const __m256i);
|
||||
let cmp_result = _mm256_cmpeq_epi8(chunk_a, chunk_b);
|
||||
let mask = _mm256_movemask_epi8(cmp_result) as u32;
|
||||
|
||||
|
||||
let valid_mask = if len >= 32 { 0xFFFFFFFF } else { (1u32 << len) - 1 };
|
||||
(mask & valid_mask) == valid_mask
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
(0..len).all(|i| *a.add(i) == *b.add(i))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Compare >32 bytes in 32-byte AVX2 chunks. 大数据比较(32 字节 AVX2 分块)。
|
||||
#[inline(always)]
|
||||
unsafe fn memcmp_large(a: *const u8, b: *const u8, len: usize) -> bool {
|
||||
let chunks = len / 32;
|
||||
|
||||
|
||||
for i in 0..chunks {
|
||||
let offset = i * 32;
|
||||
if !Self::memcmp_avx2(a.add(offset), b.add(offset), 32) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let remaining = len % 32;
|
||||
if remaining > 0 {
|
||||
return Self::memcmp_avx2(a.add(chunks * 32), b.add(chunks * 32), remaining);
|
||||
}
|
||||
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
/// SIMD-optimized zero memory. SIMD 加速的内存清零。
|
||||
#[inline(always)]
|
||||
pub unsafe fn memzero_simd_optimized(ptr: *mut u8, len: usize) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::{__m256i, _mm256_setzero_si256, _mm256_storeu_si256};
|
||||
|
||||
|
||||
let zero = _mm256_setzero_si256();
|
||||
let chunks = len / 32;
|
||||
let mut offset = 0;
|
||||
|
||||
|
||||
for _ in 0..chunks {
|
||||
_mm256_storeu_si256(ptr.add(offset) as *mut __m256i, zero);
|
||||
offset += 32;
|
||||
}
|
||||
|
||||
|
||||
let remaining = len % 32;
|
||||
for i in 0..remaining {
|
||||
*ptr.add(offset + i) = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "x86_64"))]
|
||||
{
|
||||
ptr::write_bytes(ptr, 0, len);
|
||||
@@ -291,17 +290,17 @@ impl CacheAlignedCounter {
|
||||
_padding: [0; CACHE_LINE_SIZE - size_of::<AtomicU64>()],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[inline(always)]
|
||||
pub fn increment(&self) -> u64 {
|
||||
self.value.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
|
||||
#[inline(always)]
|
||||
pub fn load(&self) -> u64 {
|
||||
self.value.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
|
||||
#[inline(always)]
|
||||
pub fn store(&self, val: u64) {
|
||||
self.value.store(val, Ordering::Relaxed)
|
||||
@@ -312,7 +311,7 @@ impl CacheLineAligned for CacheAlignedCounter {
|
||||
fn ensure_cache_aligned(&self) -> bool {
|
||||
(self as *const Self as usize) % CACHE_LINE_SIZE == 0
|
||||
}
|
||||
|
||||
|
||||
fn prefetch_data(&self) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
@@ -339,10 +338,10 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
||||
if !capacity.is_power_of_two() {
|
||||
return Err(anyhow::anyhow!("Capacity must be a power of 2"));
|
||||
}
|
||||
|
||||
|
||||
let mut buffer = Vec::with_capacity(capacity);
|
||||
buffer.resize_with(capacity, Default::default);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
buffer,
|
||||
producer_head: CachePadded::new(AtomicU64::new(0)),
|
||||
@@ -351,7 +350,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
||||
mask: capacity - 1,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// Lock-free push; returns false if full. 无锁写入,满则返回 false。
|
||||
#[inline(always)]
|
||||
pub fn try_push(&self, item: T) -> bool {
|
||||
@@ -368,7 +367,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
||||
self.producer_head.store(current_head + 1, Ordering::Release);
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
/// Lock-free pop; returns None if empty. 无锁读取,空则返回 None。
|
||||
#[inline(always)]
|
||||
pub fn try_pop(&self) -> Option<T> {
|
||||
@@ -385,7 +384,7 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
||||
self.consumer_tail.store(current_tail + 1, Ordering::Release);
|
||||
Some(item)
|
||||
}
|
||||
|
||||
|
||||
/// Current number of elements. 当前元素个数。
|
||||
#[inline(always)]
|
||||
pub fn len(&self) -> usize {
|
||||
@@ -393,12 +392,11 @@ impl<T: Copy + Default> CacheOptimizedRingBuffer<T> {
|
||||
let tail = self.consumer_tail.load(Ordering::Relaxed);
|
||||
((head + self.capacity as u64 - tail) & self.mask as u64) as usize
|
||||
}
|
||||
|
||||
|
||||
/// True if no elements. 是否为空。
|
||||
#[inline(always)]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.producer_head.load(Ordering::Relaxed) ==
|
||||
self.consumer_tail.load(Ordering::Relaxed)
|
||||
self.producer_head.load(Ordering::Relaxed) == self.consumer_tail.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,7 +404,7 @@ impl<T> CacheLineAligned for CacheOptimizedRingBuffer<T> {
|
||||
fn ensure_cache_aligned(&self) -> bool {
|
||||
(self as *const Self as usize) % CACHE_LINE_SIZE == 0
|
||||
}
|
||||
|
||||
|
||||
fn prefetch_data(&self) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
unsafe {
|
||||
@@ -428,44 +426,44 @@ impl BranchOptimizer {
|
||||
pub fn likely(condition: bool) -> bool {
|
||||
#[cold]
|
||||
fn cold() {}
|
||||
|
||||
|
||||
if !condition {
|
||||
cold();
|
||||
}
|
||||
condition
|
||||
}
|
||||
|
||||
|
||||
/// Hint: condition is usually false. 提示编译器条件大概率为假。
|
||||
#[inline(always)]
|
||||
pub fn unlikely(condition: bool) -> bool {
|
||||
#[cold]
|
||||
fn cold() {}
|
||||
|
||||
|
||||
if condition {
|
||||
cold();
|
||||
}
|
||||
condition
|
||||
}
|
||||
|
||||
|
||||
/// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_read_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T0;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T0);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Prefetch for write (T1 hint). 写预取(T1 提示)。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_write_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T1;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T1);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -479,25 +477,25 @@ impl MemoryBarriers {
|
||||
pub fn compiler_barrier() {
|
||||
std::sync::atomic::compiler_fence(Ordering::SeqCst);
|
||||
}
|
||||
|
||||
|
||||
/// Light barrier (Acquire). 轻量级屏障(Acquire)。
|
||||
#[inline(always)]
|
||||
pub fn memory_barrier_light() {
|
||||
std::sync::atomic::fence(Ordering::Acquire);
|
||||
}
|
||||
|
||||
|
||||
/// Full sequential consistency barrier. 全序一致性屏障。
|
||||
#[inline(always)]
|
||||
pub fn memory_barrier_heavy() {
|
||||
std::sync::atomic::fence(Ordering::SeqCst);
|
||||
}
|
||||
|
||||
|
||||
/// Store/release barrier. 存储屏障,保证写入可见性。
|
||||
#[inline(always)]
|
||||
pub fn store_barrier() {
|
||||
std::sync::atomic::fence(Ordering::Release);
|
||||
}
|
||||
|
||||
|
||||
/// Load/acquire barrier. 加载屏障,保证读取顺序。
|
||||
#[inline(always)]
|
||||
pub fn load_barrier() {
|
||||
@@ -508,63 +506,54 @@ impl MemoryBarriers {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_cache_aligned_counter() {
|
||||
let counter = CacheAlignedCounter::new(0);
|
||||
assert!(counter.ensure_cache_aligned());
|
||||
|
||||
|
||||
assert_eq!(counter.load(), 0);
|
||||
counter.increment();
|
||||
assert_eq!(counter.load(), 1);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_simd_memcpy() {
|
||||
let src = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
let mut dst = [0u8; 10];
|
||||
|
||||
|
||||
unsafe {
|
||||
SIMDMemoryOps::memcpy_simd_optimized(
|
||||
dst.as_mut_ptr(),
|
||||
src.as_ptr(),
|
||||
src.len()
|
||||
);
|
||||
SIMDMemoryOps::memcpy_simd_optimized(dst.as_mut_ptr(), src.as_ptr(), src.len());
|
||||
}
|
||||
|
||||
|
||||
assert_eq!(src, dst);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_cache_optimized_ring_buffer() {
|
||||
let buffer: CacheOptimizedRingBuffer<u64> =
|
||||
CacheOptimizedRingBuffer::new(16).unwrap();
|
||||
|
||||
let buffer: CacheOptimizedRingBuffer<u64> = CacheOptimizedRingBuffer::new(16).unwrap();
|
||||
|
||||
assert!(buffer.is_empty());
|
||||
|
||||
|
||||
// 测试推入
|
||||
assert!(buffer.try_push(42));
|
||||
assert_eq!(buffer.len(), 1);
|
||||
|
||||
|
||||
// 测试弹出
|
||||
assert_eq!(buffer.try_pop(), Some(42));
|
||||
assert!(buffer.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_simd_memcmp() {
|
||||
let a = [1u8, 2, 3, 4, 5];
|
||||
let b = [1u8, 2, 3, 4, 5];
|
||||
let c = [1u8, 2, 3, 4, 6];
|
||||
|
||||
|
||||
unsafe {
|
||||
assert!(SIMDMemoryOps::memcmp_simd_optimized(
|
||||
a.as_ptr(), b.as_ptr(), a.len()
|
||||
));
|
||||
|
||||
assert!(!SIMDMemoryOps::memcmp_simd_optimized(
|
||||
a.as_ptr(), c.as_ptr(), a.len()
|
||||
));
|
||||
assert!(SIMDMemoryOps::memcmp_simd_optimized(a.as_ptr(), b.as_ptr(), a.len()));
|
||||
|
||||
assert!(!SIMDMemoryOps::memcmp_simd_optimized(a.as_ptr(), c.as_ptr(), a.len()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,14 +1,14 @@
|
||||
//! Performance: SIMD, cache prefetch, branch hints, zero-copy I/O, syscall bypass, compiler hints.
|
||||
//! 性能优化:SIMD、缓存预取、分支提示、零拷贝 I/O、系统调用绕过、编译器提示。
|
||||
|
||||
pub mod simd;
|
||||
pub mod hardware_optimizations;
|
||||
pub mod zero_copy_io;
|
||||
pub mod syscall_bypass;
|
||||
pub mod compiler_optimization;
|
||||
pub mod hardware_optimizations;
|
||||
pub mod simd;
|
||||
pub mod syscall_bypass;
|
||||
pub mod zero_copy_io;
|
||||
|
||||
pub use simd::*;
|
||||
pub use hardware_optimizations::*;
|
||||
pub use zero_copy_io::*;
|
||||
pub use syscall_bypass::*;
|
||||
pub use compiler_optimization::*;
|
||||
pub use hardware_optimizations::*;
|
||||
pub use simd::*;
|
||||
pub use syscall_bypass::*;
|
||||
pub use zero_copy_io::*;
|
||||
|
||||
+1
-1
@@ -235,7 +235,7 @@ impl SIMDHash {
|
||||
/// 批量计算 SHA256 哈希
|
||||
#[inline(always)]
|
||||
pub fn hash_batch_sha256(data: &[&[u8]]) -> Vec<[u8; 32]> {
|
||||
use sha2::{Sha256, Digest};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
data.iter()
|
||||
.map(|item| {
|
||||
|
||||
+154
-154
@@ -1,11 +1,11 @@
|
||||
//! Syscall bypass: batching, vDSO fast time, io_uring, mmap, userspace impl.
|
||||
//! 系统调用绕过:批处理、vDSO 快速时间、io_uring、mmap、用户态实现。
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{SystemTime, UNIX_EPOCH, Duration, Instant};
|
||||
#[allow(unused_imports)]
|
||||
use std::fs::OpenOptions;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossbeam_utils::CachePadded;
|
||||
@@ -55,14 +55,30 @@ pub struct SyscallBatchProcessor {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SyscallRequest {
|
||||
Write { fd: i32, data: Vec<u8> },
|
||||
Read { fd: i32, size: usize },
|
||||
Send { socket: i32, data: Vec<u8> },
|
||||
Recv { socket: i32, size: usize },
|
||||
Write {
|
||||
fd: i32,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
Read {
|
||||
fd: i32,
|
||||
size: usize,
|
||||
},
|
||||
Send {
|
||||
socket: i32,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
Recv {
|
||||
socket: i32,
|
||||
size: usize,
|
||||
},
|
||||
GetTime,
|
||||
MemAlloc { size: usize },
|
||||
MemAlloc {
|
||||
size: usize,
|
||||
},
|
||||
/// 内存释放
|
||||
MemFree { ptr: usize },
|
||||
MemFree {
|
||||
ptr: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// 🚀 快速时间提供器 - 绕过系统调用获取时间
|
||||
@@ -86,24 +102,22 @@ impl FastTimeProvider {
|
||||
pub fn new(enable_vdso: bool) -> Result<Self> {
|
||||
let now = SystemTime::now();
|
||||
let instant_now = Instant::now();
|
||||
|
||||
|
||||
let provider = Self {
|
||||
_base_time: now,
|
||||
monotonic_start: instant_now,
|
||||
time_cache: CachePadded::new(AtomicU64::new(
|
||||
now.duration_since(UNIX_EPOCH)?.as_nanos() as u64
|
||||
now.duration_since(UNIX_EPOCH)?.as_nanos() as u64,
|
||||
)),
|
||||
cache_update_interval_ns: 1_000_000, // 1ms
|
||||
last_update: CachePadded::new(AtomicU64::new(
|
||||
instant_now.elapsed().as_nanos() as u64
|
||||
)),
|
||||
last_update: CachePadded::new(AtomicU64::new(instant_now.elapsed().as_nanos() as u64)),
|
||||
vdso_enabled: enable_vdso,
|
||||
};
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Fast time provider initialized with vDSO: {}", enable_vdso);
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 超快速获取当前时间 - 绕过系统调用
|
||||
#[inline(always)]
|
||||
pub fn fast_now_nanos(&self) -> u64 {
|
||||
@@ -111,19 +125,19 @@ impl FastTimeProvider {
|
||||
// 使用vDSO快速获取时间
|
||||
return self.vdso_time_nanos();
|
||||
}
|
||||
|
||||
|
||||
// 使用缓存的时间
|
||||
let now_mono = self.monotonic_start.elapsed().as_nanos() as u64;
|
||||
let last_update = self.last_update.load(Ordering::Relaxed);
|
||||
|
||||
|
||||
if now_mono.saturating_sub(last_update) > self.cache_update_interval_ns {
|
||||
// 需要更新缓存
|
||||
self.update_time_cache();
|
||||
}
|
||||
|
||||
|
||||
self.time_cache.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
|
||||
/// vDSO时间获取
|
||||
#[inline(always)]
|
||||
fn vdso_time_nanos(&self) -> u64 {
|
||||
@@ -132,36 +146,34 @@ impl FastTimeProvider {
|
||||
// 在Linux上使用vDSO获取时间,避免系统调用
|
||||
unsafe {
|
||||
let mut ts = libc::timespec { tv_sec: 0, tv_nsec: 0 };
|
||||
|
||||
|
||||
// CLOCK_MONOTONIC_RAW不受NTP调整影响,更适合性能测量
|
||||
if libc::clock_gettime(libc::CLOCK_MONOTONIC_RAW, &mut ts) == 0 {
|
||||
return (ts.tv_sec as u64) * 1_000_000_000 + (ts.tv_nsec as u64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 回退到缓存时间
|
||||
self.time_cache.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
|
||||
/// 更新时间缓存
|
||||
fn update_time_cache(&self) {
|
||||
if let Ok(now) = SystemTime::now().duration_since(UNIX_EPOCH) {
|
||||
let nanos = now.as_nanos() as u64;
|
||||
self.time_cache.store(nanos, Ordering::Relaxed);
|
||||
self.last_update.store(
|
||||
self.monotonic_start.elapsed().as_nanos() as u64,
|
||||
Ordering::Relaxed
|
||||
);
|
||||
self.last_update
|
||||
.store(self.monotonic_start.elapsed().as_nanos() as u64, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 快速获取微秒时间戳
|
||||
#[inline(always)]
|
||||
pub fn fast_now_micros(&self) -> u64 {
|
||||
self.fast_now_nanos() / 1000
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 快速获取毫秒时间戳
|
||||
#[inline(always)]
|
||||
pub fn fast_now_millis(&self) -> u64 {
|
||||
@@ -200,16 +212,16 @@ impl IOOptimizer {
|
||||
/// 创建I/O优化器
|
||||
pub fn new(_config: &SyscallBypassConfig) -> Result<Self> {
|
||||
let io_uring_available = Self::check_io_uring_support();
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 I/O Optimizer initialized - io_uring: {}", io_uring_available);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
io_uring_available,
|
||||
async_io_stats: Arc::new(AsyncIOStats::default()),
|
||||
mmap_regions: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// 检查io_uring支持
|
||||
fn check_io_uring_support() -> bool {
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -218,7 +230,7 @@ impl IOOptimizer {
|
||||
if let Ok(uname) = std::process::Command::new("uname").arg("-r").output() {
|
||||
let kernel_version = String::from_utf8_lossy(&uname.stdout);
|
||||
tracing::info!(target: "sol_trade_sdk","Kernel version: {}", kernel_version.trim());
|
||||
|
||||
|
||||
// 简单检查:内核版本 >= 5.1 支持io_uring
|
||||
if let Some(version_str) = kernel_version.split('.').next() {
|
||||
if let Ok(major_version) = version_str.parse::<u32>() {
|
||||
@@ -227,60 +239,62 @@ impl IOOptimizer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 批量异步写入 - 绕过多次系统调用
|
||||
#[inline(always)]
|
||||
pub async fn batch_async_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
|
||||
if self.io_uring_available && requests.len() > 1 {
|
||||
return self.io_uring_batch_write(requests).await;
|
||||
}
|
||||
|
||||
|
||||
// 回退到标准批量写入
|
||||
self.standard_batch_write(requests).await
|
||||
}
|
||||
|
||||
|
||||
/// 使用io_uring进行批量写入
|
||||
async fn io_uring_batch_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
|
||||
// 这里是伪代码 - 实际实现需要io_uring库
|
||||
tracing::trace!(target: "sol_trade_sdk","Using io_uring for {} write operations", requests.len());
|
||||
|
||||
|
||||
let mut results = Vec::with_capacity(requests.len());
|
||||
|
||||
|
||||
// 模拟批量提交到io_uring
|
||||
for (_fd, data) in requests {
|
||||
self.async_io_stats.operations_queued.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
|
||||
// 实际的io_uring实现会在这里提交所有操作
|
||||
// 然后等待完成,避免多次系统调用
|
||||
|
||||
|
||||
results.push(data.len()); // 模拟写入成功
|
||||
self.async_io_stats.bytes_transferred.fetch_add(data.len() as u64, Ordering::Relaxed);
|
||||
self.async_io_stats.operations_completed.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
|
||||
// 这是一个系统调用而不是N个
|
||||
self.async_io_stats.syscalls_avoided.fetch_add(requests.len() as u64 - 1, Ordering::Relaxed);
|
||||
|
||||
self.async_io_stats
|
||||
.syscalls_avoided
|
||||
.fetch_add(requests.len() as u64 - 1, Ordering::Relaxed);
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
|
||||
/// 标准批量写入
|
||||
async fn standard_batch_write(&self, requests: &[(i32, &[u8])]) -> Result<Vec<usize>> {
|
||||
let mut results = Vec::with_capacity(requests.len());
|
||||
|
||||
|
||||
// 将所有写入打包成一个写操作
|
||||
for (_fd, data) in requests {
|
||||
// 模拟写入操作
|
||||
results.push(data.len());
|
||||
self.async_io_stats.bytes_transferred.fetch_add(data.len() as u64, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 内存映射文件I/O - 避免read/write系统调用
|
||||
pub fn create_memory_mapped_io(&mut self, file_path: &str, size: usize) -> Result<usize> {
|
||||
#[cfg(unix)]
|
||||
@@ -298,16 +312,12 @@ impl IOOptimizer {
|
||||
.custom_flags(libc::O_DIRECT) // 直接I/O,绕过页面缓存
|
||||
.open(file_path)?
|
||||
};
|
||||
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(file_path)?;
|
||||
|
||||
let file = OpenOptions::new().read(true).write(true).create(true).open(file_path)?;
|
||||
|
||||
let fd = file.as_raw_fd();
|
||||
|
||||
|
||||
unsafe {
|
||||
let addr = libc::mmap(
|
||||
std::ptr::null_mut(),
|
||||
@@ -317,37 +327,42 @@ impl IOOptimizer {
|
||||
fd,
|
||||
0,
|
||||
);
|
||||
|
||||
|
||||
if addr == libc::MAP_FAILED {
|
||||
return Err(anyhow::anyhow!("Memory mapping failed"));
|
||||
}
|
||||
|
||||
let region = MemoryMappedRegion {
|
||||
address: addr as usize,
|
||||
size,
|
||||
file_descriptor: fd,
|
||||
};
|
||||
|
||||
|
||||
let region =
|
||||
MemoryMappedRegion { address: addr as usize, size, file_descriptor: fd };
|
||||
|
||||
self.mmap_regions.push(region);
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","✅ Memory mapped I/O created: {} bytes at {:p}", size, addr);
|
||||
Ok(addr as usize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
Err(anyhow::anyhow!("Memory mapped I/O not supported on this platform"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 获取I/O统计
|
||||
pub fn get_stats(&self) -> AsyncIOStats {
|
||||
AsyncIOStats {
|
||||
operations_queued: AtomicU64::new(self.async_io_stats.operations_queued.load(Ordering::Relaxed)),
|
||||
operations_completed: AtomicU64::new(self.async_io_stats.operations_completed.load(Ordering::Relaxed)),
|
||||
bytes_transferred: AtomicU64::new(self.async_io_stats.bytes_transferred.load(Ordering::Relaxed)),
|
||||
syscalls_avoided: AtomicU64::new(self.async_io_stats.syscalls_avoided.load(Ordering::Relaxed)),
|
||||
operations_queued: AtomicU64::new(
|
||||
self.async_io_stats.operations_queued.load(Ordering::Relaxed),
|
||||
),
|
||||
operations_completed: AtomicU64::new(
|
||||
self.async_io_stats.operations_completed.load(Ordering::Relaxed),
|
||||
),
|
||||
bytes_transferred: AtomicU64::new(
|
||||
self.async_io_stats.bytes_transferred.load(Ordering::Relaxed),
|
||||
),
|
||||
syscalls_avoided: AtomicU64::new(
|
||||
self.async_io_stats.syscalls_avoided.load(Ordering::Relaxed),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -357,47 +372,46 @@ impl SyscallBatchProcessor {
|
||||
pub fn new(batch_size: usize) -> Result<Self> {
|
||||
let pending_calls = crossbeam_queue::ArrayQueue::new(batch_size * 10);
|
||||
let executor = tokio::runtime::Handle::current();
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Syscall batch processor created with batch size: {}", batch_size);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
pending_calls,
|
||||
_executor: executor,
|
||||
batch_stats: CachePadded::new(AtomicU64::new(0)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 提交系统调用请求到批处理队列
|
||||
#[inline(always)]
|
||||
pub fn submit_request(&self, request: SyscallRequest) -> Result<()> {
|
||||
self.pending_calls.push(request)
|
||||
.map_err(|_| anyhow::anyhow!("Batch queue full"))?;
|
||||
|
||||
self.pending_calls.push(request).map_err(|_| anyhow::anyhow!("Batch queue full"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 执行批量系统调用
|
||||
pub async fn execute_batch(&self) -> Result<usize> {
|
||||
let mut batch = Vec::new();
|
||||
|
||||
|
||||
// 收集批量请求
|
||||
while batch.len() < 100 && !self.pending_calls.is_empty() {
|
||||
if let Some(request) = self.pending_calls.pop() {
|
||||
batch.push(request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if batch.is_empty() {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
|
||||
let batch_size = batch.len();
|
||||
|
||||
|
||||
// 按类型分组批量执行
|
||||
let mut write_requests = Vec::new();
|
||||
let mut read_requests = Vec::new();
|
||||
let mut network_requests = Vec::new();
|
||||
|
||||
|
||||
for request in batch {
|
||||
match request {
|
||||
SyscallRequest::Write { fd, data } => {
|
||||
@@ -414,28 +428,28 @@ impl SyscallBatchProcessor {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 批量执行写入
|
||||
if !write_requests.is_empty() {
|
||||
self.batch_write_operations(write_requests).await?;
|
||||
}
|
||||
|
||||
|
||||
// 批量执行读取
|
||||
if !read_requests.is_empty() {
|
||||
self.batch_read_operations(read_requests).await?;
|
||||
}
|
||||
|
||||
|
||||
// 批量执行网络操作
|
||||
if !network_requests.is_empty() {
|
||||
self.batch_network_operations(network_requests).await?;
|
||||
}
|
||||
|
||||
|
||||
self.batch_stats.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
|
||||
tracing::trace!(target: "sol_trade_sdk","Executed batch of {} syscalls", batch_size);
|
||||
Ok(batch_size)
|
||||
}
|
||||
|
||||
|
||||
/// 批量写入操作
|
||||
async fn batch_write_operations(&self, requests: Vec<(i32, Vec<u8>)>) -> Result<()> {
|
||||
// 使用writev系统调用进行批量写入
|
||||
@@ -445,7 +459,7 @@ impl SyscallBatchProcessor {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 批量读取操作
|
||||
async fn batch_read_operations(&self, requests: Vec<(i32, usize)>) -> Result<()> {
|
||||
// 使用readv系统调用进行批量读取
|
||||
@@ -454,7 +468,7 @@ impl SyscallBatchProcessor {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 批量网络操作
|
||||
async fn batch_network_operations(&self, requests: Vec<(i32, Vec<u8>)>) -> Result<()> {
|
||||
// 使用sendmsg/recvmsg进行批量网络操作
|
||||
@@ -482,22 +496,16 @@ impl SystemCallBypassManager {
|
||||
let fast_time_provider = Arc::new(FastTimeProvider::new(config.enable_vdso)?);
|
||||
let io_optimizer = Arc::new(IOOptimizer::new(&config)?);
|
||||
let stats = Arc::new(SyscallBypassStats::default());
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 System Call Bypass Manager initialized");
|
||||
tracing::info!(target: "sol_trade_sdk"," 📦 Batch Processing: {}", config.enable_batch_processing);
|
||||
tracing::info!(target: "sol_trade_sdk"," ⏰ Fast Time: {}", config.enable_fast_time);
|
||||
tracing::info!(target: "sol_trade_sdk"," 🚀 vDSO: {}", config.enable_vdso);
|
||||
tracing::info!(target: "sol_trade_sdk"," 📁 io_uring: {}", config.enable_io_uring);
|
||||
|
||||
Ok(Self {
|
||||
config,
|
||||
batch_processor,
|
||||
fast_time_provider,
|
||||
_io_optimizer: io_optimizer,
|
||||
stats,
|
||||
})
|
||||
|
||||
Ok(Self { config, batch_processor, fast_time_provider, _io_optimizer: io_optimizer, stats })
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 快速获取当前时间戳 - 绕过系统调用
|
||||
#[inline(always)]
|
||||
pub fn fast_timestamp_nanos(&self) -> u64 {
|
||||
@@ -505,28 +513,25 @@ impl SystemCallBypassManager {
|
||||
self.stats.time_calls_cached.fetch_add(1, Ordering::Relaxed);
|
||||
return self.fast_time_provider.fast_now_nanos();
|
||||
}
|
||||
|
||||
|
||||
// 回退到标准时间获取
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos() as u64
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_nanos() as u64
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 提交批量I/O操作
|
||||
pub async fn submit_batch_io(&self, operations: Vec<SyscallRequest>) -> Result<()> {
|
||||
if !self.config.enable_batch_processing {
|
||||
return Err(anyhow::anyhow!("Batch processing disabled"));
|
||||
}
|
||||
|
||||
|
||||
for op in operations {
|
||||
self.batch_processor.submit_request(op)?;
|
||||
}
|
||||
|
||||
|
||||
self.stats.syscalls_batched.fetch_add(1, Ordering::Relaxed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 执行优化的内存分配 - 绕过malloc系统调用
|
||||
#[inline(always)]
|
||||
pub fn fast_allocate(&self, size: usize) -> Result<*mut u8> {
|
||||
@@ -534,34 +539,30 @@ impl SystemCallBypassManager {
|
||||
self.stats.memory_operations_avoided.fetch_add(1, Ordering::Relaxed);
|
||||
return self.userspace_allocate(size);
|
||||
}
|
||||
|
||||
|
||||
// 回退到标准分配
|
||||
let layout = std::alloc::Layout::from_size_align(size, 8)?;
|
||||
let ptr = unsafe { std::alloc::alloc(layout) };
|
||||
|
||||
|
||||
if ptr.is_null() {
|
||||
Err(anyhow::anyhow!("Allocation failed"))
|
||||
} else {
|
||||
Ok(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 用户空间内存分配
|
||||
fn userspace_allocate(&self, size: usize) -> Result<*mut u8> {
|
||||
use std::sync::Mutex;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
|
||||
struct MemoryPool {
|
||||
pool: Box<[u8; 1024 * 1024]>,
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
static MEMORY_POOL: Lazy<Mutex<MemoryPool>> = Lazy::new(|| {
|
||||
Mutex::new(MemoryPool {
|
||||
pool: Box::new([0; 1024 * 1024]),
|
||||
offset: 0,
|
||||
})
|
||||
});
|
||||
static MEMORY_POOL: Lazy<Mutex<MemoryPool>> =
|
||||
Lazy::new(|| Mutex::new(MemoryPool { pool: Box::new([0; 1024 * 1024]), offset: 0 }));
|
||||
|
||||
let mut pool = MEMORY_POOL.lock().unwrap();
|
||||
|
||||
@@ -574,18 +575,18 @@ impl SystemCallBypassManager {
|
||||
|
||||
Ok(ptr)
|
||||
}
|
||||
|
||||
|
||||
/// 启动批处理工作线程
|
||||
pub async fn start_batch_processing(&self) -> Result<()> {
|
||||
let processor = Arc::clone(&self.batch_processor);
|
||||
let stats = Arc::clone(&self.stats);
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_micros(100)); // 100μs间隔
|
||||
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
|
||||
if let Ok(processed) = processor.execute_batch().await {
|
||||
if processed > 0 {
|
||||
stats.syscalls_bypassed.fetch_add(processed as u64, Ordering::Relaxed);
|
||||
@@ -593,11 +594,11 @@ impl SystemCallBypassManager {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","✅ Batch processing worker started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 获取绕过统计
|
||||
pub fn get_bypass_stats(&self) -> SyscallBypassStatsSnapshot {
|
||||
SyscallBypassStatsSnapshot {
|
||||
@@ -608,7 +609,7 @@ impl SystemCallBypassManager {
|
||||
memory_operations_avoided: self.stats.memory_operations_avoided.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 极致优化配置
|
||||
pub fn extreme_bypass_config() -> SyscallBypassConfig {
|
||||
SyscallBypassConfig {
|
||||
@@ -643,9 +644,11 @@ impl SyscallBypassStatsSnapshot {
|
||||
tracing::info!(target: "sol_trade_sdk"," ⏰ Time Calls Cached: {}", self.time_calls_cached);
|
||||
tracing::info!(target: "sol_trade_sdk"," 📁 I/O Operations Optimized: {}", self.io_operations_optimized);
|
||||
tracing::info!(target: "sol_trade_sdk"," 💾 Memory Operations Avoided: {}", self.memory_operations_avoided);
|
||||
|
||||
let total_optimizations = self.syscalls_bypassed + self.time_calls_cached +
|
||||
self.io_operations_optimized + self.memory_operations_avoided;
|
||||
|
||||
let total_optimizations = self.syscalls_bypassed
|
||||
+ self.time_calls_cached
|
||||
+ self.io_operations_optimized
|
||||
+ self.memory_operations_avoided;
|
||||
tracing::info!(target: "sol_trade_sdk"," 🏆 Total Optimizations: {}", total_optimizations);
|
||||
}
|
||||
}
|
||||
@@ -657,7 +660,7 @@ macro_rules! bypass_syscall {
|
||||
// 使用快速时间而不是系统调用
|
||||
crate::performance::syscall_bypass::GLOBAL_TIME_PROVIDER.fast_now_nanos()
|
||||
};
|
||||
|
||||
|
||||
(batch_io $ops:expr) => {
|
||||
// 批量提交I/O操作
|
||||
crate::performance::syscall_bypass::GLOBAL_BYPASS_MANAGER.submit_batch_io($ops).await
|
||||
@@ -667,60 +670,57 @@ macro_rules! bypass_syscall {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fast_time_provider() {
|
||||
let provider = FastTimeProvider::new(false).unwrap();
|
||||
|
||||
|
||||
let time1 = provider.fast_now_nanos();
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
let time2 = provider.fast_now_nanos();
|
||||
|
||||
|
||||
assert!(time2 > time1);
|
||||
assert!(time2 - time1 >= 1_000_000); // 至少1ms差异
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_syscall_batch_processor() {
|
||||
let processor = SyscallBatchProcessor::new(10).unwrap();
|
||||
|
||||
let request = SyscallRequest::Write {
|
||||
fd: 1,
|
||||
data: vec![1, 2, 3, 4, 5],
|
||||
};
|
||||
|
||||
|
||||
let request = SyscallRequest::Write { fd: 1, data: vec![1, 2, 3, 4, 5] };
|
||||
|
||||
processor.submit_request(request).unwrap();
|
||||
|
||||
|
||||
let processed = processor.execute_batch().await.unwrap();
|
||||
assert_eq!(processed, 1);
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_io_optimizer() {
|
||||
let config = SyscallBypassConfig::default();
|
||||
let optimizer = IOOptimizer::new(&config).unwrap();
|
||||
|
||||
|
||||
let requests = vec![(1, b"test data".as_ref())];
|
||||
let results = optimizer.batch_async_write(&requests).await.unwrap();
|
||||
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0], 9); // "test data".len()
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_system_call_bypass_manager() {
|
||||
let config = SyscallBypassConfig::default();
|
||||
let manager = SystemCallBypassManager::new(config).unwrap();
|
||||
|
||||
|
||||
// 测试快速时间戳
|
||||
let timestamp = manager.fast_timestamp_nanos();
|
||||
assert!(timestamp > 0);
|
||||
|
||||
|
||||
// 测试统计
|
||||
let stats = manager.get_bypass_stats();
|
||||
assert_eq!(stats.time_calls_cached, 1);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_extreme_bypass_config() {
|
||||
let config = SystemCallBypassManager::extreme_bypass_config();
|
||||
@@ -731,16 +731,16 @@ mod tests {
|
||||
assert_eq!(config.batch_size, 1000);
|
||||
assert_eq!(config.syscall_cache_size, 10000);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_userspace_allocation() {
|
||||
let config = SyscallBypassConfig::default();
|
||||
let manager = SystemCallBypassManager::new(config).unwrap();
|
||||
|
||||
|
||||
let ptr = manager.fast_allocate(64).unwrap();
|
||||
assert!(!ptr.is_null());
|
||||
|
||||
|
||||
let stats = manager.get_bypass_stats();
|
||||
assert_eq!(stats.memory_operations_avoided, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+133
-126
@@ -1,5 +1,5 @@
|
||||
//! 🚀 零拷贝内存映射IO - 完全消除数据拷贝开销
|
||||
//!
|
||||
//!
|
||||
//! 实现极致的零拷贝策略,包括:
|
||||
//! - 内存映射文件IO
|
||||
//! - 共享内存环形缓冲区
|
||||
@@ -10,11 +10,11 @@
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
// use std::mem::{size_of, MaybeUninit};
|
||||
use anyhow::{Context, Result};
|
||||
use crossbeam_utils::CachePadded;
|
||||
use memmap2::{MmapMut, MmapOptions};
|
||||
use std::ptr::NonNull;
|
||||
use std::slice;
|
||||
use memmap2::{MmapMut, MmapOptions};
|
||||
use anyhow::{Result, Context};
|
||||
use crossbeam_utils::CachePadded;
|
||||
|
||||
/// 🚀 零拷贝内存管理器
|
||||
pub struct ZeroCopyMemoryManager {
|
||||
@@ -50,17 +50,17 @@ impl SharedMemoryPool {
|
||||
// 确保块大小是64字节对齐(缓存行对齐)
|
||||
let aligned_block_size = (block_size + 63) & !63;
|
||||
let total_blocks = total_size / aligned_block_size;
|
||||
|
||||
|
||||
// 创建内存映射文件
|
||||
let memory_region = MmapOptions::new()
|
||||
.len(total_blocks * aligned_block_size)
|
||||
.map_anon()
|
||||
.context("Failed to create memory mapped region")?;
|
||||
|
||||
|
||||
// 初始化空闲块位图 (每个u64可以管理64个块)
|
||||
let bitmap_size = (total_blocks + 63) / 64;
|
||||
let mut free_blocks = Vec::with_capacity(bitmap_size);
|
||||
|
||||
|
||||
// 将所有块标记为空闲(全1)
|
||||
for i in 0..bitmap_size {
|
||||
let bits = if i == bitmap_size - 1 && total_blocks % 64 != 0 {
|
||||
@@ -72,10 +72,10 @@ impl SharedMemoryPool {
|
||||
};
|
||||
free_blocks.push(AtomicU64::new(bits));
|
||||
}
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Created shared memory pool {} with {} blocks of {} bytes each",
|
||||
pool_id, total_blocks, aligned_block_size);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
memory_region,
|
||||
free_blocks,
|
||||
@@ -85,31 +85,31 @@ impl SharedMemoryPool {
|
||||
pool_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 零拷贝分配内存块
|
||||
#[inline(always)]
|
||||
pub fn allocate_block(&self) -> Option<ZeroCopyBlock> {
|
||||
// 快速路径:尝试从预期位置分配
|
||||
let start_index = self.allocator_head.load(Ordering::Relaxed) / 64;
|
||||
|
||||
|
||||
// 遍历所有位图寻找空闲块
|
||||
for attempt in 0..self.free_blocks.len() {
|
||||
let bitmap_index = (start_index + attempt) % self.free_blocks.len();
|
||||
let bitmap = &self.free_blocks[bitmap_index];
|
||||
|
||||
|
||||
let mut current = bitmap.load(Ordering::Acquire);
|
||||
|
||||
|
||||
while current != 0 {
|
||||
// 找到最低位的1(最小的空闲块)
|
||||
let bit_pos = current.trailing_zeros() as usize;
|
||||
let mask = 1u64 << bit_pos;
|
||||
|
||||
|
||||
// 尝试原子地清除这一位(标记为已分配)
|
||||
match bitmap.compare_exchange_weak(
|
||||
current,
|
||||
current,
|
||||
current & !mask,
|
||||
Ordering::AcqRel,
|
||||
Ordering::Relaxed
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => {
|
||||
// 成功分配
|
||||
@@ -119,20 +119,17 @@ impl SharedMemoryPool {
|
||||
bitmap.fetch_or(mask, Ordering::Relaxed);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
let offset = block_index * self.block_size;
|
||||
let ptr = unsafe {
|
||||
NonNull::new_unchecked(
|
||||
self.memory_region.as_ptr().add(offset) as *mut u8
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
// 更新分配器头指针
|
||||
self.allocator_head.store(
|
||||
(block_index + 1) * 64,
|
||||
Ordering::Relaxed
|
||||
);
|
||||
|
||||
self.allocator_head.store((block_index + 1) * 64, Ordering::Relaxed);
|
||||
|
||||
return Some(ZeroCopyBlock {
|
||||
ptr,
|
||||
size: self.block_size,
|
||||
@@ -147,10 +144,10 @@ impl SharedMemoryPool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
None // 没有可用块
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 零拷贝释放内存块
|
||||
#[inline(always)]
|
||||
pub fn deallocate_block(&self, block: ZeroCopyBlock) {
|
||||
@@ -158,20 +155,21 @@ impl SharedMemoryPool {
|
||||
tracing::error!(target: "sol_trade_sdk", "Attempting to deallocate block from wrong pool");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let bitmap_index = block.block_index / 64;
|
||||
let bit_pos = block.block_index % 64;
|
||||
let mask = 1u64 << bit_pos;
|
||||
|
||||
|
||||
if bitmap_index < self.free_blocks.len() {
|
||||
// 原子地设置位为1(标记为空闲)
|
||||
self.free_blocks[bitmap_index].fetch_or(mask, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 获取可用块数量
|
||||
pub fn available_blocks(&self) -> usize {
|
||||
self.free_blocks.iter()
|
||||
self.free_blocks
|
||||
.iter()
|
||||
.map(|bitmap| bitmap.load(Ordering::Relaxed).count_ones() as usize)
|
||||
.sum()
|
||||
}
|
||||
@@ -195,49 +193,49 @@ impl ZeroCopyBlock {
|
||||
pub fn as_ptr(&self) -> *mut u8 {
|
||||
self.ptr.as_ptr()
|
||||
}
|
||||
|
||||
|
||||
/// 获取只读切片
|
||||
#[inline(always)]
|
||||
pub unsafe fn as_slice(&self) -> &[u8] {
|
||||
slice::from_raw_parts(self.ptr.as_ptr(), self.size)
|
||||
}
|
||||
|
||||
|
||||
/// 获取可变切片
|
||||
#[inline(always)]
|
||||
pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
|
||||
slice::from_raw_parts_mut(self.ptr.as_ptr(), self.size)
|
||||
}
|
||||
|
||||
|
||||
/// 获取块大小
|
||||
#[inline(always)]
|
||||
pub fn size(&self) -> usize {
|
||||
self.size
|
||||
}
|
||||
|
||||
|
||||
/// 零拷贝写入数据
|
||||
#[inline(always)]
|
||||
pub unsafe fn write_bytes(&mut self, data: &[u8]) -> Result<()> {
|
||||
if data.len() > self.size {
|
||||
return Err(anyhow::anyhow!("Data too large for block"));
|
||||
}
|
||||
|
||||
|
||||
// 使用硬件优化的内存拷贝
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
self.ptr.as_ptr(),
|
||||
data.as_ptr(),
|
||||
data.len()
|
||||
data.len(),
|
||||
);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
/// 零拷贝读取数据
|
||||
#[inline(always)]
|
||||
pub unsafe fn read_bytes(&self, len: usize) -> Result<&[u8]> {
|
||||
if len > self.size {
|
||||
return Err(anyhow::anyhow!("Read length exceeds block size"));
|
||||
}
|
||||
|
||||
|
||||
Ok(slice::from_raw_parts(self.ptr.as_ptr(), len))
|
||||
}
|
||||
}
|
||||
@@ -266,9 +264,9 @@ impl MemoryMappedBuffer {
|
||||
.len(size)
|
||||
.map_anon()
|
||||
.context("Failed to create memory mapped buffer")?;
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Created memory mapped buffer {} with size {} bytes", buffer_id, size);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
mmap,
|
||||
read_pos: CachePadded::new(AtomicUsize::new(0)),
|
||||
@@ -277,128 +275,136 @@ impl MemoryMappedBuffer {
|
||||
_buffer_id: buffer_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 零拷贝写入数据
|
||||
#[inline(always)]
|
||||
pub fn write_data(&self, data: &[u8]) -> Result<usize> {
|
||||
let data_len = data.len();
|
||||
let current_write = self.write_pos.load(Ordering::Relaxed);
|
||||
let current_read = self.read_pos.load(Ordering::Acquire);
|
||||
|
||||
|
||||
// 计算可用空间
|
||||
let available_space = if current_write >= current_read {
|
||||
self.size - (current_write - current_read) - 1
|
||||
} else {
|
||||
current_read - current_write - 1
|
||||
};
|
||||
|
||||
|
||||
if data_len > available_space {
|
||||
return Err(anyhow::anyhow!("Insufficient buffer space"));
|
||||
}
|
||||
|
||||
|
||||
// 零拷贝写入
|
||||
unsafe {
|
||||
let write_ptr = self.mmap.as_ptr().add(current_write) as *mut u8;
|
||||
|
||||
|
||||
if current_write + data_len <= self.size {
|
||||
// 数据不跨越缓冲区边界
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
write_ptr, data.as_ptr(), data_len
|
||||
write_ptr,
|
||||
data.as_ptr(),
|
||||
data_len,
|
||||
);
|
||||
} else {
|
||||
// 数据跨越缓冲区边界,分两段写入
|
||||
let first_part = self.size - current_write;
|
||||
let second_part = data_len - first_part;
|
||||
|
||||
|
||||
// 写入第一部分
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
write_ptr, data.as_ptr(), first_part
|
||||
write_ptr,
|
||||
data.as_ptr(),
|
||||
first_part,
|
||||
);
|
||||
|
||||
|
||||
// 写入第二部分(从缓冲区开头)
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
self.mmap.as_ptr() as *mut u8,
|
||||
data.as_ptr().add(first_part),
|
||||
second_part
|
||||
self.mmap.as_ptr() as *mut u8,
|
||||
data.as_ptr().add(first_part),
|
||||
second_part,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 更新写指针
|
||||
let new_write_pos = (current_write + data_len) % self.size;
|
||||
self.write_pos.store(new_write_pos, Ordering::Release);
|
||||
|
||||
|
||||
Ok(data_len)
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 零拷贝读取数据
|
||||
#[inline(always)]
|
||||
pub fn read_data(&self, buffer: &mut [u8]) -> Result<usize> {
|
||||
let buffer_len = buffer.len();
|
||||
let current_read = self.read_pos.load(Ordering::Relaxed);
|
||||
let current_write = self.write_pos.load(Ordering::Acquire);
|
||||
|
||||
|
||||
// 计算可读数据量
|
||||
let available_data = if current_write >= current_read {
|
||||
current_write - current_read
|
||||
} else {
|
||||
self.size - (current_read - current_write)
|
||||
};
|
||||
|
||||
|
||||
if available_data == 0 {
|
||||
return Ok(0); // 无数据可读
|
||||
}
|
||||
|
||||
|
||||
let read_len = buffer_len.min(available_data);
|
||||
|
||||
|
||||
// 零拷贝读取
|
||||
unsafe {
|
||||
let read_ptr = self.mmap.as_ptr().add(current_read);
|
||||
|
||||
|
||||
if current_read + read_len <= self.size {
|
||||
// 数据不跨越缓冲区边界
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
buffer.as_mut_ptr(), read_ptr, read_len
|
||||
buffer.as_mut_ptr(),
|
||||
read_ptr,
|
||||
read_len,
|
||||
);
|
||||
} else {
|
||||
// 数据跨越缓冲区边界,分两段读取
|
||||
let first_part = self.size - current_read;
|
||||
let second_part = read_len - first_part;
|
||||
|
||||
|
||||
// 读取第一部分
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
buffer.as_mut_ptr(), read_ptr, first_part
|
||||
buffer.as_mut_ptr(),
|
||||
read_ptr,
|
||||
first_part,
|
||||
);
|
||||
|
||||
|
||||
// 读取第二部分(从缓冲区开头)
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
buffer.as_mut_ptr().add(first_part),
|
||||
self.mmap.as_ptr(),
|
||||
second_part
|
||||
self.mmap.as_ptr(),
|
||||
second_part,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 更新读指针
|
||||
let new_read_pos = (current_read + read_len) % self.size;
|
||||
self.read_pos.store(new_read_pos, Ordering::Release);
|
||||
|
||||
|
||||
Ok(read_len)
|
||||
}
|
||||
|
||||
|
||||
/// 获取可读数据量
|
||||
#[inline(always)]
|
||||
pub fn available_data(&self) -> usize {
|
||||
let current_read = self.read_pos.load(Ordering::Relaxed);
|
||||
let current_write = self.write_pos.load(Ordering::Relaxed);
|
||||
|
||||
|
||||
if current_write >= current_read {
|
||||
current_write - current_read
|
||||
} else {
|
||||
self.size - (current_read - current_write)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 获取可用空间
|
||||
#[inline(always)]
|
||||
pub fn available_space(&self) -> usize {
|
||||
@@ -420,38 +426,39 @@ impl DirectMemoryAccessManager {
|
||||
/// 创建DMA管理器
|
||||
pub fn new(num_channels: usize) -> Result<Self> {
|
||||
let mut dma_channels = Vec::with_capacity(num_channels);
|
||||
|
||||
|
||||
for i in 0..num_channels {
|
||||
dma_channels.push(Arc::new(DMAChannel::new(i)?));
|
||||
}
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Created DMA manager with {} channels", num_channels);
|
||||
|
||||
|
||||
Ok(Self {
|
||||
dma_channels,
|
||||
channel_allocator: AtomicUsize::new(0),
|
||||
dma_stats: Arc::new(DMAStats::new()),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 执行零拷贝DMA传输
|
||||
#[inline(always)]
|
||||
pub async fn dma_transfer(&self, src: &[u8], dst: &mut [u8]) -> Result<usize> {
|
||||
if src.len() != dst.len() {
|
||||
return Err(anyhow::anyhow!("Source and destination sizes don't match"));
|
||||
}
|
||||
|
||||
|
||||
// 选择DMA通道(轮询分配)
|
||||
let channel_index = self.channel_allocator.fetch_add(1, Ordering::Relaxed) % self.dma_channels.len();
|
||||
let channel_index =
|
||||
self.channel_allocator.fetch_add(1, Ordering::Relaxed) % self.dma_channels.len();
|
||||
let channel = &self.dma_channels[channel_index];
|
||||
|
||||
|
||||
// 执行DMA传输
|
||||
let transferred = channel.transfer(src, dst).await?;
|
||||
|
||||
|
||||
// 更新统计
|
||||
self.dma_stats.bytes_transferred.fetch_add(transferred as u64, Ordering::Relaxed);
|
||||
self.dma_stats.transfers_completed.fetch_add(1, Ordering::Relaxed);
|
||||
|
||||
|
||||
Ok(transferred)
|
||||
}
|
||||
}
|
||||
@@ -475,21 +482,21 @@ impl DMAChannel {
|
||||
_status: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 执行零拷贝传输
|
||||
#[inline(always)]
|
||||
pub async fn transfer(&self, src: &[u8], dst: &mut [u8]) -> Result<usize> {
|
||||
let transfer_size = src.len();
|
||||
|
||||
|
||||
// 使用硬件优化的SIMD内存拷贝
|
||||
unsafe {
|
||||
super::hardware_optimizations::SIMDMemoryOps::memcpy_simd_optimized(
|
||||
dst.as_mut_ptr(),
|
||||
src.as_ptr(),
|
||||
transfer_size
|
||||
transfer_size,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Ok(transfer_size)
|
||||
}
|
||||
}
|
||||
@@ -541,14 +548,14 @@ impl ZeroCopyStats {
|
||||
mmap_buffer_usage: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 打印统计信息
|
||||
pub fn print_stats(&self) {
|
||||
let allocated = self.blocks_allocated.load(Ordering::Relaxed);
|
||||
let freed = self.blocks_freed.load(Ordering::Relaxed);
|
||||
let bytes = self.bytes_transferred.load(Ordering::Relaxed);
|
||||
let mmap_usage = self.mmap_buffer_usage.load(Ordering::Relaxed);
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Zero-Copy Stats:");
|
||||
tracing::info!(target: "sol_trade_sdk"," 📦 Blocks: Allocated={}, Freed={}, Active={}",
|
||||
allocated, freed, allocated.saturating_sub(freed));
|
||||
@@ -564,36 +571,36 @@ impl ZeroCopyMemoryManager {
|
||||
pub fn new() -> Result<Self> {
|
||||
let mut shared_pools = Vec::new();
|
||||
let mut mmap_buffers = Vec::new();
|
||||
|
||||
|
||||
// 创建不同大小的内存池
|
||||
// 小块池: 64KB blocks, 1GB total
|
||||
shared_pools.push(Arc::new(SharedMemoryPool::new(0, 1024 * 1024 * 1024, 64 * 1024)?));
|
||||
// 中块池: 1MB blocks, 4GB total
|
||||
// 中块池: 1MB blocks, 4GB total
|
||||
shared_pools.push(Arc::new(SharedMemoryPool::new(1, 4 * 1024 * 1024 * 1024, 1024 * 1024)?));
|
||||
// 大块池: 16MB blocks, 8GB total
|
||||
shared_pools.push(Arc::new(SharedMemoryPool::new(2, 8 * 1024 * 1024 * 1024, 16 * 1024 * 1024)?));
|
||||
|
||||
shared_pools.push(Arc::new(SharedMemoryPool::new(
|
||||
2,
|
||||
8 * 1024 * 1024 * 1024,
|
||||
16 * 1024 * 1024,
|
||||
)?));
|
||||
|
||||
// 创建内存映射缓冲区
|
||||
for i in 0..8 {
|
||||
mmap_buffers.push(Arc::new(MemoryMappedBuffer::new(i, 256 * 1024 * 1024)?)); // 256MB each
|
||||
mmap_buffers.push(Arc::new(MemoryMappedBuffer::new(i, 256 * 1024 * 1024)?));
|
||||
// 256MB each
|
||||
}
|
||||
|
||||
|
||||
let dma_manager = Arc::new(DirectMemoryAccessManager::new(16)?); // 16 DMA channels
|
||||
let stats = Arc::new(ZeroCopyStats::new());
|
||||
|
||||
|
||||
tracing::info!(target: "sol_trade_sdk","🚀 Zero-Copy Memory Manager initialized");
|
||||
tracing::info!(target: "sol_trade_sdk"," 📦 Memory Pools: {}", shared_pools.len());
|
||||
tracing::info!(target: "sol_trade_sdk"," 💾 Mapped Buffers: {}", mmap_buffers.len());
|
||||
tracing::info!(target: "sol_trade_sdk"," 🔄 DMA Channels: 16");
|
||||
|
||||
Ok(Self {
|
||||
shared_pools,
|
||||
mmap_buffers,
|
||||
dma_manager,
|
||||
stats,
|
||||
})
|
||||
|
||||
Ok(Self { shared_pools, mmap_buffers, dma_manager, stats })
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 分配零拷贝内存块
|
||||
#[inline(always)]
|
||||
pub fn allocate(&self, size: usize) -> Option<ZeroCopyBlock> {
|
||||
@@ -605,7 +612,7 @@ impl ZeroCopyMemoryManager {
|
||||
} else {
|
||||
&self.shared_pools[2] // 大块池
|
||||
};
|
||||
|
||||
|
||||
if let Some(block) = pool.allocate_block() {
|
||||
self.stats.blocks_allocated.fetch_add(1, Ordering::Relaxed);
|
||||
Some(block)
|
||||
@@ -613,7 +620,7 @@ impl ZeroCopyMemoryManager {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 🚀 释放零拷贝内存块
|
||||
#[inline(always)]
|
||||
pub fn deallocate(&self, block: ZeroCopyBlock) {
|
||||
@@ -623,19 +630,19 @@ impl ZeroCopyMemoryManager {
|
||||
self.stats.blocks_freed.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 获取内存映射缓冲区
|
||||
#[inline(always)]
|
||||
pub fn get_mmap_buffer(&self, buffer_id: usize) -> Option<Arc<MemoryMappedBuffer>> {
|
||||
self.mmap_buffers.get(buffer_id).cloned()
|
||||
}
|
||||
|
||||
|
||||
/// 获取DMA管理器
|
||||
#[inline(always)]
|
||||
pub fn get_dma_manager(&self) -> Arc<DirectMemoryAccessManager> {
|
||||
self.dma_manager.clone()
|
||||
}
|
||||
|
||||
|
||||
/// 获取统计信息
|
||||
pub fn get_stats(&self) -> Arc<ZeroCopyStats> {
|
||||
self.stats.clone()
|
||||
@@ -645,73 +652,73 @@ impl ZeroCopyMemoryManager {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_shared_memory_pool() -> Result<()> {
|
||||
let pool = SharedMemoryPool::new(0, 1024 * 1024, 4096)?;
|
||||
|
||||
|
||||
// 测试分配
|
||||
let block1 = pool.allocate_block().expect("Should allocate block");
|
||||
assert_eq!(block1.size(), 4096);
|
||||
|
||||
|
||||
let block2 = pool.allocate_block().expect("Should allocate another block");
|
||||
assert_eq!(block2.size(), 4096);
|
||||
|
||||
|
||||
// 测试释放
|
||||
pool.deallocate_block(block1);
|
||||
pool.deallocate_block(block2);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_memory_mapped_buffer() -> Result<()> {
|
||||
let buffer = MemoryMappedBuffer::new(0, 1024 * 1024)?;
|
||||
|
||||
|
||||
let test_data = b"Hello, Zero-Copy World!";
|
||||
|
||||
|
||||
// 测试写入
|
||||
let written = buffer.write_data(test_data)?;
|
||||
assert_eq!(written, test_data.len());
|
||||
|
||||
|
||||
// 测试读取
|
||||
let mut read_buffer = vec![0u8; test_data.len()];
|
||||
let read = buffer.read_data(&mut read_buffer)?;
|
||||
assert_eq!(read, test_data.len());
|
||||
assert_eq!(&read_buffer, test_data);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_dma_transfer() -> Result<()> {
|
||||
let dma_manager = DirectMemoryAccessManager::new(4)?;
|
||||
|
||||
|
||||
let src = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
|
||||
let mut dst = vec![0u8; 8];
|
||||
|
||||
|
||||
let transferred = dma_manager.dma_transfer(&src, &mut dst).await?;
|
||||
assert_eq!(transferred, 8);
|
||||
assert_eq!(src, dst);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_zero_copy_manager() -> Result<()> {
|
||||
let manager = ZeroCopyMemoryManager::new()?;
|
||||
|
||||
|
||||
// 测试小块分配
|
||||
let small_block = manager.allocate(1024).expect("Should allocate small block");
|
||||
assert_eq!(small_block.size(), 65536); // 小块池的块大小
|
||||
|
||||
|
||||
// 测试大块分配
|
||||
let large_block = manager.allocate(5 * 1024 * 1024).expect("Should allocate large block");
|
||||
assert_eq!(large_block.size(), 16 * 1024 * 1024); // 大块池的块大小
|
||||
|
||||
|
||||
manager.deallocate(small_block);
|
||||
manager.deallocate(large_block);
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+204
-98
@@ -2,45 +2,74 @@ use crate::swqos::common::{default_http_client_builder, poll_transaction_confirm
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use tracing::warn;
|
||||
|
||||
use std::time::Duration;
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use bincode::serialize as bincode_serialize;
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
/// Empty body for getHealth POST; avoid per-request allocation.
|
||||
static PING_BODY: &[u8] = &[];
|
||||
|
||||
use crate::swqos::astralane_quic::AstralaneQuicClient;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AstralaneBackend {
|
||||
Http {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
/// Mirrors global `mev_protection`: adds `mev-protect=true` on HTTP sends (QUIC uses :9000 instead).
|
||||
mev_http: bool,
|
||||
http_client: Client,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
},
|
||||
Quic(Arc<AstralaneQuicClient>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AstralaneClient {
|
||||
pub endpoint: String,
|
||||
pub auth_token: String,
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
pub http_client: Client,
|
||||
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
pub stop_ping: Arc<AtomicBool>,
|
||||
backend: AstralaneBackend,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for AstralaneClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *ASTRALANE_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ASTRALANE_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *ASTRALANE_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| ASTRALANE_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -50,60 +79,86 @@ impl SwqosClientTrait for AstralaneClient {
|
||||
}
|
||||
|
||||
impl AstralaneClient {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
/// HTTP 提交:`/iris`(Plain)或 `/irisb`(Binary),由 `endpoint` URL 路径区分;`mev_http` 为 true 时附加 `mev-protect=true`。
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String, mev_http: bool) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
stop_ping: Arc::new(AtomicBool::new(false)),
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
backend: AstralaneBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_http,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
},
|
||||
};
|
||||
|
||||
// Start ping task
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
/// Start periodic ping task to keep connections active
|
||||
/// 使用 QUIC 提交。
|
||||
pub async fn new_quic(rpc_url: String, quic_endpoint: &str, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let quic_client = AstralaneQuicClient::connect(quic_endpoint, &api_key).await?;
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
backend: AstralaneBackend::Quic(Arc::new(quic_client)),
|
||||
})
|
||||
}
|
||||
|
||||
async fn start_ping_task(&self) {
|
||||
let endpoint = self.endpoint.clone();
|
||||
let auth_token = self.auth_token.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await; // first tick completes immediately → one ping at start
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Astralane ping request failed: {}", e);
|
||||
match &self.backend {
|
||||
AstralaneBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
..
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
let http_client = http_client.clone();
|
||||
let ping_handle = ping_handle.clone();
|
||||
let stop_ping = stop_ping.clone();
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) =
|
||||
Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
warn!(target: "sol_trade_sdk", "Astralane ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut guard = ping_handle.lock().await;
|
||||
if let Some(old) = guard.as_ref() {
|
||||
old.abort();
|
||||
}
|
||||
*guard = Some(handle);
|
||||
}
|
||||
});
|
||||
|
||||
// Update ping_handle - use Mutex to safely update
|
||||
{
|
||||
let mut ping_guard = self.ping_handle.lock().await;
|
||||
if let Some(old_handle) = ping_guard.as_ref() {
|
||||
old_handle.abort();
|
||||
}
|
||||
*ping_guard = Some(handle);
|
||||
AstralaneBackend::Quic(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<()> {
|
||||
/// Send ping request: POST endpoint?api-key=...&method=getHealth
|
||||
async fn send_ping_request(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
auth_token: &str,
|
||||
) -> Result<()> {
|
||||
let response = http_client
|
||||
.post(endpoint)
|
||||
.query(&[("api-key", auth_token), ("method", "getHealth")])
|
||||
@@ -112,57 +167,107 @@ impl AstralaneClient {
|
||||
.send()
|
||||
.await?;
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await; // consume body so connection returns to pool
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!("Astralane ping request returned non-success status: {}", status);
|
||||
warn!(target: "sol_trade_sdk", "Astralane ping request returned non-success status: {}", status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send transaction via /irisb binary API (no Base64; lower latency).
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let signature = transaction.get_signature();
|
||||
let body_bytes = bincode_serialize(transaction)
|
||||
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||
|
||||
let body_bytes = bincode_serialize(transaction).map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||
|
||||
let response = self.http_client
|
||||
.post(&self.endpoint)
|
||||
.query(&[("api-key", self.auth_token.as_str()), ("method", "sendTransaction")])
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if status.is_success() {
|
||||
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else {
|
||||
eprintln!(" [astralane] {} submission failed: status {}", trade_type, status);
|
||||
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
|
||||
match &self.backend {
|
||||
AstralaneBackend::Http { endpoint, auth_token, mev_http, http_client, .. } => {
|
||||
let mut req = http_client
|
||||
.post(endpoint)
|
||||
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")]);
|
||||
if *mev_http {
|
||||
req = req.query(&[("mev-protect", "true")]);
|
||||
}
|
||||
let response = req
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
.await?;
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if status.is_success() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {}", status),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
|
||||
}
|
||||
}
|
||||
AstralaneBackend::Quic(quic) => {
|
||||
if let Err(e) = quic.send_transaction(&body_bytes).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [astralane] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [astralane] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -170,18 +275,19 @@ impl AstralaneClient {
|
||||
|
||||
impl Drop for AstralaneClient {
|
||||
fn drop(&mut self) {
|
||||
// Ensure ping task stops when client is destroyed
|
||||
self.stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
// Try to stop ping task immediately
|
||||
// Use tokio::spawn to avoid blocking Drop
|
||||
let ping_handle = self.ping_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut ping_guard = ping_handle.lock().await;
|
||||
if let Some(handle) = ping_guard.as_ref() {
|
||||
handle.abort();
|
||||
match &self.backend {
|
||||
AstralaneBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
stop_ping.store(true, Ordering::Relaxed);
|
||||
let ping_handle = ping_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut guard = ping_handle.lock().await;
|
||||
if let Some(handle) = guard.as_ref() {
|
||||
handle.abort();
|
||||
}
|
||||
*guard = None;
|
||||
});
|
||||
}
|
||||
*ping_guard = None;
|
||||
});
|
||||
AstralaneBackend::Quic(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
//! 内联自 [Astralane/astralane-quic-client](https://github.com/Astralane/astralane-quic-client),
|
||||
//! 用于向 Astralane QUIC TPU 提交交易,不依赖外部 crate,便于审计与安全可控。
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use quinn::crypto::rustls::QuicClientConfig;
|
||||
use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig};
|
||||
use rcgen::{CertificateParams, KeyPair};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{info, warn};
|
||||
|
||||
/// ALPN protocol identifier for Astralane TPU.
|
||||
const ALPN_ASTRALANE_TPU: &[u8] = b"astralane-tpu";
|
||||
|
||||
/// Maximum Solana transaction size.
|
||||
pub const MAX_TRANSACTION_SIZE: usize = 1232;
|
||||
|
||||
/// QUIC application error codes returned by the server.
|
||||
pub mod error_code {
|
||||
pub const OK: u32 = 0;
|
||||
pub const UNKNOWN_API_KEY: u32 = 1;
|
||||
pub const CONNECTION_LIMIT: u32 = 2;
|
||||
|
||||
pub fn describe(code: u32) -> &'static str {
|
||||
match code {
|
||||
OK => "OK",
|
||||
UNKNOWN_API_KEY => "Unknown API key",
|
||||
CONNECTION_LIMIT => "Connection limit exceeded",
|
||||
_ => "Unknown error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// QUIC client for sending transactions to Astralane's TPU endpoint.
|
||||
pub struct AstralaneQuicClient {
|
||||
endpoint: Endpoint,
|
||||
connection: Mutex<Connection>,
|
||||
server_addr: SocketAddr,
|
||||
server_candidates: Vec<SocketAddr>,
|
||||
next_server_idx: AtomicUsize,
|
||||
#[allow(dead_code)]
|
||||
api_key: String,
|
||||
}
|
||||
|
||||
impl AstralaneQuicClient {
|
||||
#[inline]
|
||||
fn astralane_quic_ip_candidates(host: &str, port: u16) -> Vec<SocketAddr> {
|
||||
// Official recommended direct-IP list (faster/more stable than DNS-only for QUIC).
|
||||
// We intentionally avoid fr2/ams2 per prior guidance.
|
||||
// Both port 7000 (standard) and port 9000 (MEV-protected) use the same IPs.
|
||||
match host {
|
||||
"fr.gateway.astralane.io" => vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), port),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), port),
|
||||
],
|
||||
"ny.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), port)]
|
||||
}
|
||||
"ams.gateway.astralane.io" => vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), port),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), port),
|
||||
],
|
||||
"la.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), port)]
|
||||
}
|
||||
"lim.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), port)]
|
||||
}
|
||||
"sg.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), port)]
|
||||
}
|
||||
"lit.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_host_port(server_addr: &str) -> Option<(&str, u16)> {
|
||||
let (host, port_str) = server_addr.rsplit_once(':')?;
|
||||
let port = port_str.parse::<u16>().ok()?;
|
||||
Some((host, port))
|
||||
}
|
||||
|
||||
/// Resolve `host:port` and prefer IPv4 result to avoid v6-remote/v4-local mismatch.
|
||||
#[inline]
|
||||
fn resolve_server_candidates(server_addr: &str) -> Result<Vec<SocketAddr>> {
|
||||
if let Ok(addr) = SocketAddr::from_str(server_addr) {
|
||||
return Ok(vec![addr]);
|
||||
}
|
||||
|
||||
let mut candidates: Vec<SocketAddr> = Vec::with_capacity(8);
|
||||
if let Some((host, port)) = Self::parse_host_port(server_addr) {
|
||||
candidates.extend(Self::astralane_quic_ip_candidates(host, port));
|
||||
}
|
||||
|
||||
use std::net::ToSocketAddrs;
|
||||
let mut addrs: Vec<SocketAddr> = server_addr
|
||||
.to_socket_addrs()
|
||||
.with_context(|| format!("Cannot resolve address: {}", server_addr))?
|
||||
.collect();
|
||||
if addrs.is_empty() && candidates.is_empty() {
|
||||
anyhow::bail!("Cannot resolve address: {}", server_addr);
|
||||
}
|
||||
// QUIC in many bot/VPS environments is primarily IPv4; prefer A over AAAA.
|
||||
addrs.sort_by_key(|a| if a.is_ipv4() { 0 } else { 1 });
|
||||
for addr in addrs {
|
||||
if !candidates.contains(&addr) {
|
||||
candidates.push(addr);
|
||||
}
|
||||
}
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn local_bind_for_remote(remote: SocketAddr) -> SocketAddr {
|
||||
match remote.ip() {
|
||||
IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to an Astralane QUIC server.
|
||||
/// Generates a self-signed TLS certificate with the API key as the Common Name (CN).
|
||||
pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let candidates =
|
||||
Self::resolve_server_candidates(server_addr).context("Invalid server address")?;
|
||||
let addr = candidates[0];
|
||||
|
||||
info!("[astralane-quic] Building TLS config (CN = api_key)");
|
||||
let client_config = Self::build_client_config(api_key)?;
|
||||
|
||||
let mut endpoint = Endpoint::client(Self::local_bind_for_remote(addr))
|
||||
.context("Failed to create QUIC endpoint")?;
|
||||
endpoint.set_default_client_config(client_config);
|
||||
|
||||
info!("[astralane-quic] Connecting to {} ...", addr);
|
||||
let mut last_err: Option<anyhow::Error> = None;
|
||||
let mut selected_addr = addr;
|
||||
let mut connection_opt: Option<Connection> = None;
|
||||
for candidate in &candidates {
|
||||
selected_addr = *candidate;
|
||||
match endpoint.connect(*candidate, "astralane") {
|
||||
Ok(connecting) => match connecting.await {
|
||||
Ok(conn) => {
|
||||
connection_opt = Some(conn);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("[astralane-quic] connect failed for {}: {}", candidate, e);
|
||||
last_err = Some(e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("[astralane-quic] connect setup failed for {}: {}", candidate, e);
|
||||
last_err = Some(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
let connection = connection_opt.ok_or_else(|| {
|
||||
last_err
|
||||
.unwrap_or_else(|| anyhow::anyhow!("Failed to connect to Astralane QUIC server"))
|
||||
})?;
|
||||
|
||||
info!("[astralane-quic] Connected at {}", selected_addr);
|
||||
|
||||
Ok(Self {
|
||||
endpoint,
|
||||
connection: Mutex::new(connection),
|
||||
server_addr: selected_addr,
|
||||
server_candidates: candidates,
|
||||
next_server_idx: AtomicUsize::new(0),
|
||||
api_key: api_key.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn reconnect_next_candidate(&self) -> Result<Connection> {
|
||||
let total = self.server_candidates.len().max(1);
|
||||
let start = self.next_server_idx.fetch_add(1, Ordering::Relaxed) % total;
|
||||
let mut last_err: Option<anyhow::Error> = None;
|
||||
for offset in 0..total {
|
||||
let idx = (start + offset) % total;
|
||||
let addr = self.server_candidates[idx];
|
||||
match self.endpoint.connect(addr, "astralane") {
|
||||
Ok(connecting) => match connecting.await {
|
||||
Ok(conn) => return Ok(conn),
|
||||
Err(e) => {
|
||||
warn!("[astralane-quic] reconnect failed for {}: {}", addr, e);
|
||||
last_err = Some(e.into());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
warn!("[astralane-quic] reconnect setup failed for {}: {}", addr, e);
|
||||
last_err = Some(e.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err
|
||||
.unwrap_or_else(|| anyhow::anyhow!("Failed to reconnect to Astralane QUIC server")))
|
||||
}
|
||||
|
||||
/// Send a single bincode-serialized `VersionedTransaction`.
|
||||
/// Fire-and-forget; automatically reconnects if the connection is dead.
|
||||
pub async fn send_transaction(&self, transaction_bytes: &[u8]) -> Result<()> {
|
||||
if transaction_bytes.len() > MAX_TRANSACTION_SIZE {
|
||||
anyhow::bail!(
|
||||
"Transaction too large: {} bytes (max {})",
|
||||
transaction_bytes.len(),
|
||||
MAX_TRANSACTION_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
let conn = {
|
||||
let mut guard = self.connection.lock().await;
|
||||
if let Some(reason) = guard.close_reason() {
|
||||
if let quinn::ConnectionError::ApplicationClosed(ref info) = reason {
|
||||
let code = info.error_code.into_inner();
|
||||
if code != error_code::OK as u64 {
|
||||
anyhow::bail!(
|
||||
"Server closed connection: {} (code {})",
|
||||
error_code::describe(code as u32),
|
||||
code
|
||||
);
|
||||
}
|
||||
}
|
||||
warn!("[astralane-quic] Connection dead, reconnecting to {} ...", self.server_addr);
|
||||
let new_conn = self.reconnect_next_candidate().await?;
|
||||
*guard = new_conn.clone();
|
||||
info!("[astralane-quic] Reconnected");
|
||||
}
|
||||
guard.clone()
|
||||
};
|
||||
|
||||
let mut send_stream =
|
||||
conn.open_uni().await.context("Failed to open unidirectional stream")?;
|
||||
|
||||
send_stream
|
||||
.write_all(transaction_bytes)
|
||||
.await
|
||||
.context("Failed to write transaction data")?;
|
||||
|
||||
send_stream.finish().context("Failed to finish stream")?;
|
||||
info!("[astralane-quic] Transaction sent ({} bytes)", transaction_bytes.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reconnect to the server if the connection was closed.
|
||||
pub async fn reconnect(&self) -> Result<()> {
|
||||
let mut guard = self.connection.lock().await;
|
||||
if guard.close_reason().is_some() {
|
||||
info!("[astralane-quic] Reconnecting at {}", self.server_addr);
|
||||
*guard = self.reconnect_next_candidate().await?;
|
||||
info!("[astralane-quic] Reconnected");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the connection is still alive.
|
||||
pub async fn is_connected(&self) -> bool {
|
||||
self.connection.lock().await.close_reason().is_none()
|
||||
}
|
||||
|
||||
/// Close the connection gracefully.
|
||||
pub async fn close(&self) {
|
||||
self.connection.lock().await.close(error_code::OK.into(), b"client closing");
|
||||
}
|
||||
|
||||
fn build_client_config(api_key: &str) -> Result<ClientConfig> {
|
||||
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||
let mut cert_params = CertificateParams::new(vec![])?;
|
||||
cert_params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, rcgen::DnValue::Utf8String(api_key.to_string()));
|
||||
let cert = cert_params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
|
||||
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
|
||||
.with_client_auth_cert(vec![cert_der], key_der)
|
||||
.context("Failed to set client certificate")?;
|
||||
|
||||
crypto.alpn_protocols = vec![ALPN_ASTRALANE_TPU.to_vec()];
|
||||
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(30)).unwrap()));
|
||||
transport.keep_alive_interval(Some(Duration::from_secs(25)));
|
||||
|
||||
let mut client_config =
|
||||
ClientConfig::new(Arc::new(QuicClientConfig::try_from(crypto).unwrap()));
|
||||
client_config.transport_config(Arc::new(transport));
|
||||
|
||||
Ok(client_config)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for AstralaneQuicClient {
|
||||
fn drop(&mut self) {
|
||||
self.connection.get_mut().close(error_code::OK.into(), b"client closing");
|
||||
}
|
||||
}
|
||||
|
||||
/// Skip server certificate verification (Astralane server may use self-signed cert).
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA256,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA512,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
||||
rustls::SignatureScheme::ED25519,
|
||||
]
|
||||
}
|
||||
}
|
||||
+463
-102
@@ -1,43 +1,146 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use std::time::Duration;
|
||||
use arc_swap::ArcSwap;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::BLOCKRAZOR_TIP_ACCOUNTS};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
use tonic::metadata::AsciiMetadataValue;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
// Include pre-generated gRPC code
|
||||
pub mod serverpb {
|
||||
include!("pb/serverpb.rs");
|
||||
}
|
||||
|
||||
// gRPC client wrapper
|
||||
#[derive(Clone)]
|
||||
pub struct BlockRazorGrpcClient {
|
||||
channel: Channel,
|
||||
auth_token: String,
|
||||
}
|
||||
|
||||
impl BlockRazorGrpcClient {
|
||||
pub fn new(channel: Channel, auth_token: String) -> Self {
|
||||
Self { channel, auth_token }
|
||||
}
|
||||
|
||||
pub async fn get_health(&self) -> Result<String> {
|
||||
let mut client = serverpb::server_client::ServerClient::new(self.channel.clone());
|
||||
let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?;
|
||||
|
||||
let mut request = tonic::Request::new(serverpb::HealthRequest {});
|
||||
request.metadata_mut().insert("apikey", apikey);
|
||||
|
||||
let response = client
|
||||
.get_health(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("gRPC health check failed: {}", e))?;
|
||||
Ok(response.into_inner().status)
|
||||
}
|
||||
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
transaction: String,
|
||||
mode: String,
|
||||
safe_window: Option<i32>,
|
||||
revert_protection: bool,
|
||||
) -> Result<String> {
|
||||
// 检查交易数据大小
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor transaction size: {} bytes", transaction.len());
|
||||
}
|
||||
|
||||
let mut client = serverpb::server_client::ServerClient::new(self.channel.clone());
|
||||
let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?;
|
||||
|
||||
let mut request = tonic::Request::new(serverpb::SendRequest {
|
||||
transaction,
|
||||
mode: String::from(mode),
|
||||
safe_window,
|
||||
revert_protection,
|
||||
});
|
||||
request.metadata_mut().insert("apikey", apikey);
|
||||
|
||||
let response = client
|
||||
.send_transaction(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("gRPC send transaction failed: {}", e))?;
|
||||
Ok(response.into_inner().signature)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum BlockRazorBackend {
|
||||
Grpc {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
grpc_client: Arc<ArcSwap<BlockRazorGrpcClient>>,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
/// When true, gRPC send_transaction sets revert_protection=true for MEV protection.
|
||||
mev_protection: bool,
|
||||
},
|
||||
Http {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
http_client: Client,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
/// When true, HTTP request adds revertProtection=true query param for MEV protection.
|
||||
mev_protection: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BlockRazorClient {
|
||||
pub endpoint: String,
|
||||
pub auth_token: String,
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
pub http_client: Client,
|
||||
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
pub stop_ping: Arc<AtomicBool>,
|
||||
backend: BlockRazorBackend,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for BlockRazorClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *BLOCKRAZOR_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *BLOCKRAZOR_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -47,68 +150,212 @@ impl SwqosClientTrait for BlockRazorClient {
|
||||
}
|
||||
|
||||
impl BlockRazorClient {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
|
||||
// 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR
|
||||
Ok(Self::new_http(rpc_url, endpoint, auth_token, false))
|
||||
}
|
||||
|
||||
pub async fn new_grpc(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
stop_ping: Arc::new(AtomicBool::new(false)),
|
||||
|
||||
// 配置 Channel,增加连接超时
|
||||
let channel = tonic::transport::Channel::from_shared(endpoint.clone())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))?
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
|
||||
|
||||
let grpc_client =
|
||||
Arc::new(ArcSwap::from_pointee(BlockRazorGrpcClient::new(channel, auth_token.clone())));
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
backend: BlockRazorBackend::Grpc {
|
||||
endpoint,
|
||||
auth_token,
|
||||
grpc_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
mev_protection,
|
||||
},
|
||||
};
|
||||
|
||||
// Start ping task
|
||||
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn new_http(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
mev_protection: bool,
|
||||
) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().user_agent("").build().unwrap();
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
backend: BlockRazorBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
mev_protection,
|
||||
},
|
||||
};
|
||||
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
/// Start periodic ping task to keep connections active
|
||||
async fn start_ping_task(&self) {
|
||||
let endpoint = self.endpoint.clone();
|
||||
let auth_token = self.auth_token.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor ping request failed: {}", e);
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc {
|
||||
grpc_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
endpoint,
|
||||
auth_token,
|
||||
..
|
||||
} => {
|
||||
let grpc_client = grpc_client.clone();
|
||||
let ping_handle = ping_handle.clone();
|
||||
let stop_ping = stop_ping.clone();
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut delay = 1u64;
|
||||
|
||||
// 初始健康检查
|
||||
{
|
||||
let client = grpc_client.load();
|
||||
if let Err(e) = client.get_health().await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC initial health check failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 健康检查(使用 load() 无锁读取)
|
||||
let client = grpc_client.load();
|
||||
match client.get_health().await {
|
||||
Ok(_) => {
|
||||
delay = 1; // 成功,重置延迟
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC health check failed: {} - reconnecting in {}s", e, delay);
|
||||
}
|
||||
|
||||
// 等待指数退避时间
|
||||
tokio::time::sleep(Duration::from_secs(delay)).await;
|
||||
delay = (delay * 2).min(60);
|
||||
|
||||
// 尝试重连
|
||||
match Self::reconnect_grpc(&endpoint, &auth_token).await {
|
||||
Ok(new_client) => {
|
||||
// 使用 swap() 无锁替换客户端
|
||||
grpc_client.swap(Arc::new(new_client));
|
||||
delay = 1; // 重置延迟
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC reconnected successfully");
|
||||
}
|
||||
}
|
||||
Err(reconnect_err) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
"BlockRazor gRPC reconnect failed: {}",
|
||||
reconnect_err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut ping_guard = ping_handle.lock().await;
|
||||
if let Some(old_handle) = ping_guard.as_ref() {
|
||||
old_handle.abort();
|
||||
}
|
||||
*ping_guard = Some(handle);
|
||||
}
|
||||
});
|
||||
|
||||
// Update ping_handle - use Mutex to safely update
|
||||
{
|
||||
let mut ping_guard = self.ping_handle.lock().await;
|
||||
if let Some(old_handle) = ping_guard.as_ref() {
|
||||
old_handle.abort();
|
||||
BlockRazorBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
..
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
let http_client = http_client.clone();
|
||||
let ping_handle = ping_handle.clone();
|
||||
let stop_ping = stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor HTTP ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) =
|
||||
Self::send_http_ping(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor HTTP ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut ping_guard = ping_handle.lock().await;
|
||||
if let Some(old_handle) = ping_guard.as_ref() {
|
||||
old_handle.abort();
|
||||
}
|
||||
*ping_guard = Some(handle);
|
||||
}
|
||||
*ping_guard = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send ping request: POST /v2/health?auth=... (Keep Alive). Only required param: auth.
|
||||
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||
async fn send_http_ping(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
|
||||
let response = http_client
|
||||
.post(&ping_url)
|
||||
@@ -121,78 +368,192 @@ impl BlockRazorClient {
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!("BlockRazor ping request failed with status: {}", status);
|
||||
eprintln!("BlockRazor HTTP ping request failed with status: {}", status);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send transaction via v2 API: plain Base64 body, Content-Type: text/plain. Only required URI param: auth.
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
/// 重新建立 gRPC 连接
|
||||
async fn reconnect_grpc(endpoint: &str, auth_token: &str) -> Result<BlockRazorGrpcClient> {
|
||||
let channel = tonic::transport::Channel::from_shared(endpoint.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))?
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to reconnect to gRPC endpoint: {}", e))?;
|
||||
|
||||
Ok(BlockRazorGrpcClient::new(channel, auth_token.to_string()))
|
||||
}
|
||||
|
||||
async fn send_transaction_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let response = self.http_client
|
||||
.post(&self.endpoint)
|
||||
.query(&[("auth", self.auth_token.as_str())])
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(content)
|
||||
.send()
|
||||
.await?;
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc { grpc_client, mev_protection, .. } => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if status.is_success() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
// 使用 load() 无锁获取客户端引用
|
||||
let client = grpc_client.load();
|
||||
let signature = client
|
||||
.send_transaction(
|
||||
content,
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revert_protection is unrelated to MEV; keep false.
|
||||
if *mev_protection {
|
||||
"sandwichMitigation".to_string()
|
||||
} else {
|
||||
"fast".to_string()
|
||||
},
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
match signature {
|
||||
Ok(sig) => {
|
||||
if !sig.is_empty() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"empty signature".to_string(),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor gRPC returned empty signature"
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("gRPC error: {}", e),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor gRPC sendTransaction failed: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [blockrazor] {} submission failed: status {}", trade_type, status);
|
||||
BlockRazorBackend::Http {
|
||||
endpoint, auth_token, http_client, mev_protection, ..
|
||||
} => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let query_params: Vec<(&str, &str)> = vec![
|
||||
("auth", auth_token.as_str()),
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revertProtection is unrelated to MEV; not set.
|
||||
("mode", if *mev_protection { "sandwichMitigation" } else { "fast" }),
|
||||
];
|
||||
|
||||
let response = http_client
|
||||
.post(endpoint)
|
||||
.query(&query_params)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(content)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let status = response.status();
|
||||
if status.is_success() {
|
||||
let _ = response.bytes().await;
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {} body: {}", status, body),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor HTTP sendTransaction failed: status {} body: {}",
|
||||
status,
|
||||
body
|
||||
));
|
||||
}
|
||||
}
|
||||
return Err(anyhow::anyhow!("BlockRazor sendTransaction failed: {}", status));
|
||||
}
|
||||
|
||||
let start_time = Instant::now();
|
||||
let signature = transaction.signatures[0];
|
||||
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [blockrazor] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [blockrazor] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BlockRazorClient {
|
||||
fn drop(&mut self) {
|
||||
// Ensure ping task stops when client is destroyed
|
||||
self.stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
// Try to stop ping task immediately
|
||||
// Use tokio::spawn to avoid blocking Drop
|
||||
let ping_handle = self.ping_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut ping_guard = ping_handle.lock().await;
|
||||
if let Some(handle) = ping_guard.as_ref() {
|
||||
handle.abort();
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. }
|
||||
| BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
let ping_handle = ping_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut ping_guard = ping_handle.lock().await;
|
||||
if let Some(handle) = ping_guard.as_ref() {
|
||||
handle.abort();
|
||||
}
|
||||
*ping_guard = None;
|
||||
});
|
||||
}
|
||||
*ping_guard = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+78
-20
@@ -6,17 +6,16 @@ use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use std::time::Duration;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::BLOX_TIP_ACCOUNTS};
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BloxrouteClient {
|
||||
pub endpoint: String,
|
||||
@@ -27,16 +26,29 @@ pub struct BloxrouteClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for BloxrouteClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *BLOX_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOX_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *BLOX_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| BLOX_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -56,9 +68,15 @@ impl BloxrouteClient {
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Single format! for body to avoid json! + to_string() double allocation
|
||||
let body = format!(
|
||||
@@ -67,7 +85,9 @@ impl BloxrouteClient {
|
||||
);
|
||||
|
||||
let endpoint = format!("{}/api/v2/submit", self.endpoint);
|
||||
let response_text = self.http_client.post(&endpoint)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&endpoint)
|
||||
.body(body)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", self.auth_token.as_str())
|
||||
@@ -80,13 +100,27 @@ impl BloxrouteClient {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, _error);
|
||||
eprintln!(
|
||||
" [bloxroute] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -95,20 +129,37 @@ impl BloxrouteClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [bloxroute] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [bloxroute] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
_wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let contents = serialization::serialize_transactions_batch_sync(
|
||||
@@ -123,7 +174,9 @@ impl BloxrouteClient {
|
||||
let body = format!(r#"{{"entries":[{}]}}"#, entries);
|
||||
|
||||
let endpoint = format!("{}/api/v2/submit-batch", self.endpoint);
|
||||
let response_text = self.http_client.post(&endpoint)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&endpoint)
|
||||
.body(body)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", self.auth_token.as_str())
|
||||
@@ -137,11 +190,16 @@ impl BloxrouteClient {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" bloxroute {} submission failed: {:?}", trade_type, _error);
|
||||
eprintln!(
|
||||
" bloxroute {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-11
@@ -1,9 +1,9 @@
|
||||
use crate::common::types::SolanaRpcClient;
|
||||
use crate::swqos::serialization;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::{self, STANDARD};
|
||||
use base64::Engine;
|
||||
use bincode::serialize;
|
||||
use crate::swqos::serialization;
|
||||
use reqwest::Client;
|
||||
use serde_json;
|
||||
use serde_json::json;
|
||||
@@ -117,7 +117,11 @@ pub async fn poll_any_transaction_confirmation(
|
||||
|
||||
loop {
|
||||
if start.elapsed() >= timeout {
|
||||
return Err(anyhow::anyhow!("Transaction confirmation timed out after {}s ({} signatures polled)", timeout.as_secs(), signatures.len()));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Transaction confirmation timed out after {}s ({} signatures polled)",
|
||||
timeout.as_secs(),
|
||||
signatures.len()
|
||||
));
|
||||
}
|
||||
|
||||
poll_count += 1;
|
||||
@@ -205,7 +209,7 @@ pub async fn poll_any_transaction_confirmation(
|
||||
let ui_err = meta.err.unwrap();
|
||||
let tx_err: TransactionError =
|
||||
serde_json::from_value(serde_json::to_value(&ui_err)?)?;
|
||||
|
||||
|
||||
// Use Solana InstructionError codes directly
|
||||
let mut code = 0u32;
|
||||
let mut index = None;
|
||||
@@ -230,7 +234,7 @@ pub async fn poll_any_transaction_confirmation(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
|
||||
return Err(anyhow::Error::new(TradeError {
|
||||
code: code,
|
||||
message: format!("{} {:?}", tx_err, error_msg),
|
||||
@@ -241,11 +245,16 @@ pub async fn poll_any_transaction_confirmation(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
|
||||
pub async fn send_nb_transaction(
|
||||
client: Client,
|
||||
endpoint: &str,
|
||||
auth_token: &str,
|
||||
transaction: &Transaction,
|
||||
) -> Result<Signature, anyhow::Error> {
|
||||
// Serialize transaction
|
||||
let serialized = bincode::serialize(transaction)
|
||||
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
|
||||
|
||||
|
||||
// Base64 encode
|
||||
let encoded = STANDARD.encode(serialized);
|
||||
|
||||
@@ -266,18 +275,21 @@ pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &st
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Request failed: {}", e))?;
|
||||
|
||||
let resp = response.json::<serde_json::Value>().await
|
||||
let resp = response
|
||||
.json::<serde_json::Value>()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Response parsing failed: {}", e))?;
|
||||
|
||||
if let Some(reason) = resp["reason"].as_str() {
|
||||
return Err(anyhow::anyhow!(reason.to_string()));
|
||||
}
|
||||
|
||||
let signature = resp["signature"].as_str()
|
||||
let signature = resp["signature"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing signature field in response"))?;
|
||||
|
||||
let signature = Signature::from_str(signature)
|
||||
.map_err(|e| anyhow::anyhow!("Invalid signature: {}", e))?;
|
||||
let signature =
|
||||
Signature::from_str(signature).map_err(|e| anyhow::anyhow!("Invalid signature: {}", e))?;
|
||||
|
||||
Ok(signature)
|
||||
}
|
||||
@@ -314,4 +326,4 @@ pub async fn serialize_smart_transaction_and_encode(
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
Ok((serialized, *signature))
|
||||
}
|
||||
}
|
||||
|
||||
+70
-17
@@ -1,4 +1,6 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
@@ -6,14 +8,13 @@ use std::{sync::Arc, time::Instant};
|
||||
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::FLASHBLOCK_TIP_ACCOUNTS};
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct FlashBlockClient {
|
||||
pub endpoint: String,
|
||||
@@ -24,16 +25,29 @@ pub struct FlashBlockClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for FlashBlockClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *FLASHBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| FLASHBLOCK_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *FLASHBLOCK_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| FLASHBLOCK_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -49,9 +63,15 @@ impl FlashBlockClient {
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// FlashBlock API format
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
@@ -61,7 +81,9 @@ impl FlashBlockClient {
|
||||
let url = format!("{}/api/v2/submit-batch", self.endpoint);
|
||||
|
||||
// Send request to FlashBlock
|
||||
let response_text = self.http_client.post(&url)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
.body(request_body)
|
||||
.header("Authorization", &self.auth_token)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -75,12 +97,26 @@ impl FlashBlockClient {
|
||||
// Parse response
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("success").is_some() || response_json.get("result").is_some() {
|
||||
println!(" [FlashBlock] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [FlashBlock] {} submission failed: {:?}", trade_type, _error);
|
||||
eprintln!(
|
||||
" [FlashBlock] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [FlashBlock] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -88,19 +124,36 @@ impl FlashBlockClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [FlashBlock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [FlashBlock] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
|
||||
+33
-26
@@ -20,7 +20,9 @@ use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::constants::swqos::{HELIUS_TIP_ACCOUNTS, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY};
|
||||
use crate::constants::swqos::{
|
||||
HELIUS_TIP_ACCOUNTS, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY,
|
||||
};
|
||||
use crate::swqos::{SwqosClientTrait, SwqosType, TradeType};
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -43,12 +45,7 @@ impl HeliusClient {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
let submit_url = Self::build_submit_url(&endpoint, api_key.as_deref(), swqos_only);
|
||||
Self {
|
||||
submit_url,
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
http_client,
|
||||
swqos_only,
|
||||
}
|
||||
Self { submit_url, rpc_client: Arc::new(rpc_client), http_client, swqos_only }
|
||||
}
|
||||
|
||||
/// Build URL once at construction; no per-request allocation.
|
||||
@@ -109,8 +106,11 @@ impl HeliusClient {
|
||||
if !status.is_success() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [helius] {} submission failed status={} body={}",
|
||||
trade_type, status, response_text
|
||||
" [helius] {} submission failed after {:?} status={} body={}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
status,
|
||||
response_text
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -127,21 +127,28 @@ impl HeliusClient {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [helius] {} submission error: {}", trade_type, err_msg);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
err_msg,
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("Helius Sender error: {}", err_msg));
|
||||
}
|
||||
if response_json.get("result").is_some() && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
" [helius] {} submitted: {:?}",
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [helius] {} submission failed: {:?}",
|
||||
trade_type, response_text
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,23 +157,24 @@ impl HeliusClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [helius] {} confirmation failed: {:?}",
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" signature: {:?}",
|
||||
signature
|
||||
);
|
||||
println!(
|
||||
" [helius] {} confirmed: {:?}",
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
@@ -191,8 +199,7 @@ impl SwqosClientTrait for HeliusClient {
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation)
|
||||
.await?;
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+75
-23
@@ -1,5 +1,7 @@
|
||||
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
FormatBase64VersionedTransaction,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
@@ -7,14 +9,13 @@ use std::{sync::Arc, time::Instant};
|
||||
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::JITO_TIP_ACCOUNTS};
|
||||
|
||||
|
||||
pub struct JitoClient {
|
||||
pub endpoint: String,
|
||||
pub auth_token: String,
|
||||
@@ -24,11 +25,21 @@ pub struct JitoClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for JitoClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions_impl(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
@@ -52,9 +63,15 @@ impl JitoClient {
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||
}
|
||||
|
||||
pub async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"id": 1,
|
||||
@@ -76,8 +93,7 @@ impl JitoClient {
|
||||
let response = if self.auth_token.is_empty() {
|
||||
self.http_client.post(&endpoint)
|
||||
} else {
|
||||
self.http_client.post(&endpoint)
|
||||
.header("x-jito-auth", &self.auth_token)
|
||||
self.http_client.post(&endpoint).header("x-jito-auth", &self.auth_token)
|
||||
};
|
||||
let response_text = response
|
||||
.body(request_body)
|
||||
@@ -89,12 +105,26 @@ impl JitoClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [jito] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [jito] {} submission failed: {:?}", trade_type, _error);
|
||||
eprintln!(
|
||||
" [jito] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [jito] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -102,21 +132,39 @@ impl JitoClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [jito] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [jito] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions_impl(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
_wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let txs_base64 = transactions.iter().map(|tx| tx.to_base64_string()).collect::<Vec<String>>();
|
||||
let txs_base64 =
|
||||
transactions.iter().map(|tx| tx.to_base64_string()).collect::<Vec<String>>();
|
||||
let body = serde_json::json!({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "sendBundle",
|
||||
@@ -135,8 +183,7 @@ impl JitoClient {
|
||||
let response = if self.auth_token.is_empty() {
|
||||
self.http_client.post(&endpoint)
|
||||
} else {
|
||||
self.http_client.post(&endpoint)
|
||||
.header("x-jito-auth", &self.auth_token)
|
||||
self.http_client.post(&endpoint).header("x-jito-auth", &self.auth_token)
|
||||
};
|
||||
let response_text = response
|
||||
.body(body.to_string())
|
||||
@@ -150,10 +197,15 @@ impl JitoClient {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" jito {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" jito {} submission failed: {:?}", trade_type, _error);
|
||||
eprintln!(
|
||||
" jito {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+70
-16
@@ -1,4 +1,6 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
@@ -6,10 +8,10 @@ use std::{sync::Arc, time::Instant};
|
||||
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::LIGHTSPEED_TIP_ACCOUNTS};
|
||||
|
||||
@@ -23,16 +25,29 @@ pub struct LightspeedClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for LightspeedClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *LIGHTSPEED_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| LIGHTSPEED_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *LIGHTSPEED_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| LIGHTSPEED_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -50,9 +65,15 @@ impl LightspeedClient {
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
@@ -70,7 +91,9 @@ impl LightspeedClient {
|
||||
]
|
||||
}))?;
|
||||
|
||||
let response_text = self.http_client.post(&self.endpoint)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&self.endpoint)
|
||||
.body(request_body)
|
||||
.header("Content-Type", "application/json")
|
||||
.send()
|
||||
@@ -80,12 +103,26 @@ impl LightspeedClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [lightspeed] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [lightspeed] {} submission failed: {:?}", trade_type, _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [lightspeed] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -93,19 +130,36 @@ impl LightspeedClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [lightspeed] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [lightspeed] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
|
||||
+274
-173
@@ -1,91 +1,90 @@
|
||||
pub mod astralane;
|
||||
pub mod astralane_quic;
|
||||
pub mod blockrazor;
|
||||
pub mod bloxroute;
|
||||
pub mod common;
|
||||
pub mod flashblock;
|
||||
pub mod helius;
|
||||
pub mod jito;
|
||||
pub mod lightspeed;
|
||||
pub mod nextblock;
|
||||
pub mod node1;
|
||||
pub mod node1_quic;
|
||||
pub mod serialization;
|
||||
pub mod solana_rpc;
|
||||
pub mod jito;
|
||||
pub mod nextblock;
|
||||
pub mod zeroslot;
|
||||
pub mod temporal;
|
||||
pub mod bloxroute;
|
||||
pub mod node1;
|
||||
pub mod flashblock;
|
||||
pub mod blockrazor;
|
||||
pub mod astralane;
|
||||
pub mod stellium;
|
||||
pub mod lightspeed;
|
||||
pub mod soyas;
|
||||
pub mod speedlanding;
|
||||
pub mod helius;
|
||||
pub mod stellium;
|
||||
pub mod temporal;
|
||||
pub mod zeroslot;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
constants::swqos::{
|
||||
SWQOS_ENDPOINTS_BLOX,
|
||||
SWQOS_ENDPOINTS_JITO,
|
||||
SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
SWQOS_ENDPOINTS_TEMPORAL,
|
||||
SWQOS_ENDPOINTS_ZERO_SLOT,
|
||||
SWQOS_ENDPOINTS_NODE1,
|
||||
SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
SWQOS_ENDPOINTS_ASTRALANE,
|
||||
SWQOS_ENDPOINTS_STELLIUM,
|
||||
SWQOS_ENDPOINTS_SOYAS,
|
||||
SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
SWQOS_ENDPOINTS_HELIUS,
|
||||
SWQOS_MIN_TIP_DEFAULT,
|
||||
SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_NEXTBLOCK,
|
||||
SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
SWQOS_MIN_TIP_TEMPORAL,
|
||||
SWQOS_MIN_TIP_BLOXROUTE,
|
||||
SWQOS_MIN_TIP_NODE1,
|
||||
SWQOS_MIN_TIP_FLASHBLOCK,
|
||||
SWQOS_MIN_TIP_BLOCKRAZOR,
|
||||
SWQOS_MIN_TIP_ASTRALANE,
|
||||
SWQOS_MIN_TIP_STELLIUM,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED,
|
||||
SWQOS_MIN_TIP_SOYAS,
|
||||
SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SWQOS_MIN_TIP_HELIUS,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX,
|
||||
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO,
|
||||
SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||
SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
|
||||
SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
|
||||
SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
|
||||
SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1,
|
||||
SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM,
|
||||
SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
},
|
||||
swqos::{
|
||||
bloxroute::BloxrouteClient,
|
||||
jito::JitoClient,
|
||||
nextblock::NextBlockClient,
|
||||
solana_rpc::SolRpcClient,
|
||||
temporal::TemporalClient,
|
||||
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
|
||||
flashblock::FlashBlockClient, helius::HeliusClient, jito::JitoClient,
|
||||
lightspeed::LightspeedClient, nextblock::NextBlockClient, node1::Node1Client,
|
||||
node1_quic::Node1QuicClient, solana_rpc::SolRpcClient, soyas::SoyasClient,
|
||||
speedlanding::SpeedlandingClient, stellium::StelliumClient, temporal::TemporalClient,
|
||||
zeroslot::ZeroSlotClient,
|
||||
node1::Node1Client,
|
||||
flashblock::FlashBlockClient,
|
||||
blockrazor::BlockRazorClient,
|
||||
astralane::AstralaneClient,
|
||||
stellium::StelliumClient,
|
||||
lightspeed::LightspeedClient,
|
||||
soyas::SoyasClient,
|
||||
speedlanding::SpeedlandingClient,
|
||||
helius::HeliusClient,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
|
||||
}
|
||||
// Tip 账户:`SwqosClient::get_tip_account()` 在各实现里多为静态常量;同一批多路提交时,
|
||||
// 在 `trading::core::async_executor::execute_parallel` 内用局部 `tip_cache`(按 client 指针)去重解析。
|
||||
|
||||
/// SWQOS provider blacklist configuration
|
||||
/// Providers added here will be disabled even if configured by user
|
||||
/// To enable a provider, remove it from this list
|
||||
pub const SWQOS_BLACKLIST: &[SwqosType] = &[
|
||||
SwqosType::NextBlock, // NextBlock is disabled by default
|
||||
SwqosType::NextBlock, // NextBlock is disabled by default
|
||||
];
|
||||
|
||||
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
|
||||
/// BlockRazor 支持 gRPC 和 HTTP。
|
||||
/// Node1 支持 QUIC。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum SwqosTransport {
|
||||
#[default]
|
||||
Http,
|
||||
Grpc,
|
||||
Quic,
|
||||
}
|
||||
|
||||
/// Astralane 三种提交方式:QUIC TPU、Plain HTTP(`/iris`)、Binary HTTP(`/irisb` + bincode)。
|
||||
/// 与全局 [`crate::common::TradeConfig::mev_protection`] 配合:HTTP 加 `mev-protect=true`;QUIC 选 `:9000` / `:7000`。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum AstralaneTransport {
|
||||
/// Binary over HTTP:`…/irisb?api-key=…&method=sendTransaction`(与 `AstralaneClient` 当前序列化一致)。
|
||||
#[default]
|
||||
Binary,
|
||||
/// Plain HTTP:`…/iris?…`(非 irisb 路径)。
|
||||
Plain,
|
||||
/// QUIC(`host:7000`;MEV 时 `host:9000`)。
|
||||
Quic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TradeType {
|
||||
Create,
|
||||
@@ -126,6 +125,28 @@ pub enum SwqosType {
|
||||
}
|
||||
|
||||
impl SwqosType {
|
||||
/// Label for log alignment; same as Debug output (e.g. "Soyas", "Speedlanding").
|
||||
#[inline]
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Jito => "Jito",
|
||||
Self::NextBlock => "NextBlock",
|
||||
Self::ZeroSlot => "ZeroSlot",
|
||||
Self::Temporal => "Temporal",
|
||||
Self::Bloxroute => "Bloxroute",
|
||||
Self::Node1 => "Node1",
|
||||
Self::FlashBlock => "FlashBlock",
|
||||
Self::BlockRazor => "BlockRazor",
|
||||
Self::Astralane => "Astralane",
|
||||
Self::Stellium => "Stellium",
|
||||
Self::Lightspeed => "Lightspeed",
|
||||
Self::Soyas => "Soyas",
|
||||
Self::Speedlanding => "Speedlanding",
|
||||
Self::Helius => "Helius",
|
||||
Self::Default => "Default",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn values() -> Vec<Self> {
|
||||
vec![
|
||||
Self::Jito,
|
||||
@@ -151,8 +172,18 @@ pub type SwqosClient = dyn SwqosClientTrait + Send + Sync + 'static;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait SwqosClientTrait {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()>;
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()>;
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()>;
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()>;
|
||||
fn get_tip_account(&self) -> Result<String>;
|
||||
fn get_swqos_type(&self) -> SwqosType;
|
||||
/// Minimum tip in SOL required by this provider. Helius returns lower value when swqos_only is true.
|
||||
@@ -178,15 +209,23 @@ pub trait SwqosClientTrait {
|
||||
}
|
||||
}
|
||||
|
||||
/// 地理区域,用于默认 SWQOS 端点下标(见 `constants::swqos`)。
|
||||
///
|
||||
/// 各服务商常量表在**缺独立 PoP**时,于**已公布的端点集合内**按地理距离选最近项;[`SwqosRegion::Default`] 不表示地球上的位置,表中为全局/枢纽回退,不适用地理就近。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SwqosRegion {
|
||||
NewYork,
|
||||
Frankfurt,
|
||||
Amsterdam,
|
||||
/// Ireland (EU); Jito publishes `dublin.mainnet.block-engine.jito.wtf`.
|
||||
Dublin,
|
||||
SLC,
|
||||
Tokyo,
|
||||
/// Southeast Asia (Singapore); not interchangeable with [`SwqosRegion::Tokyo`].
|
||||
Singapore,
|
||||
London,
|
||||
LosAngeles,
|
||||
/// 非地理区域:未指定区域时的回退,对应表中全局 URL 或枢纽,**不按地理距离选取**。
|
||||
Default,
|
||||
}
|
||||
|
||||
@@ -203,14 +242,14 @@ pub enum SwqosConfig {
|
||||
Temporal(String, SwqosRegion, Option<String>),
|
||||
/// ZeroSlot(api_token, region, custom_url)
|
||||
ZeroSlot(String, SwqosRegion, Option<String>),
|
||||
/// Node1(api_token, region, custom_url)
|
||||
Node1(String, SwqosRegion, Option<String>),
|
||||
/// Node1(api_token, region, custom_url, transport). transport=None => HTTP; Some(Quic) => QUIC (port 16666, UUID auth).
|
||||
Node1(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// FlashBlock(api_token, region, custom_url)
|
||||
FlashBlock(String, SwqosRegion, Option<String>),
|
||||
/// BlockRazor(api_token, region, custom_url)
|
||||
BlockRazor(String, SwqosRegion, Option<String>),
|
||||
/// Astralane(api_token, region, custom_url)
|
||||
Astralane(String, SwqosRegion, Option<String>),
|
||||
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
|
||||
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`](`/irisb`)。
|
||||
Astralane(String, SwqosRegion, Option<String>, Option<AstralaneTransport>),
|
||||
/// Stellium(api_token, region, custom_url)
|
||||
Stellium(String, SwqosRegion, Option<String>),
|
||||
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
|
||||
@@ -228,7 +267,7 @@ pub enum SwqosConfig {
|
||||
}
|
||||
|
||||
impl SwqosConfig {
|
||||
pub fn swqos_type(&self) -> SwqosType{
|
||||
pub fn swqos_type(&self) -> SwqosType {
|
||||
match self {
|
||||
SwqosConfig::Default(_) => SwqosType::Default,
|
||||
SwqosConfig::Jito(_, _, _) => SwqosType::Jito,
|
||||
@@ -236,10 +275,10 @@ impl SwqosConfig {
|
||||
SwqosConfig::Bloxroute(_, _, _) => SwqosType::Bloxroute,
|
||||
SwqosConfig::Temporal(_, _, _) => SwqosType::Temporal,
|
||||
SwqosConfig::ZeroSlot(_, _, _) => SwqosType::ZeroSlot,
|
||||
SwqosConfig::Node1(_, _, _) => SwqosType::Node1,
|
||||
SwqosConfig::Node1(_, _, _, _) => SwqosType::Node1,
|
||||
SwqosConfig::FlashBlock(_, _, _) => SwqosType::FlashBlock,
|
||||
SwqosConfig::BlockRazor(_, _, _) => SwqosType::BlockRazor,
|
||||
SwqosConfig::Astralane(_, _, _) => SwqosType::Astralane,
|
||||
SwqosConfig::BlockRazor(_, _, _, _) => SwqosType::BlockRazor,
|
||||
SwqosConfig::Astralane(_, _, _, _) => SwqosType::Astralane,
|
||||
SwqosConfig::Stellium(_, _, _) => SwqosType::Stellium,
|
||||
SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed,
|
||||
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
|
||||
@@ -267,7 +306,7 @@ impl SwqosConfig {
|
||||
SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(),
|
||||
SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(),
|
||||
SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(),
|
||||
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string(),
|
||||
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string(),
|
||||
SwqosType::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(),
|
||||
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
|
||||
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
||||
@@ -277,145 +316,207 @@ impl SwqosConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_swqos_client(rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig) -> Result<Arc<SwqosClient>> {
|
||||
pub fn get_endpoint_with_transport(
|
||||
swqos_type: SwqosType,
|
||||
region: SwqosRegion,
|
||||
url: Option<String>,
|
||||
transport: Option<SwqosTransport>,
|
||||
_mev_protection: bool,
|
||||
) -> String {
|
||||
if let Some(custom_url) = url {
|
||||
return custom_url;
|
||||
}
|
||||
|
||||
match swqos_type {
|
||||
SwqosType::BlockRazor => {
|
||||
// transport=None 或 transport=Grpc => gRPC; transport=Http => HTTP
|
||||
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
|
||||
if use_http {
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
SwqosType::Node1 => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
SWQOS_ENDPOINTS_NODE1_QUIC[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
_ => Self::get_endpoint(swqos_type, region, None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_swqos_client(
|
||||
rpc_url: String,
|
||||
commitment: CommitmentConfig,
|
||||
swqos_config: SwqosConfig,
|
||||
mev_protection: bool,
|
||||
) -> Result<Arc<SwqosClient>> {
|
||||
match swqos_config {
|
||||
SwqosConfig::Jito(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Jito, region, url);
|
||||
let jito_client = JitoClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token
|
||||
);
|
||||
let jito_client = JitoClient::new(rpc_url.clone(), endpoint, auth_token);
|
||||
Ok(Arc::new(jito_client))
|
||||
}
|
||||
SwqosConfig::NextBlock(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::NextBlock, region, url);
|
||||
let nextblock_client = NextBlockClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let nextblock_client =
|
||||
NextBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(nextblock_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::ZeroSlot(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::ZeroSlot, region, url);
|
||||
let zeroslot_client = ZeroSlotClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let zeroslot_client =
|
||||
ZeroSlotClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(zeroslot_client))
|
||||
},
|
||||
SwqosConfig::Temporal(auth_token, region, url) => {
|
||||
}
|
||||
SwqosConfig::Temporal(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Temporal, region, url);
|
||||
let temporal_client = TemporalClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let temporal_client =
|
||||
TemporalClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(temporal_client))
|
||||
},
|
||||
SwqosConfig::Bloxroute(auth_token, region, url) => {
|
||||
}
|
||||
SwqosConfig::Bloxroute(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Bloxroute, region, url);
|
||||
let bloxroute_client = BloxrouteClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let bloxroute_client =
|
||||
BloxrouteClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(bloxroute_client))
|
||||
},
|
||||
SwqosConfig::Node1(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Node1, region, url);
|
||||
let node1_client = Node1Client::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
Ok(Arc::new(node1_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::Node1(auth_token, region, url, transport) => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
let quic_endpoint = url
|
||||
.unwrap_or_else(|| SWQOS_ENDPOINTS_NODE1_QUIC[region as usize].to_string());
|
||||
let node1_quic =
|
||||
Node1QuicClient::connect(&quic_endpoint, &auth_token, rpc_url.clone())
|
||||
.await?;
|
||||
Ok(Arc::new(node1_quic))
|
||||
} else {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Node1, region, url);
|
||||
let node1_client =
|
||||
Node1Client::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(node1_client))
|
||||
}
|
||||
}
|
||||
SwqosConfig::FlashBlock(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::FlashBlock, region, url);
|
||||
let flashblock_client = FlashBlockClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let flashblock_client =
|
||||
FlashBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(flashblock_client))
|
||||
},
|
||||
SwqosConfig::BlockRazor(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url);
|
||||
let blockrazor_client = BlockRazorClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
}
|
||||
SwqosConfig::BlockRazor(auth_token, region, url, transport) => {
|
||||
// BlockRazor: transport=None 或 transport=Grpc 时使用 gRPC,transport=Http 时使用 HTTP
|
||||
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::BlockRazor,
|
||||
region,
|
||||
url,
|
||||
transport,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
},
|
||||
SwqosConfig::Astralane(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
},
|
||||
if use_http {
|
||||
let blockrazor_client = BlockRazorClient::new_http(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
} else {
|
||||
// 使用 gRPC 模式(默认或用户明确指定了 gRPC)
|
||||
let blockrazor_client = BlockRazorClient::new_grpc(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token,
|
||||
mev_protection,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
}
|
||||
}
|
||||
SwqosConfig::Astralane(auth_token, region, url, mode) => {
|
||||
let mode = mode.unwrap_or_default();
|
||||
match mode {
|
||||
AstralaneTransport::Quic => {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
if mev_protection {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
}
|
||||
});
|
||||
let astralane_client =
|
||||
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
||||
.await?;
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
AstralaneTransport::Plain => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_PLAIN[region as usize].to_string()
|
||||
});
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
AstralaneTransport::Binary => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string()
|
||||
});
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
}
|
||||
}
|
||||
SwqosConfig::Stellium(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Stellium, region, url);
|
||||
let stellium_client = StelliumClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let stellium_client =
|
||||
StelliumClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(stellium_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::Lightspeed(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Lightspeed, region, url);
|
||||
let lightspeed_client = LightspeedClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
);
|
||||
let lightspeed_client =
|
||||
LightspeedClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(lightspeed_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::Soyas(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Soyas, region, url);
|
||||
let soyas_client = SoyasClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
).await?;
|
||||
let soyas_client =
|
||||
SoyasClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
|
||||
Ok(Arc::new(soyas_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::Speedlanding(auth_token, region, url) => {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Speedlanding, region, url);
|
||||
let speedlanding_client = SpeedlandingClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token
|
||||
).await?;
|
||||
let speedlanding_client =
|
||||
SpeedlandingClient::new(rpc_url.clone(), endpoint.to_string(), auth_token)
|
||||
.await?;
|
||||
Ok(Arc::new(speedlanding_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::Helius(api_key, region, url, swqos_only) => {
|
||||
let swqos_only = swqos_only.unwrap_or(false);
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Helius, region, url.clone());
|
||||
let api_key_opt = if api_key.is_empty() { None } else { Some(api_key.clone()) };
|
||||
let helius_client = HeliusClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
api_key_opt,
|
||||
swqos_only,
|
||||
);
|
||||
let helius_client =
|
||||
HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only);
|
||||
Ok(Arc::new(helius_client))
|
||||
},
|
||||
}
|
||||
SwqosConfig::Default(endpoint) => {
|
||||
let rpc = SolanaRpcClient::new_with_commitment(
|
||||
endpoint,
|
||||
commitment
|
||||
);
|
||||
let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment);
|
||||
let rpc_client = SolRpcClient::new(Arc::new(rpc));
|
||||
Ok(Arc::new(rpc_client))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
-17
@@ -1,4 +1,6 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
@@ -6,10 +8,10 @@ use std::{sync::Arc, time::Instant};
|
||||
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::NEXTBLOCK_TIP_ACCOUNTS};
|
||||
|
||||
@@ -23,16 +25,29 @@ pub struct NextBlockClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for NextBlockClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -54,9 +69,15 @@ impl NextBlockClient {
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"transaction": {
|
||||
@@ -65,7 +86,9 @@ impl NextBlockClient {
|
||||
"frontRunningProtection": false
|
||||
}))?;
|
||||
|
||||
let response_text = self.http_client.post(&self.endpoint)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&self.endpoint)
|
||||
.body(request_body)
|
||||
.header("Authorization", &self.auth_token)
|
||||
.header("Content-Type", "application/json")
|
||||
@@ -76,12 +99,26 @@ impl NextBlockClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [nextblock] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [nextblock] {} submission failed: {:?}", trade_type, _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [nextblock] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -89,22 +126,39 @@ impl NextBlockClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [nextblock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [nextblock] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+91
-34
@@ -1,21 +1,23 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use std::time::Duration;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::NODE1_TIP_ACCOUNTS};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Node1Client {
|
||||
@@ -29,16 +31,29 @@ pub struct Node1Client {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for Node1Client {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *NODE1_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NODE1_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *NODE1_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| NODE1_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -51,22 +66,22 @@ impl Node1Client {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
stop_ping: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
|
||||
// Start ping task
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
@@ -76,7 +91,7 @@ impl Node1Client {
|
||||
let auth_token = self.auth_token.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
@@ -90,14 +105,15 @@ impl Node1Client {
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("Node1 ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update ping_handle - use Mutex to safely update
|
||||
{
|
||||
let mut ping_guard = self.ping_handle.lock().await;
|
||||
@@ -109,7 +125,11 @@ impl Node1Client {
|
||||
}
|
||||
|
||||
/// Send ping request to /ping endpoint
|
||||
async fn send_ping_request(http_client: &Client, endpoint: &str, _auth_token: &str) -> Result<()> {
|
||||
async fn send_ping_request(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
_auth_token: &str,
|
||||
) -> Result<()> {
|
||||
// Build ping URL
|
||||
let ping_url = if endpoint.ends_with('/') {
|
||||
format!("{}ping", endpoint)
|
||||
@@ -118,10 +138,8 @@ impl Node1Client {
|
||||
};
|
||||
|
||||
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||
let response = http_client.get(&ping_url)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
let response =
|
||||
http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await?;
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
|
||||
@@ -130,9 +148,15 @@ impl Node1Client {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"jsonrpc": "2.0",
|
||||
@@ -145,7 +169,9 @@ impl Node1Client {
|
||||
}))?;
|
||||
|
||||
// Node1 uses api-key header instead of URL parameter
|
||||
let response_text = self.http_client.post(&self.endpoint)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&self.endpoint)
|
||||
.body(request_body)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("api-key", &self.auth_token)
|
||||
@@ -158,13 +184,27 @@ impl Node1Client {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [node1] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [node1] {} submission failed: {:?}", trade_type, _error);
|
||||
eprintln!(
|
||||
" [node1] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [node1] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -173,20 +213,37 @@ impl Node1Client {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [node1] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [node1] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
@@ -198,7 +255,7 @@ impl Drop for Node1Client {
|
||||
fn drop(&mut self) {
|
||||
// Ensure ping task stops when client is destroyed
|
||||
self.stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
|
||||
// Try to stop ping task immediately
|
||||
// Use tokio::spawn to avoid blocking Drop
|
||||
let ping_handle = self.ping_handle.clone();
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
//! Node1 QUIC SWQOS client.
|
||||
//!
|
||||
//! Protocol: first bi stream = auth (16-byte UUID); each transaction uses a new bi stream.
|
||||
//! Request body = bincode(VersionedTransaction); response = 2 bytes status (BE) + 4 bytes msg_len (BE) + msg.
|
||||
//! Reuses a single authenticated connection; reconnects and re-auth when connection is closed.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use quinn::crypto::rustls::QuicClientConfig;
|
||||
use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, RecvStream, TransportConfig};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::constants::swqos::NODE1_TIP_ACCOUNTS;
|
||||
use crate::swqos::common::poll_transaction_confirmation;
|
||||
use crate::swqos::{SwqosClientTrait, SwqosType, TradeType};
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use std::time::Instant;
|
||||
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const AUTH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(15);
|
||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const MAX_TX_SIZE: usize = 1232;
|
||||
|
||||
/// Node1 QUIC client: one authenticated connection, reuse for all transactions.
|
||||
pub struct Node1QuicClient {
|
||||
endpoint: Endpoint,
|
||||
connection: Mutex<Connection>,
|
||||
server_addr: String,
|
||||
server_name: String,
|
||||
api_key_uuid: [u8; 16],
|
||||
rpc_client: Arc<SolanaRpcClient>,
|
||||
}
|
||||
|
||||
impl Node1QuicClient {
|
||||
/// Connect and authenticate. Reuse the returned client for all subsequent sends.
|
||||
pub async fn connect(server_addr: &str, api_key: &str, rpc_url: String) -> Result<Self> {
|
||||
let socket_addr = server_addr
|
||||
.to_socket_addrs()
|
||||
.context("resolve Node1 QUIC server address")?
|
||||
.next()
|
||||
.context("no socket address for Node1 QUIC")?;
|
||||
|
||||
let api_key_uuid =
|
||||
Uuid::parse_str(api_key).context("Node1 API key must be a valid UUID")?;
|
||||
let api_key_bytes: [u8; 16] = *api_key_uuid.as_bytes();
|
||||
|
||||
let server_name = server_addr.split(':').next().unwrap_or(server_addr);
|
||||
|
||||
let client_config = Self::build_client_config()?;
|
||||
let mut endpoint =
|
||||
Endpoint::client("0.0.0.0:0".parse()?).context("create QUIC endpoint")?;
|
||||
endpoint.set_default_client_config(client_config);
|
||||
|
||||
let connecting =
|
||||
endpoint.connect(socket_addr, server_name).context("Node1 QUIC connect failed")?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Node1 QUIC connect timeout")?
|
||||
.context("Node1 QUIC handshake failed")?;
|
||||
|
||||
timeout(AUTH_TIMEOUT, Self::authenticate(&connection, &api_key_bytes))
|
||||
.await
|
||||
.context("Node1 QUIC auth timeout")??;
|
||||
|
||||
Ok(Self {
|
||||
endpoint,
|
||||
connection: Mutex::new(connection),
|
||||
server_addr: server_addr.to_string(),
|
||||
server_name: server_name.to_string(),
|
||||
api_key_uuid: api_key_bytes,
|
||||
rpc_client: Arc::new(SolanaRpcClient::new(rpc_url)),
|
||||
})
|
||||
}
|
||||
|
||||
fn build_client_config() -> Result<ClientConfig> {
|
||||
let crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||
let client_crypto = QuicClientConfig::try_from(crypto).context("build QUIC TLS config")?;
|
||||
let mut client_config = ClientConfig::new(Arc::new(client_crypto));
|
||||
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT).unwrap()));
|
||||
transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
|
||||
client_config.transport_config(Arc::new(transport));
|
||||
|
||||
Ok(client_config)
|
||||
}
|
||||
|
||||
async fn authenticate(connection: &Connection, api_key_bytes: &[u8; 16]) -> Result<()> {
|
||||
let (mut send, mut recv) = connection.open_bi().await.context("open_bi for auth")?;
|
||||
send.write_all(api_key_bytes).await.context("write auth bytes")?;
|
||||
send.finish().context("finish auth stream")?;
|
||||
|
||||
let mut reply = [0u8; 1];
|
||||
recv.read_exact(&mut reply).await.context("read auth reply")?;
|
||||
match reply[0] {
|
||||
0 => Ok(()),
|
||||
code => anyhow::bail!("Node1 QUIC auth rejected, reply={}", code),
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_connected(&self) -> Result<Connection> {
|
||||
let guard = self.connection.lock().await;
|
||||
if let Some(_reason) = guard.close_reason() {
|
||||
drop(guard);
|
||||
let socket_addr = self
|
||||
.server_addr
|
||||
.to_socket_addrs()
|
||||
.context("resolve Node1 QUIC server address")?
|
||||
.next()
|
||||
.context("no socket address")?;
|
||||
let connecting = self
|
||||
.endpoint
|
||||
.connect(socket_addr, &self.server_name)
|
||||
.context("Node1 QUIC reconnect failed")?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Node1 QUIC reconnect timeout")?
|
||||
.context("Node1 QUIC re-handshake failed")?;
|
||||
|
||||
timeout(AUTH_TIMEOUT, Self::authenticate(&connection, &self.api_key_uuid))
|
||||
.await
|
||||
.context("Node1 QUIC re-auth timeout")??;
|
||||
|
||||
let mut g = self.connection.lock().await;
|
||||
*g = connection.clone();
|
||||
Ok(connection)
|
||||
} else {
|
||||
Ok(guard.clone())
|
||||
}
|
||||
}
|
||||
|
||||
async fn read_response(recv: &mut RecvStream) -> Result<(u16, String)> {
|
||||
let mut header = [0u8; 6];
|
||||
recv.read_exact(&mut header)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("read response header: {:?}", e))?;
|
||||
let status = u16::from_be_bytes(header[0..2].try_into().unwrap());
|
||||
let msg_len = u32::from_be_bytes(header[2..6].try_into().unwrap()) as usize;
|
||||
let mut msg = vec![0u8; msg_len];
|
||||
if msg_len > 0 {
|
||||
recv.read_exact(&mut msg)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("read response body: {:?}", e))?;
|
||||
}
|
||||
Ok((status, String::from_utf8_lossy(&msg).into_owned()))
|
||||
}
|
||||
|
||||
/// Send one transaction over QUIC (opens new bi stream, writes bincode tx, reads status+msg).
|
||||
pub async fn send_transaction_bytes(&self, tx_bytes: &[u8]) -> Result<(u16, String)> {
|
||||
if tx_bytes.len() > MAX_TX_SIZE {
|
||||
anyhow::bail!(
|
||||
"Node1 QUIC: transaction too large ({} > {})",
|
||||
tx_bytes.len(),
|
||||
MAX_TX_SIZE
|
||||
);
|
||||
}
|
||||
|
||||
let conn = self.ensure_connected().await?;
|
||||
let (mut send, mut recv) = conn.open_bi().await.context("open_bi for tx")?;
|
||||
send.write_all(tx_bytes).await.context("write tx")?;
|
||||
send.finish().context("finish tx stream")?;
|
||||
Self::read_response(&mut recv).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for Node1QuicClient {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let signature = transaction.signatures.first().copied().unwrap_or_default();
|
||||
let tx_bytes = bincode::serialize(transaction).context("Node1 QUIC: bincode serialize")?;
|
||||
|
||||
let (status, msg) = timeout(SEND_TIMEOUT, self.send_transaction_bytes(&tx_bytes))
|
||||
.await
|
||||
.context("Node1 QUIC send timeout")??;
|
||||
|
||||
if status != 200 {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [node1-quic] {} submit failed: status={} msg={}",
|
||||
trade_type, status, msg
|
||||
);
|
||||
}
|
||||
anyhow::bail!("Node1 QUIC submit failed: status={} msg={}", status, msg);
|
||||
}
|
||||
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" [node1-quic] {} submitted: {:?}", trade_type, start.elapsed());
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => {
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" [node1-quic] {} confirmed: {:?}", trade_type, start.elapsed());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [node1-quic] {} confirmation failed: {:?}",
|
||||
trade_type,
|
||||
start.elapsed()
|
||||
);
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for tx in transactions {
|
||||
self.send_transaction(trade_type, tx, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip = *NODE1_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| NODE1_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip.to_string())
|
||||
}
|
||||
|
||||
fn get_swqos_type(&self) -> SwqosType {
|
||||
SwqosType::Node1
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Node1QuicClient {
|
||||
fn drop(&mut self) {
|
||||
self.connection.get_mut().close(0u32.into(), b"client closing");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_: &rustls::pki_types::CertificateDer<'_>,
|
||||
_: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_: &rustls::pki_types::ServerName<'_>,
|
||||
_: &[u8],
|
||||
_: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: &rustls::pki_types::CertificateDer<'_>,
|
||||
_: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_: &[u8],
|
||||
_: &rustls::pki_types::CertificateDer<'_>,
|
||||
_: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA256,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA384,
|
||||
rustls::SignatureScheme::RSA_PSS_SHA512,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA384,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA512,
|
||||
rustls::SignatureScheme::ED25519,
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# Proto 生成的代码说明
|
||||
|
||||
## 概述
|
||||
|
||||
这个目录包含了从 `.proto` 文件预生成的 Rust 代码。
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `serverpb.rs` - 从 `blockrazor.proto` 生成的 gRPC 代码
|
||||
- 消息类型: `SendRequest`, `SendResponse`, `HealthRequest`, `HealthResponse`
|
||||
- gRPC 客户端: `server_client::ServerClient`
|
||||
- gRPC 服务端: `server_server::Server`
|
||||
|
||||
## 用户使用
|
||||
|
||||
用户**不需要**安装 `protoc` 或编译 proto 文件。这些代码已经预生成好了,可以直接使用。
|
||||
|
||||
在 `blockrazor.rs` 中使用:
|
||||
```rust
|
||||
pub mod serverpb {
|
||||
include!("pb/serverpb.rs");
|
||||
}
|
||||
```
|
||||
|
||||
## 开发者如何重新生成代码
|
||||
|
||||
如果你修改了 `.proto` 文件并需要重新生成代码:
|
||||
|
||||
```bash
|
||||
cd sol-trade-sdk/proto/gen
|
||||
cargo run
|
||||
```
|
||||
|
||||
这会在 `src/swqos/pb/serverpb.rs` 生成新的代码。
|
||||
|
||||
## 技术细节
|
||||
|
||||
生成工具使用 `tonic-prost-build` crate:
|
||||
- 输出目录: `src/swqos/pb`
|
||||
- Proto 文件: `proto/blockrazor.proto`
|
||||
- 生成工具: `proto/gen/`
|
||||
- 包含完整的客户端和服务端代码
|
||||
@@ -0,0 +1,383 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct SendRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub transaction: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub mode: ::prost::alloc::string::String,
|
||||
/// only take effect in sandwichMitigation mode
|
||||
#[prost(int32, optional, tag = "3")]
|
||||
pub safe_window: ::core::option::Option<i32>,
|
||||
#[prost(bool, tag = "4")]
|
||||
pub revert_protection: bool,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct SendResponse {
|
||||
#[prost(string, tag = "1")]
|
||||
pub signature: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct HealthRequest {}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct HealthResponse {
|
||||
#[prost(string, tag = "1")]
|
||||
pub status: ::prost::alloc::string::String,
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod server_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
use tonic::codegen::http::Uri;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServerClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl ServerClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> ServerClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> ServerClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
ServerClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
pub async fn send_transaction(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::SendRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::SendResponse>, tonic::Status> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/serverpb.Server/SendTransaction",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("serverpb.Server", "SendTransaction"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_health(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::HealthRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/serverpb.Server/GetHealth",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut().insert(GrpcMethod::new("serverpb.Server", "GetHealth"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod server_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with ServerServer.
|
||||
#[async_trait]
|
||||
pub trait Server: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
request: tonic::Request<super::SendRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::SendResponse>, tonic::Status>;
|
||||
async fn get_health(
|
||||
&self,
|
||||
request: tonic::Request<super::HealthRequest>,
|
||||
) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct ServerServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> ServerServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: Default::default(),
|
||||
send_compression_encodings: Default::default(),
|
||||
max_decoding_message_size: None,
|
||||
max_encoding_message_size: None,
|
||||
}
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> InterceptedService<Self, F>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
{
|
||||
InterceptedService::new(Self::new(inner), interceptor)
|
||||
}
|
||||
/// Enable decompressing requests with the given encoding.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.accept_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Compress responses with the given encoding, if the client supports it.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.send_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_decoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_encoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for ServerServer<T>
|
||||
where
|
||||
T: Server,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::BoxBody>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/serverpb.Server/SendTransaction" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct SendTransactionSvc<T: Server>(pub Arc<T>);
|
||||
impl<T: Server> tonic::server::UnaryService<super::SendRequest>
|
||||
for SendTransactionSvc<T> {
|
||||
type Response = super::SendResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::SendRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Server>::send_transaction(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = SendTransactionSvc(inner);
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/serverpb.Server/GetHealth" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetHealthSvc<T: Server>(pub Arc<T>);
|
||||
impl<T: Server> tonic::server::UnaryService<super::HealthRequest>
|
||||
for GetHealthSvc<T> {
|
||||
type Response = super::HealthResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::HealthRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as Server>::get_health(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetHealthSvc(inner);
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
_ => {
|
||||
Box::pin(async move {
|
||||
let mut response = http::Response::new(
|
||||
tonic::body::BoxBody::default(),
|
||||
);
|
||||
let headers = response.headers_mut();
|
||||
headers
|
||||
.insert(
|
||||
tonic::Status::GRPC_STATUS,
|
||||
(tonic::Code::Unimplemented as i32).into(),
|
||||
);
|
||||
headers
|
||||
.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||
);
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for ServerServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: self.accept_compression_encodings,
|
||||
send_compression_encodings: self.send_compression_encodings,
|
||||
max_decoding_message_size: self.max_decoding_message_size,
|
||||
max_encoding_message_size: self.max_encoding_message_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "serverpb.Server";
|
||||
impl<T> tonic::server::NamedService for ServerServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
+144
-33
@@ -1,18 +1,24 @@
|
||||
//! Transaction serialization module.
|
||||
|
||||
use crate::perf::{
|
||||
compiler_optimization::CompileTimeOptimizedEventProcessor, simd::SIMDSerializer,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD;
|
||||
use base64::Engine;
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
use solana_sdk::signature::Signature;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::sync::Arc;
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use crate::perf::{
|
||||
simd::SIMDSerializer,
|
||||
compiler_optimization::CompileTimeOptimizedEventProcessor,
|
||||
};
|
||||
|
||||
/// Max number of reusable buffers kept in the queue.
|
||||
const SERIALIZER_POOL_SIZE: usize = 10_000;
|
||||
/// Per-buffer reserved capacity (bytes).
|
||||
const SERIALIZER_BUFFER_SIZE: usize = 256 * 1024;
|
||||
/// Cold-start prewarm count. Keep small to avoid first-submit spikes.
|
||||
const SERIALIZER_PREWARM_BUFFERS: usize = 64;
|
||||
|
||||
/// Zero-allocation serializer using a buffer pool to avoid runtime allocation.
|
||||
pub struct ZeroAllocSerializer {
|
||||
@@ -22,28 +28,30 @@ pub struct ZeroAllocSerializer {
|
||||
|
||||
impl ZeroAllocSerializer {
|
||||
pub fn new(pool_size: usize, buffer_size: usize) -> Self {
|
||||
let pool = ArrayQueue::new(pool_size);
|
||||
|
||||
// Pre-allocate buffers
|
||||
for _ in 0..pool_size {
|
||||
let mut buffer = Vec::with_capacity(buffer_size);
|
||||
buffer.resize(buffer_size, 0);
|
||||
let _ = pool.push(buffer);
|
||||
}
|
||||
|
||||
Self {
|
||||
buffer_pool: Arc::new(pool),
|
||||
buffer_size,
|
||||
}
|
||||
Self::new_with_prewarm(pool_size, buffer_size, SERIALIZER_PREWARM_BUFFERS)
|
||||
}
|
||||
|
||||
pub fn serialize_zero_alloc<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> {
|
||||
fn new_with_prewarm(pool_size: usize, buffer_size: usize, prewarm_buffers: usize) -> Self {
|
||||
let pool = ArrayQueue::new(pool_size);
|
||||
let prewarm_count = prewarm_buffers.min(pool_size);
|
||||
|
||||
// Prewarm only a small hot set to avoid large cold-start blocking.
|
||||
// Remaining buffers are allocated lazily and returned to this pool.
|
||||
for _ in 0..prewarm_count {
|
||||
let _ = pool.push(Vec::with_capacity(buffer_size));
|
||||
}
|
||||
|
||||
Self { buffer_pool: Arc::new(pool), buffer_size }
|
||||
}
|
||||
|
||||
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 buf = Vec::with_capacity(self.buffer_size);
|
||||
buf.resize(self.buffer_size, 0);
|
||||
buf
|
||||
});
|
||||
let mut buffer =
|
||||
self.buffer_pool.pop().unwrap_or_else(|| Vec::with_capacity(self.buffer_size));
|
||||
|
||||
// Serialize into buffer
|
||||
let serialized = bincode::serialize(data)?;
|
||||
@@ -67,12 +75,8 @@ impl ZeroAllocSerializer {
|
||||
}
|
||||
|
||||
/// Global serializer instance.
|
||||
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
|
||||
Arc::new(ZeroAllocSerializer::new(
|
||||
10_000, // Pool size
|
||||
256 * 1024, // Buffer size: 256KB
|
||||
))
|
||||
});
|
||||
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> =
|
||||
Lazy::new(|| Arc::new(ZeroAllocSerializer::new(SERIALIZER_POOL_SIZE, SERIALIZER_BUFFER_SIZE)));
|
||||
|
||||
/// Compile-time optimized event processor (zero runtime cost).
|
||||
static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor =
|
||||
@@ -101,7 +105,9 @@ impl Base64Encoder {
|
||||
event_type: &str,
|
||||
) -> Result<String> {
|
||||
let serialized = SERIALIZER.serialize_zero_alloc(value, event_type)?;
|
||||
Ok(STANDARD.encode(&serialized))
|
||||
let encoded = STANDARD.encode(&serialized);
|
||||
SERIALIZER.return_buffer(serialized);
|
||||
Ok(encoded)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +233,7 @@ pub fn get_serializer_stats() -> (usize, usize) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn test_base64_encode() {
|
||||
@@ -243,6 +250,110 @@ mod tests {
|
||||
fn test_serializer_stats() {
|
||||
let (available, capacity) = get_serializer_stats();
|
||||
assert!(available <= capacity);
|
||||
assert_eq!(capacity, 10_000);
|
||||
assert_eq!(capacity, SERIALIZER_POOL_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serializer_prewarm_is_bounded() {
|
||||
let serializer = ZeroAllocSerializer::new_with_prewarm(128, 1024, 8);
|
||||
let (available, capacity) = serializer.get_pool_stats();
|
||||
assert_eq!(capacity, 128);
|
||||
assert_eq!(available, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serializer_lazy_alloc_and_return() {
|
||||
let serializer = ZeroAllocSerializer::new_with_prewarm(8, 1024, 0);
|
||||
let (available_before, capacity) = serializer.get_pool_stats();
|
||||
assert_eq!(capacity, 8);
|
||||
assert_eq!(available_before, 0);
|
||||
|
||||
let buf = serializer.serialize_zero_alloc(&"hello", "test").unwrap();
|
||||
assert!(buf.capacity() >= 1024);
|
||||
serializer.return_buffer(buf);
|
||||
|
||||
let (available_after, _) = serializer.get_pool_stats();
|
||||
assert_eq!(available_after, 1);
|
||||
}
|
||||
|
||||
fn legacy_eager_zero_fill_serializer(
|
||||
pool_size: usize,
|
||||
buffer_size: usize,
|
||||
) -> ZeroAllocSerializer {
|
||||
let pool = ArrayQueue::new(pool_size);
|
||||
for _ in 0..pool_size {
|
||||
let mut buffer = Vec::with_capacity(buffer_size);
|
||||
buffer.resize(buffer_size, 0);
|
||||
let _ = pool.push(buffer);
|
||||
}
|
||||
ZeroAllocSerializer { buffer_pool: Arc::new(pool), buffer_size }
|
||||
}
|
||||
|
||||
/// Manual perf test: compares old eager cold-start behavior to current bounded prewarm.
|
||||
/// Run with:
|
||||
/// cargo test --release perf_serializer_cold_start_vs_legacy_eager -- --ignored --nocapture
|
||||
#[test]
|
||||
#[ignore = "manual perf benchmark"]
|
||||
fn perf_serializer_cold_start_vs_legacy_eager() {
|
||||
const POOL_SIZE: usize = 4096;
|
||||
const BUFFER_SIZE: usize = 32 * 1024;
|
||||
const PREWARM: usize = 64;
|
||||
let payload = vec![7u8; 4096];
|
||||
|
||||
let legacy_init_start = Instant::now();
|
||||
let legacy = legacy_eager_zero_fill_serializer(POOL_SIZE, BUFFER_SIZE);
|
||||
let legacy_init = legacy_init_start.elapsed();
|
||||
|
||||
let current_init_start = Instant::now();
|
||||
let current = ZeroAllocSerializer::new_with_prewarm(POOL_SIZE, BUFFER_SIZE, PREWARM);
|
||||
let current_init = current_init_start.elapsed();
|
||||
|
||||
let legacy_first_start = Instant::now();
|
||||
let legacy_buf = legacy.serialize_zero_alloc(&payload, "perf").unwrap();
|
||||
let legacy_first = legacy_first_start.elapsed();
|
||||
legacy.return_buffer(legacy_buf);
|
||||
|
||||
let current_first_start = Instant::now();
|
||||
let current_buf = current.serialize_zero_alloc(&payload, "perf").unwrap();
|
||||
let current_first = current_first_start.elapsed();
|
||||
current.return_buffer(current_buf);
|
||||
|
||||
println!(
|
||||
"[perf] serializer cold-start compare\n pool_size={POOL_SIZE} buffer_size={BUFFER_SIZE} prewarm={PREWARM}\n legacy_init={legacy_init:?} current_init={current_init:?}\n legacy_first_serialize={legacy_first:?} current_first_serialize={current_first:?}"
|
||||
);
|
||||
|
||||
assert!(
|
||||
current_init <= legacy_init,
|
||||
"expected bounded prewarm init ({current_init:?}) to be <= legacy eager init ({legacy_init:?})"
|
||||
);
|
||||
}
|
||||
|
||||
/// Manual perf test: demonstrates lazy allocation amortization.
|
||||
/// Run with:
|
||||
/// cargo test --release perf_serializer_lazy_growth_amortization -- --ignored --nocapture
|
||||
#[test]
|
||||
#[ignore = "manual perf benchmark"]
|
||||
fn perf_serializer_lazy_growth_amortization() {
|
||||
const POOL_SIZE: usize = 128;
|
||||
const BUFFER_SIZE: usize = 256 * 1024;
|
||||
let serializer = ZeroAllocSerializer::new_with_prewarm(POOL_SIZE, BUFFER_SIZE, 0);
|
||||
let payload = vec![1u8; 8 * 1024];
|
||||
|
||||
let first_start = Instant::now();
|
||||
let first_buf = serializer.serialize_zero_alloc(&payload, "perf").unwrap();
|
||||
let first = first_start.elapsed();
|
||||
serializer.return_buffer(first_buf);
|
||||
|
||||
let second_start = Instant::now();
|
||||
let second_buf = serializer.serialize_zero_alloc(&payload, "perf").unwrap();
|
||||
let second = second_start.elapsed();
|
||||
serializer.return_buffer(second_buf);
|
||||
|
||||
let (available, capacity) = serializer.get_pool_stats();
|
||||
println!(
|
||||
"[perf] serializer lazy growth\n pool_size={POOL_SIZE} buffer_size={BUFFER_SIZE}\n first_serialize={first:?} second_serialize={second:?}\n available={available} capacity={capacity}"
|
||||
);
|
||||
assert!(available >= 1);
|
||||
assert_eq!(capacity, POOL_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
common::{sdk_log, SolanaRpcClient},
|
||||
swqos::{common::poll_transaction_confirmation, SwqosType, TradeType},
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -25,6 +25,7 @@ impl SwqosClientTrait for SolRpcClient {
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let submit_start = Instant::now();
|
||||
let signature = self
|
||||
.rpc_client
|
||||
.send_transaction_with_config(
|
||||
@@ -39,6 +40,8 @@ impl SwqosClientTrait for SolRpcClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
sdk_log::log_swqos_submitted("Default", trade_type, submit_start.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
|
||||
+108
-9
@@ -6,9 +6,11 @@ use quinn::{
|
||||
TransportConfig,
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use rcgen::{CertificateParams, KeyPair as RcgenKeyPair};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
net::{SocketAddr, ToSocketAddrs as _},
|
||||
@@ -25,6 +27,66 @@ use crate::{
|
||||
swqos::{SwqosType, TradeType},
|
||||
};
|
||||
|
||||
// Skip server verification implementation
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl SkipServerVerification {
|
||||
fn new() -> Arc<Self> {
|
||||
Arc::new(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS 客户端证书:ECDSA P-256 + CN=钱包公钥(与 Speedlanding / Astralane QUIC 策略一致)。
|
||||
fn generate_client_tls_credentials(
|
||||
keypair: &Keypair,
|
||||
) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
|
||||
let tls_key = RcgenKeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||
let mut cert_params = CertificateParams::new(vec![])?;
|
||||
cert_params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, rcgen::DnValue::Utf8String(keypair.pubkey().to_string()));
|
||||
let cert = cert_params.self_signed(&tls_key)?;
|
||||
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(tls_key.serialize_der()));
|
||||
Ok((cert_der, key_der))
|
||||
}
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
const SOYAS_SERVER: &str = "soyas-landing";
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||
@@ -42,8 +104,13 @@ pub struct SoyasClient {
|
||||
impl SoyasClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let keypair_bytes = bs58::decode(api_key.trim()).into_vec().map_err(|e| {
|
||||
anyhow::anyhow!("Soyas api_token base58 解码失败(QUIC mTLS 用): {}", e)
|
||||
})?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Soyas api_token 无法解析为 Solana keypair(QUIC mTLS 用): {}", e)
|
||||
})?;
|
||||
let (cert, key) = generate_client_tls_credentials(&keypair)?;
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
@@ -109,25 +176,57 @@ impl SwqosClientTrait for SoyasClient {
|
||||
let serialized_tx = bincode::serialize(transaction)?;
|
||||
let connection = self.connection.load_full();
|
||||
if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() {
|
||||
eprintln!(" [soyas] {} submission failed, reconnecting", trade_type);
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"reconnecting",
|
||||
);
|
||||
}
|
||||
self.reconnect().await?;
|
||||
let connection = self.connection.load_full();
|
||||
if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await {
|
||||
eprintln!(" [soyas] {} submission failed: {:?}", trade_type, e);
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Soyas", trade_type, start_time.elapsed());
|
||||
}
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [soyas] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [soyas] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+107
-26
@@ -6,6 +6,7 @@ use quinn::{
|
||||
TransportConfig,
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||
use std::time::Instant;
|
||||
@@ -15,6 +16,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::timeout;
|
||||
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::swqos::common::poll_transaction_confirmation;
|
||||
@@ -26,9 +28,12 @@ use crate::{
|
||||
};
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
/// QUIC TLS SNI:与 Speedlanding 官方客户端一致,固定为 `speed-landing`(勿用 PoP 主机名,否则易握手失败)。
|
||||
const SPEED_SERVER: &str = "speed-landing";
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub struct SpeedlandingClient {
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
@@ -42,7 +47,13 @@ pub struct SpeedlandingClient {
|
||||
impl SpeedlandingClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
// Speedlanding QUIC:与官方一致使用 `solana_tls_utils::new_dummy_x509_certificate`(Ed25519 dummy cert)+ SNI `speed-landing`。
|
||||
let keypair_bytes = bs58::decode(api_key.trim()).into_vec().map_err(|e| {
|
||||
anyhow::anyhow!("Speedlanding api_token base58 解码失败(mTLS 用): {}", e)
|
||||
})?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Speedlanding api_token 无法解析为 Solana keypair(mTLS 用): {}", e)
|
||||
})?;
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
@@ -66,7 +77,17 @@ impl SpeedlandingClient {
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
|
||||
let connection = endpoint.connect(addr, SPEED_SERVER)?.await?;
|
||||
let connecting = endpoint.connect(addr, SPEED_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC connect timeout")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Speedlanding QUIC handshake failed(请确认:1) 机器人登记的身份与钱包公钥 {} 一致 2) 本机 UDP 可访问 {} 3) region 与 PoP 匹配)",
|
||||
keypair.pubkey(),
|
||||
endpoint_string
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
@@ -78,14 +99,34 @@ impl SpeedlandingClient {
|
||||
})
|
||||
}
|
||||
|
||||
async fn reconnect(&self) -> Result<()> {
|
||||
let _guard = self.reconnect.try_lock()?;
|
||||
let connection = self
|
||||
.endpoint
|
||||
.connect_with(self.client_config.clone(), self.addr, SPEED_SERVER)?
|
||||
.await?;
|
||||
self.connection.store(Arc::new(connection));
|
||||
Ok(())
|
||||
/// Ensure we have a live connection: if current one is closed, reconnect under lock so
|
||||
/// concurrent senders wait and then all use the new connection. Uses blocking lock so
|
||||
/// waiters get the updated connection.
|
||||
async fn ensure_connected(&self) -> Result<Arc<Connection>> {
|
||||
let guard = self.reconnect.lock().await;
|
||||
let current = self.connection.load_full();
|
||||
if current.close_reason().is_none() {
|
||||
return Ok(current);
|
||||
}
|
||||
drop(guard);
|
||||
let _guard = self.reconnect.lock().await;
|
||||
let current = self.connection.load_full();
|
||||
if current.close_reason().is_some() {
|
||||
let connecting =
|
||||
self.endpoint.connect_with(self.client_config.clone(), self.addr, SPEED_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC reconnect timeout")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Speedlanding QUIC re-handshake failed(对端 {} SNI {})",
|
||||
self.addr, SPEED_SERVER
|
||||
)
|
||||
})?;
|
||||
self.connection.store(Arc::new(connection));
|
||||
return Ok(self.connection.load_full());
|
||||
}
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> Result<()> {
|
||||
@@ -106,33 +147,73 @@ impl SwqosClientTrait for SpeedlandingClient {
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (buf_guard, signature) = serialize_transaction_bincode_sync(transaction)?;
|
||||
let connection = self.connection.load_full();
|
||||
if Self::try_send_bytes(&connection, &*buf_guard).await.is_err() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [speedlanding] {} submission failed, reconnecting", trade_type);
|
||||
let connection = self.ensure_connected().await?;
|
||||
let mut send_result =
|
||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
|
||||
let need_retry = match &send_result {
|
||||
Ok(Ok(())) => false,
|
||||
Ok(Err(_)) | Err(_) => true,
|
||||
};
|
||||
if need_retry {
|
||||
eprintln!(
|
||||
" [Speedlanding] {} QUIC 首次发送失败 {:?},正在重试",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
);
|
||||
let connection = self.ensure_connected().await?;
|
||||
send_result =
|
||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
|
||||
}
|
||||
match send_result.context("Speedlanding QUIC send timeout") {
|
||||
Ok(Ok(())) => {
|
||||
// 提交结果与「详细耗时/SDK 开关」无关,便于确认当前通道确实在执行
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
self.reconnect().await?;
|
||||
let connection = self.connection.load_full();
|
||||
if let Err(e) = Self::try_send_bytes(&connection, &*buf_guard).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [speedlanding] {} submission failed: {:?}", trade_type, e);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
Err(e) => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"timeout",
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [speedlanding] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
}
|
||||
println!(" signature: {:?}", signature);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [speedlanding] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+75
-20
@@ -1,21 +1,22 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use std::time::Duration;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::STELLIUM_TIP_ACCOUNTS};
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct StelliumClient {
|
||||
pub endpoint: String,
|
||||
@@ -27,16 +28,29 @@ pub struct StelliumClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for StelliumClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *STELLIUM_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| STELLIUM_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *STELLIUM_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| STELLIUM_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -79,7 +93,9 @@ impl StelliumClient {
|
||||
tokio::spawn(async move {
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
let url = format!("{}/{}", endpoint, auth_token);
|
||||
if let Ok(resp) = http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
|
||||
if let Ok(resp) =
|
||||
http_client.get(&url).timeout(Duration::from_millis(1500)).send().await
|
||||
{
|
||||
let status = resp.status();
|
||||
let _ = resp.bytes().await;
|
||||
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
|
||||
@@ -111,9 +127,15 @@ impl StelliumClient {
|
||||
});
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Stellium uses standard Solana sendTransaction format
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
@@ -130,7 +152,9 @@ impl StelliumClient {
|
||||
let url = format!("{}/{}", self.endpoint, self.auth_token);
|
||||
|
||||
// Send request to Stellium
|
||||
let response_text = self.http_client.post(&url)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
.body(request_body)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Connection", "keep-alive")
|
||||
@@ -144,13 +168,27 @@ impl StelliumClient {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [Stellium] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -159,20 +197,37 @@ impl StelliumClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [Stellium] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [Stellium] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
|
||||
+97
-41
@@ -1,27 +1,29 @@
|
||||
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
|
||||
};
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
use std::time::Duration;
|
||||
use sha2::{Digest, Sha256};
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use sha2::{Sha256, Digest};
|
||||
use std::time::Duration;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::NOZOMI_TIP_ACCOUNTS};
|
||||
|
||||
use tokio::task::JoinHandle;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
const SPECIAL_API_KEY_PREFIX: &str = "298b5025";
|
||||
const SPECIAL_API_KEY_SUFFIX: &str = "a055323";
|
||||
|
||||
const SPECIAL_API_KEY_HASH: &str = "e7be933c8058aebcb4d08a6120fb4dfd2ead568d42527a3fc2b60a703f25e48d";
|
||||
const SPECIAL_API_KEY_HASH: &str =
|
||||
"e7be933c8058aebcb4d08a6120fb4dfd2ead568d42527a3fc2b60a703f25e48d";
|
||||
const TEMPORAL_COMMUNITY_TIP_ADDRESS: &str = "mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj";
|
||||
|
||||
#[inline]
|
||||
@@ -31,7 +33,6 @@ fn fast_sha256_hex(input: &str) -> String {
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporalClient {
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
@@ -44,18 +45,30 @@ pub struct TemporalClient {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for TemporalClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let api_key = &self.auth_token;
|
||||
if api_key.len() >= SPECIAL_API_KEY_PREFIX.len() + SPECIAL_API_KEY_SUFFIX.len() {
|
||||
if api_key.starts_with(SPECIAL_API_KEY_PREFIX) && api_key.ends_with(SPECIAL_API_KEY_SUFFIX) {
|
||||
if api_key.starts_with(SPECIAL_API_KEY_PREFIX)
|
||||
&& api_key.ends_with(SPECIAL_API_KEY_SUFFIX)
|
||||
{
|
||||
let current_api_key_hash = fast_sha256_hex(api_key);
|
||||
|
||||
if current_api_key_hash == SPECIAL_API_KEY_HASH {
|
||||
@@ -64,7 +77,10 @@ impl SwqosClientTrait for TemporalClient {
|
||||
}
|
||||
}
|
||||
|
||||
let tip_account = *NOZOMI_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NOZOMI_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *NOZOMI_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| NOZOMI_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -77,22 +93,22 @@ impl TemporalClient {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
stop_ping: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
|
||||
// Start ping task
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
@@ -102,7 +118,7 @@ impl TemporalClient {
|
||||
let auth_token = self.auth_token.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
@@ -114,12 +130,13 @@ impl TemporalClient {
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
eprintln!("Temporal ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update ping_handle - use Mutex to safely update
|
||||
{
|
||||
let mut ping_guard = self.ping_handle.lock().await;
|
||||
@@ -131,7 +148,11 @@ impl TemporalClient {
|
||||
}
|
||||
|
||||
/// Send ping request to /ping endpoint
|
||||
async fn send_ping_request(http_client: &Client, endpoint: &str, _auth_token: &str) -> Result<()> {
|
||||
async fn send_ping_request(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
_auth_token: &str,
|
||||
) -> Result<()> {
|
||||
// Build ping URL (no auth token required for ping endpoint)
|
||||
let ping_url = if endpoint.ends_with('/') {
|
||||
format!("{}ping", endpoint)
|
||||
@@ -140,10 +161,8 @@ impl TemporalClient {
|
||||
};
|
||||
|
||||
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||
let response = http_client.get(&ping_url)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
let response =
|
||||
http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await?;
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
@@ -152,9 +171,15 @@ impl TemporalClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
let (content, signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Build request body according to Nozomi documentation requirements
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
@@ -172,7 +197,9 @@ impl TemporalClient {
|
||||
url.push_str("/?c=");
|
||||
url.push_str(&self.auth_token);
|
||||
|
||||
let response_text = self.http_client.post(&url)
|
||||
let response_text = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
.body(request_body)
|
||||
.header("Content-Type", "application/json")
|
||||
.send()
|
||||
@@ -182,12 +209,26 @@ impl TemporalClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [nozomi] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
// eprintln!("nozomi transaction submission failed: {:?}", _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [nozomi] {} submission failed: {:?}", trade_type, response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -195,19 +236,34 @@ impl TemporalClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [nozomi] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [nozomi] {} confirmation failed: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [nozomi] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
@@ -219,7 +275,7 @@ impl Drop for TemporalClient {
|
||||
fn drop(&mut self) {
|
||||
// Ensure ping task stops when client is destroyed
|
||||
self.stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
|
||||
// Try to stop ping task immediately
|
||||
// Use tokio::spawn to avoid blocking Drop
|
||||
let ping_handle = self.ping_handle.clone();
|
||||
@@ -231,4 +287,4 @@ impl Drop for TemporalClient {
|
||||
*ping_guard = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+255
-44
@@ -1,39 +1,53 @@
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
|
||||
use bincode;
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use serde_json::json;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{sync::Arc, time::Duration, time::Instant};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use anyhow::Result;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
|
||||
use crate::{common::SolanaRpcClient, constants::swqos::ZEROSLOT_TIP_ACCOUNTS};
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ZeroSlotClient {
|
||||
pub endpoint: String,
|
||||
pub auth_token: String,
|
||||
pub rpc_client: Arc<SolanaRpcClient>,
|
||||
pub http_client: Client,
|
||||
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
pub stop_ping: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for ZeroSlotClient {
|
||||
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transactions(trade_type, transactions, wait_confirmation).await
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ZEROSLOT_TIP_ACCOUNTS.first()).unwrap();
|
||||
let tip_account = *ZEROSLOT_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| ZEROSLOT_TIP_ACCOUNTS.first())
|
||||
.unwrap();
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
@@ -46,69 +60,266 @@ impl ZeroSlotClient {
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
|
||||
stop_ping: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
|
||||
// Start ping task
|
||||
let client_clone = client.clone();
|
||||
tokio::spawn(async move {
|
||||
client_clone.start_ping_task().await;
|
||||
});
|
||||
|
||||
client
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
/// Start periodic ping task to keep connections active
|
||||
async fn start_ping_task(&self) {
|
||||
let endpoint = self.endpoint.clone();
|
||||
let auth_token = self.auth_token.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("0slot ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive under 65s server timeout
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("0slot ping request failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update ping_handle - use Mutex to safely update
|
||||
{
|
||||
let mut ping_guard = self.ping_handle.lock().await;
|
||||
if let Some(old_handle) = ping_guard.as_ref() {
|
||||
old_handle.abort();
|
||||
}
|
||||
*ping_guard = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send ping request: POST with getHealth method (Keep Alive). Free operation, not counted toward TPS.
|
||||
async fn send_ping_request(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
auth_token: &str,
|
||||
) -> Result<()> {
|
||||
let url = format!("{}/?api-key={}", endpoint, auth_token);
|
||||
let response = http_client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.body(r#"{"jsonrpc":"2.0","id":1,"method":"getHealth"}"#)
|
||||
.send()
|
||||
.await?;
|
||||
let _ = response.bytes().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "sendTransaction",
|
||||
"params": [
|
||||
content,
|
||||
{ "encoding": "base64", "skipPreflight": true }
|
||||
]
|
||||
}))?;
|
||||
// Binary-Tx: Send raw binary transaction bytes directly
|
||||
// This is faster than JSON-RPC as it avoids unnecessary encoding/decoding
|
||||
let tx_bytes = bincode::serialize(transaction)?;
|
||||
|
||||
// Build URL for Binary-Tx endpoint: {endpoint}/txb?api-key={auth_token}
|
||||
let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20);
|
||||
url.push_str(&self.endpoint);
|
||||
url.push_str("/?api-key=");
|
||||
url.push_str("/txb?api-key=");
|
||||
url.push_str(&self.auth_token);
|
||||
|
||||
// 4. Use `text().await?` directly, avoiding async JSON parsing from `json().await?`
|
||||
let response_text = self.http_client.post(&url)
|
||||
.body(request_body) // Pass string directly, avoiding `json()` overhead
|
||||
.header("Content-Type", "application/json") // Explicitly specify JSON header
|
||||
// Send binary transaction directly
|
||||
let response = self
|
||||
.http_client
|
||||
.post(&url)
|
||||
.header("User-Agent", "") // Optional: 0slot recommends empty User-Agent
|
||||
.body(tx_bytes)
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [0slot] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [0slot] {} submission failed: {:?}", trade_type, _error);
|
||||
let status = response.status();
|
||||
let response_text = response.text().await?;
|
||||
|
||||
// Binary-Tx returns JSON-RPC 2.0 format responses
|
||||
// 200: success with result field containing signature, or error field with code/message
|
||||
// 403: api-key error (null, doesn't exist, or expired)
|
||||
// 419: rate limit exceeded
|
||||
// 500: submission failed
|
||||
match status.as_u16() {
|
||||
200 => {
|
||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if json_value.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(error) = json_value.get("error") {
|
||||
let code = error
|
||||
.get("code")
|
||||
.and_then(|c| c.as_i64())
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let message = error
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("code {}: {}", code, message),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx error: {}", message));
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("unexpected JSON: {}", response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"0slot Binary-Tx unexpected JSON: {}",
|
||||
response_text
|
||||
));
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("invalid JSON: {}", response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx invalid JSON: {}", response_text));
|
||||
}
|
||||
}
|
||||
403 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text.clone(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot API key error: {}", response_text));
|
||||
}
|
||||
419 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text.clone(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot rate limit exceeded"));
|
||||
}
|
||||
500 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"submission failed".to_string(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot transaction submission failed"));
|
||||
}
|
||||
_ => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {} body: {}", status, response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"0slot Binary-Tx failed with status {}: {}",
|
||||
status,
|
||||
response_text
|
||||
));
|
||||
}
|
||||
} else {
|
||||
eprintln!(" [0slot] {} submission failed: {:?}", trade_type, response_text);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
// Get transaction signature from the transaction for confirmation polling
|
||||
let signature = transaction.signatures[0];
|
||||
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [0slot] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [0slot] {} confirmed: {:?}", trade_type, start_time.elapsed());
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
|
||||
pub async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ZeroSlotClient {
|
||||
fn drop(&mut self) {
|
||||
// Ensure ping task stops when client is destroyed
|
||||
self.stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
// Try to stop ping task immediately
|
||||
// Use tokio::spawn to avoid blocking Drop
|
||||
let ping_handle = self.ping_handle.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut ping_guard = ping_handle.lock().await;
|
||||
if let Some(handle) = ping_guard.as_ref() {
|
||||
handle.abort();
|
||||
}
|
||||
*ping_guard = None;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use dashmap::DashMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use smallvec::SmallVec;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use solana_compute_budget_interface::ComputeBudgetInstruction;
|
||||
use solana_sdk::instruction::Instruction;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Cache key containing all parameters for compute budget instructions
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -11,43 +12,52 @@ struct ComputeBudgetCacheKey {
|
||||
unit_limit: u32,
|
||||
}
|
||||
|
||||
/// Global cache storing compute budget instructions
|
||||
/// Uses DashMap for high-performance lock-free concurrent access
|
||||
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, SmallVec<[Instruction; 2]>>> =
|
||||
/// Global cache storing compute budget instructions (Arc to avoid clone on hit).
|
||||
/// Uses DashMap for high-performance lock-free concurrent access.
|
||||
static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, Arc<SmallVec<[Instruction; 2]>>>> =
|
||||
Lazy::new(|| DashMap::new());
|
||||
|
||||
/// Extend `instructions` with compute budget instructions; on cache hit extends from cached Arc (no SmallVec clone).
|
||||
#[inline(always)]
|
||||
pub fn compute_budget_instructions(
|
||||
pub fn extend_compute_budget_instructions(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
unit_price: u64,
|
||||
unit_limit: u32,
|
||||
) -> SmallVec<[Instruction; 2]> {
|
||||
// Create cache key
|
||||
let cache_key = ComputeBudgetCacheKey {
|
||||
unit_price: unit_price,
|
||||
unit_limit: unit_limit,
|
||||
};
|
||||
) {
|
||||
let cache_key = ComputeBudgetCacheKey { unit_price, unit_limit };
|
||||
|
||||
// Try to get from cache first
|
||||
if let Some(cached_insts) = COMPUTE_BUDGET_CACHE.get(&cache_key) {
|
||||
return cached_insts.clone();
|
||||
if let Some(cached) = COMPUTE_BUDGET_CACHE.get(&cache_key) {
|
||||
instructions.extend(cached.iter().cloned());
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache miss, generate new instructions
|
||||
let mut insts = SmallVec::<[Instruction; 2]>::new();
|
||||
|
||||
// Only add compute unit price instruction if > 0
|
||||
if unit_price > 0 {
|
||||
insts.push(ComputeBudgetInstruction::set_compute_unit_price(unit_price));
|
||||
}
|
||||
|
||||
// Only add compute unit limit instruction if > 0
|
||||
if unit_limit > 0 {
|
||||
insts.push(ComputeBudgetInstruction::set_compute_unit_limit(unit_limit));
|
||||
}
|
||||
let arc = Arc::new(insts);
|
||||
instructions.extend(arc.iter().cloned());
|
||||
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
|
||||
}
|
||||
|
||||
// Store result in cache
|
||||
let insts_clone = insts.clone();
|
||||
COMPUTE_BUDGET_CACHE.insert(cache_key, insts_clone);
|
||||
|
||||
/// Returns compute budget instructions (allocates on cache hit; prefer `extend_compute_budget_instructions` on hot path).
|
||||
#[inline(always)]
|
||||
pub fn compute_budget_instructions(unit_price: u64, unit_limit: u32) -> SmallVec<[Instruction; 2]> {
|
||||
let cache_key = ComputeBudgetCacheKey { unit_price, unit_limit };
|
||||
if let Some(cached) = COMPUTE_BUDGET_CACHE.get(&cache_key) {
|
||||
return (**cached).clone();
|
||||
}
|
||||
let mut insts = SmallVec::<[Instruction; 2]>::new();
|
||||
if unit_price > 0 {
|
||||
insts.push(ComputeBudgetInstruction::set_compute_unit_price(unit_price));
|
||||
}
|
||||
if unit_limit > 0 {
|
||||
insts.push(ComputeBudgetInstruction::set_compute_unit_limit(unit_limit));
|
||||
}
|
||||
let arc = Arc::new(insts.clone());
|
||||
COMPUTE_BUDGET_CACHE.insert(cache_key, arc);
|
||||
insts
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
pub mod compute_budget_manager;
|
||||
pub mod nonce_manager;
|
||||
pub mod transaction_builder;
|
||||
pub mod compute_budget_manager;
|
||||
pub mod utils;
|
||||
pub mod wsol_manager;
|
||||
|
||||
// Re-export commonly used functions
|
||||
pub use compute_budget_manager::*;
|
||||
pub use nonce_manager::*;
|
||||
pub use transaction_builder::*;
|
||||
pub use compute_budget_manager::*;
|
||||
pub use utils::*;
|
||||
pub use wsol_manager::*;
|
||||
pub use wsol_manager::*;
|
||||
|
||||
@@ -12,29 +12,33 @@ use crate::common::nonce_cache::DurableNonceInfo;
|
||||
pub fn add_nonce_instruction(
|
||||
instructions: &mut Vec<Instruction>,
|
||||
payer: &Keypair,
|
||||
// nonce_account: Option<Pubkey>,
|
||||
// current_nonce: Option<Hash>,
|
||||
durable_nonce: Option<DurableNonceInfo>,
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
if let Some(durable_nonce) = durable_nonce {
|
||||
let nonce_advance_ix = advance_nonce_account(&durable_nonce.nonce_account.unwrap(), &payer.pubkey());
|
||||
let nonce_advance_ix =
|
||||
advance_nonce_account(&durable_nonce.nonce_account.unwrap(), &payer.pubkey());
|
||||
instructions.push(nonce_advance_ix);
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get blockhash for transaction
|
||||
/// If nonce account is used, return blockhash from nonce, otherwise return the provided recent_blockhash
|
||||
/// Get blockhash for transaction.
|
||||
/// If nonce account is used, returns blockhash from nonce; otherwise returns the provided recent_blockhash.
|
||||
/// Returns error when neither durable_nonce nor recent_blockhash is set (caller must provide one for low latency).
|
||||
pub fn get_transaction_blockhash(
|
||||
recent_blockhash: Option<Hash>,
|
||||
durable_nonce: Option<DurableNonceInfo>,
|
||||
// nonce_account: Option<Pubkey>,
|
||||
// current_nonce: Option<Hash>,
|
||||
) -> Hash {
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<Hash, anyhow::Error> {
|
||||
if let Some(durable_nonce) = durable_nonce {
|
||||
durable_nonce.current_nonce.unwrap()
|
||||
durable_nonce
|
||||
.current_nonce
|
||||
.ok_or_else(|| anyhow::anyhow!("durable_nonce.current_nonce is None"))
|
||||
} else if let Some(hash) = recent_blockhash {
|
||||
Ok(hash)
|
||||
} else {
|
||||
recent_blockhash.unwrap()
|
||||
Err(anyhow::anyhow!(
|
||||
"Must provide either recent_blockhash or durable_nonce for transaction"
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, message::AddressLookupTableAccount, native_token::sol_str_to_lamports, pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::VersionedTransaction
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer,
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{
|
||||
compute_budget_manager::compute_budget_instructions,
|
||||
nonce_manager::{add_nonce_instruction, get_transaction_blockhash},
|
||||
};
|
||||
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
|
||||
use crate::{
|
||||
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
|
||||
trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}},
|
||||
trading::{
|
||||
core::transaction_pool::{acquire_builder, release_builder},
|
||||
MiddlewareManager,
|
||||
},
|
||||
};
|
||||
|
||||
/// Build standard RPC transaction.
|
||||
/// Takes `business_instructions` by reference to avoid per-task Vec clone in execute_parallel.
|
||||
/// Convert SOL amount (f64) to lamports without string allocation (hot path).
|
||||
#[inline(always)]
|
||||
fn sol_f64_to_lamports(sol: f64) -> u64 {
|
||||
if sol <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
let lamports = sol * 1_000_000_000.0;
|
||||
(lamports.min(u64::MAX as f64)).round() as u64
|
||||
}
|
||||
|
||||
/// Build standard RPC transaction (worker hot path).
|
||||
/// Takes Arc/refs only; one Vec allocation (with_capacity), extend_from_slice for business_instructions, no extra clone of payer/rpc/middleware.
|
||||
pub async fn build_transaction(
|
||||
payer: Arc<Keypair>,
|
||||
_rpc: Option<Arc<SolanaRpcClient>>,
|
||||
payer: &Arc<Keypair>,
|
||||
_rpc: Option<&Arc<SolanaRpcClient>>,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
business_instructions: &[Instruction],
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
address_lookup_table_account: Option<&AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
middleware_manager: Option<&Arc<MiddlewareManager>>,
|
||||
protocol_name: &str,
|
||||
is_buy: bool,
|
||||
with_tip: bool,
|
||||
tip_account: &Pubkey,
|
||||
tip_amount: f64,
|
||||
durable_nonce: Option<DurableNonceInfo>,
|
||||
// nonce_account: Option<Pubkey>,
|
||||
// current_nonce: Option<Hash>,
|
||||
durable_nonce: Option<&DurableNonceInfo>,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let mut instructions = Vec::with_capacity(business_instructions.len() + 5);
|
||||
|
||||
// Add nonce instruction
|
||||
if let Err(e) =
|
||||
add_nonce_instruction(&mut instructions, payer.as_ref(), durable_nonce.clone())
|
||||
{
|
||||
if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref(), durable_nonce) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Add tip transfer instruction
|
||||
if with_tip && tip_amount > 0.0 {
|
||||
instructions.push(transfer(
|
||||
&payer.pubkey(),
|
||||
tip_account,
|
||||
sol_str_to_lamports(tip_amount.to_string().as_str()).unwrap_or(0),
|
||||
));
|
||||
let tip_lamports = sol_f64_to_lamports(tip_amount);
|
||||
instructions.push(system_instruction::transfer(&payer.pubkey(), tip_account, tip_lamports));
|
||||
}
|
||||
|
||||
// Add compute budget instructions
|
||||
instructions.extend(compute_budget_instructions(
|
||||
super::compute_budget_manager::extend_compute_budget_instructions(
|
||||
&mut instructions,
|
||||
unit_price,
|
||||
unit_limit,
|
||||
));
|
||||
);
|
||||
|
||||
// Add business instructions (clone only here; avoids per-task Vec clone in execute_parallel)
|
||||
instructions.extend_from_slice(business_instructions);
|
||||
|
||||
// Get blockhash for transaction
|
||||
let blockhash = get_transaction_blockhash(recent_blockhash, durable_nonce.clone());
|
||||
let blockhash = get_transaction_blockhash(recent_blockhash, durable_nonce)?;
|
||||
|
||||
// Build transaction
|
||||
build_versioned_transaction(
|
||||
payer,
|
||||
instructions,
|
||||
@@ -77,23 +77,18 @@ pub async fn build_transaction(
|
||||
.await
|
||||
}
|
||||
|
||||
/// Low-level function for building versioned transactions
|
||||
async fn build_versioned_transaction(
|
||||
payer: Arc<Keypair>,
|
||||
payer: &Arc<Keypair>,
|
||||
instructions: Vec<Instruction>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
address_lookup_table_account: Option<&AddressLookupTableAccount>,
|
||||
blockhash: Hash,
|
||||
middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
middleware_manager: Option<&Arc<MiddlewareManager>>,
|
||||
protocol_name: &str,
|
||||
is_buy: bool,
|
||||
) -> Result<VersionedTransaction, anyhow::Error> {
|
||||
let full_instructions = match middleware_manager {
|
||||
Some(middleware_manager) => middleware_manager
|
||||
.apply_middlewares_process_full_instructions(
|
||||
instructions,
|
||||
protocol_name.to_string(),
|
||||
is_buy,
|
||||
)?,
|
||||
.apply_middlewares_process_full_instructions(instructions, protocol_name, is_buy)?,
|
||||
None => instructions,
|
||||
};
|
||||
|
||||
@@ -108,7 +103,7 @@ async fn build_versioned_transaction(
|
||||
);
|
||||
|
||||
let msg_bytes = versioned_msg.serialize();
|
||||
let signature = payer.try_sign_message(&msg_bytes).expect("sign failed");
|
||||
let signature = payer.as_ref().try_sign_message(&msg_bytes).expect("sign failed");
|
||||
let tx = VersionedTransaction { signatures: vec![signature], message: versioned_msg };
|
||||
|
||||
// 归还构建器到池
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
|
||||
use crate::common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast, spl_token::close_account,
|
||||
fast_fn::{
|
||||
get_associated_token_address_with_program_id_fast,
|
||||
get_associated_token_address_with_program_id_fast_use_seed,
|
||||
},
|
||||
spl_token::close_account,
|
||||
SolanaRpcClient,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@@ -36,10 +40,23 @@ pub async fn get_token_balance(
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
get_token_balance_with_options(rpc, payer, mint, &crate::constants::TOKEN_PROGRAM, false).await
|
||||
}
|
||||
|
||||
/// 使用与交易指令一致的 ATA 推导(可选 seed)查询余额;卖出/余额查询应与买入使用同一 ATA 地址。
|
||||
#[inline]
|
||||
pub async fn get_token_balance_with_options(
|
||||
rpc: &SolanaRpcClient,
|
||||
payer: &Pubkey,
|
||||
mint: &Pubkey,
|
||||
token_program: &Pubkey,
|
||||
use_seed: bool,
|
||||
) -> Result<u64, anyhow::Error> {
|
||||
let ata = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
payer,
|
||||
mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
token_program,
|
||||
use_seed,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&ata).await?;
|
||||
let balance_u64 =
|
||||
@@ -71,7 +88,8 @@ pub async fn transfer_sol(
|
||||
return Err(anyhow!("Insufficient balance"));
|
||||
}
|
||||
|
||||
let transfer_instruction = transfer(&payer.pubkey(), receive_wallet, amount);
|
||||
let transfer_instruction =
|
||||
system_instruction::transfer(&payer.pubkey(), receive_wallet, amount);
|
||||
|
||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
use crate::common::{
|
||||
fast_fn::create_associated_token_account_idempotent_fast,
|
||||
seed::{
|
||||
create_associated_token_account_use_seed,
|
||||
get_associated_token_address_with_program_id_use_seed,
|
||||
},
|
||||
spl_token::close_account,
|
||||
seed::{create_associated_token_account_use_seed, get_associated_token_address_with_program_id_use_seed},
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use solana_sdk::{instruction::Instruction, message::AccountMeta, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_sdk::{instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
|
||||
#[inline]
|
||||
pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]> {
|
||||
@@ -24,7 +27,7 @@ pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]>
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
));
|
||||
insts.extend([
|
||||
transfer(&payer, &wsol_token_account, amount_in),
|
||||
system_instruction::transfer(&payer, &wsol_token_account, amount_in),
|
||||
// sync_native
|
||||
Instruction {
|
||||
program_id: crate::constants::TOKEN_PROGRAM,
|
||||
@@ -38,7 +41,7 @@ pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]>
|
||||
|
||||
pub fn close_wsol(payer: &Pubkey) -> Vec<Instruction> {
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
let wsol_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&payer,
|
||||
@@ -61,7 +64,7 @@ pub fn close_wsol(payer: &Pubkey) -> Vec<Instruction> {
|
||||
.unwrap()]
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
// 🚀 性能优化:尝试零开销解包 Arc
|
||||
Arc::try_unwrap(arc_instructions).unwrap_or_else(|arc| (*arc).clone())
|
||||
}
|
||||
@@ -88,7 +91,7 @@ pub fn wrap_sol_only(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 2
|
||||
|
||||
let mut insts = SmallVec::<[Instruction; 2]>::new();
|
||||
insts.extend([
|
||||
transfer(&payer, &wsol_token_account, amount_in),
|
||||
system_instruction::transfer(&payer, &wsol_token_account, amount_in),
|
||||
// sync_native
|
||||
Instruction {
|
||||
program_id: crate::constants::TOKEN_PROGRAM,
|
||||
@@ -109,10 +112,7 @@ pub fn wrap_sol_only(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 2
|
||||
///
|
||||
/// 注意:此函数只生成指令,不检查账户是否存在(需要调用方在发送交易前检查)
|
||||
/// 如果临时账户已存在,可以安全地跳过创建步骤,直接转账并关闭
|
||||
pub fn wrap_wsol_to_sol(
|
||||
payer: &Pubkey,
|
||||
amount: u64,
|
||||
) -> Result<Vec<Instruction>, anyhow::Error> {
|
||||
pub fn wrap_wsol_to_sol(payer: &Pubkey, amount: u64) -> Result<Vec<Instruction>, anyhow::Error> {
|
||||
let mut instructions = Vec::new();
|
||||
|
||||
// 1. 创建 WSOL seed 账户(注意:如果账户已存在会失败)
|
||||
@@ -151,13 +151,8 @@ pub fn wrap_wsol_to_sol(
|
||||
instructions.push(transfer_instruction);
|
||||
|
||||
// 5. 添加关闭 WSOL seed 账户的指令
|
||||
let close_instruction = close_account(
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
&seed_ata_address,
|
||||
payer,
|
||||
payer,
|
||||
&[],
|
||||
)?;
|
||||
let close_instruction =
|
||||
close_account(&crate::constants::TOKEN_PROGRAM, &seed_ata_address, payer, payer, &[])?;
|
||||
instructions.push(close_instruction);
|
||||
|
||||
Ok(instructions)
|
||||
@@ -197,13 +192,8 @@ pub fn wrap_wsol_to_sol_without_create(
|
||||
instructions.push(transfer_instruction);
|
||||
|
||||
// 4. 添加关闭 WSOL seed 账户的指令
|
||||
let close_instruction = close_account(
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
&seed_ata_address,
|
||||
payer,
|
||||
payer,
|
||||
&[],
|
||||
)?;
|
||||
let close_instruction =
|
||||
close_account(&crate::constants::TOKEN_PROGRAM, &seed_ata_address, payer, payer, &[])?;
|
||||
instructions.push(close_instruction);
|
||||
|
||||
Ok(instructions)
|
||||
|
||||
+371
-169
@@ -1,30 +1,240 @@
|
||||
//! Parallel executor for multi-SWQOS submit.
|
||||
//!
|
||||
//! **Hot path (submit):** no lock (OnceCell + lock-free ArrayQueue), no `get_core_ids()`, only Arc clones and queue push.
|
||||
//! - **Pool**: Pre-spawned workers (default 18); hot path only enqueues jobs (no per-call tokio::spawn).
|
||||
//! - **Dedicated threads** (opt-in via `with_dedicated_sender_threads`): N OS threads run sender work only, optionally pinned to cores.
|
||||
//! - **Arc**: Shared data behind `Arc` → clone = refcount increment (no data copy).
|
||||
//! - **Refs**: `build_transaction` takes refs only; worker path avoids extra clones.
|
||||
//!
|
||||
//! **Core affinity & latency:** Each job is assigned a core (round-robin from `effective_core_ids`). When a worker runs a job,
|
||||
//! it sets thread affinity to that core. If that core is busy with other work (e.g. node sync, bot logic), SWQOS submit on that
|
||||
//! core will compete for CPU and latency can increase. For lowest latency, reserve a subset of cores for SWQOS only via
|
||||
//! `with_dedicated_sender_threads(Some(indices))` and avoid running other CPU-heavy work on those core indices.
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::message::AddressLookupTableAccount;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::{str::FromStr, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use fnv::FnvHasher;
|
||||
|
||||
type FnvHashMap<K, V> = HashMap<K, V, BuildHasherDefault<FnvHasher>>;
|
||||
|
||||
use crate::{
|
||||
common::nonce_cache::DurableNonceInfo,
|
||||
common::{GasFeeStrategy, SolanaRpcClient},
|
||||
swqos::{SwqosClient, SwqosType, TradeType},
|
||||
trading::core::params::SenderConcurrencyConfig,
|
||||
trading::{common::build_transaction, MiddlewareManager},
|
||||
};
|
||||
|
||||
/// 与 transaction_pool::PARALLEL_SENDER_COUNT 一致,保证多路 build 不串行
|
||||
const SWQOS_POOL_WORKERS: usize = 18;
|
||||
const SWQOS_QUEUE_CAP: usize = 128;
|
||||
const SWQOS_DEDICATED_DEFAULT_THREADS: usize = 18;
|
||||
|
||||
/// Shared across all jobs in one batch; built once, cloned as single Arc per job (minimal hot-path clone).
|
||||
struct SwqosSharedContext {
|
||||
payer: Arc<Keypair>,
|
||||
instructions: Arc<Vec<Instruction>>,
|
||||
rpc: Option<Arc<SolanaRpcClient>>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
durable_nonce: Option<DurableNonceInfo>,
|
||||
middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
protocol_name: &'static str,
|
||||
is_buy: bool,
|
||||
wait_transaction_confirmed: bool,
|
||||
with_tip: bool,
|
||||
collector: Arc<ResultCollector>,
|
||||
}
|
||||
|
||||
/// One SWQOS submit task; only per-task data + one Arc to shared (reduces hot-path clones).
|
||||
struct SwqosJob {
|
||||
shared: Arc<SwqosSharedContext>,
|
||||
tip: f64,
|
||||
unit_limit: u32,
|
||||
unit_price: u64,
|
||||
tip_account: Arc<Pubkey>,
|
||||
swqos_client: Arc<SwqosClient>,
|
||||
swqos_type: SwqosType,
|
||||
core_id: Option<core_affinity::CoreId>,
|
||||
use_affinity: bool,
|
||||
}
|
||||
|
||||
async fn run_one_swqos_job(job: SwqosJob) {
|
||||
let s = &job.shared;
|
||||
if job.use_affinity {
|
||||
if let Some(cid) = job.core_id {
|
||||
core_affinity::set_for_current(cid);
|
||||
}
|
||||
}
|
||||
|
||||
let tip_amount = if s.with_tip { job.tip } else { 0.0 };
|
||||
|
||||
let transaction = match build_transaction(
|
||||
&s.payer,
|
||||
s.rpc.as_ref(),
|
||||
job.unit_limit,
|
||||
job.unit_price,
|
||||
s.instructions.as_ref(),
|
||||
s.address_lookup_table_account.as_ref(),
|
||||
s.recent_blockhash,
|
||||
s.middleware_manager.as_ref(),
|
||||
s.protocol_name,
|
||||
s.is_buy,
|
||||
job.swqos_type != SwqosType::Default,
|
||||
&job.tip_account,
|
||||
tip_amount,
|
||||
s.durable_nonce.as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
s.collector.submit(TaskResult {
|
||||
success: false,
|
||||
signature: Signature::default(),
|
||||
error: Some(e),
|
||||
swqos_type: job.swqos_type,
|
||||
landed_on_chain: false,
|
||||
submit_done_us: crate::common::clock::now_micros(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let (success, err, landed_on_chain) = match job
|
||||
.swqos_client
|
||||
.send_transaction(
|
||||
if s.is_buy { TradeType::Buy } else { TradeType::Sell },
|
||||
&transaction,
|
||||
s.wait_transaction_confirmed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => (true, None, true),
|
||||
Err(e) => {
|
||||
let landed = is_landed_error(&e);
|
||||
(false, Some(e), landed)
|
||||
}
|
||||
};
|
||||
|
||||
let sig = transaction.signatures.first().copied().unwrap_or_default();
|
||||
s.collector.submit(TaskResult {
|
||||
success,
|
||||
signature: sig,
|
||||
error: err,
|
||||
swqos_type: job.swqos_type,
|
||||
landed_on_chain,
|
||||
submit_done_us: crate::common::clock::now_micros(),
|
||||
});
|
||||
}
|
||||
|
||||
async fn swqos_worker_loop(queue: Arc<ArrayQueue<SwqosJob>>, notify: Arc<Notify>) {
|
||||
loop {
|
||||
if let Some(job) = queue.pop() {
|
||||
run_one_swqos_job(job).await;
|
||||
} else {
|
||||
notify.notified().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static SWQOS_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
|
||||
static SWQOS_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
|
||||
static SWQOS_WORKERS_STARTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Dedicated OS-thread sender pool. Queue and notify are in OnceCell so hot path never takes a lock after init.
|
||||
static DEDICATED_QUEUE: OnceCell<Arc<ArrayQueue<SwqosJob>>> = OnceCell::new();
|
||||
static DEDICATED_NOTIFY: OnceCell<Arc<Notify>> = OnceCell::new();
|
||||
/// JoinHandles kept so dedicated threads are not detached; only touched during init under lock.
|
||||
static DEDICATED_INIT: Mutex<Option<Vec<std::thread::JoinHandle<()>>>> = Mutex::new(None);
|
||||
|
||||
fn ensure_dedicated_pool(
|
||||
sender_thread_cores: Option<&[usize]>,
|
||||
max_sender_concurrency: usize,
|
||||
) -> (Arc<ArrayQueue<SwqosJob>>, Arc<Notify>) {
|
||||
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
|
||||
return (q.clone(), n.clone());
|
||||
}
|
||||
let mut guard = DEDICATED_INIT.lock();
|
||||
if let (Some(q), Some(n)) = (DEDICATED_QUEUE.get(), DEDICATED_NOTIFY.get()) {
|
||||
return (q.clone(), n.clone());
|
||||
}
|
||||
let n = sender_thread_cores
|
||||
.map(|v| v.len().min(max_sender_concurrency))
|
||||
.unwrap_or_else(|| SWQOS_DEDICATED_DEFAULT_THREADS.min(max_sender_concurrency))
|
||||
.min(32)
|
||||
.max(1);
|
||||
let queue = Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP));
|
||||
let notify = Arc::new(Notify::new());
|
||||
let core_ids: Vec<core_affinity::CoreId> = core_affinity::get_core_ids()
|
||||
.map(|all_ids| {
|
||||
sender_thread_cores
|
||||
.map(|indices| {
|
||||
indices.iter().take(n).filter_map(|&i| all_ids.get(i).cloned()).collect()
|
||||
})
|
||||
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let mut handles = Vec::with_capacity(n);
|
||||
for i in 0..n {
|
||||
let queue = queue.clone();
|
||||
let notify = notify.clone();
|
||||
let core_id = core_ids.get(i).cloned();
|
||||
let handle = std::thread::spawn(move || {
|
||||
if let Some(cid) = core_id {
|
||||
core_affinity::set_for_current(cid);
|
||||
}
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("dedicated sender runtime");
|
||||
rt.block_on(swqos_worker_loop(queue, notify));
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
let _ = DEDICATED_QUEUE.set(queue.clone());
|
||||
let _ = DEDICATED_NOTIFY.set(notify.clone());
|
||||
*guard = Some(handles);
|
||||
(queue, notify)
|
||||
}
|
||||
|
||||
fn ensure_swqos_pool(queue: Arc<ArrayQueue<SwqosJob>>, max_sender_concurrency: usize) {
|
||||
if SWQOS_WORKERS_STARTED.swap(true, Ordering::AcqRel) {
|
||||
return;
|
||||
}
|
||||
let n = SWQOS_POOL_WORKERS.min(max_sender_concurrency).max(1);
|
||||
let notify = SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone();
|
||||
for _ in 0..n {
|
||||
tokio::spawn(swqos_worker_loop(queue.clone(), notify.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(align(64))]
|
||||
struct TaskResult {
|
||||
success: bool,
|
||||
signature: Signature,
|
||||
error: Option<anyhow::Error>,
|
||||
#[allow(dead_code)]
|
||||
swqos_type: SwqosType,
|
||||
landed_on_chain: bool,
|
||||
/// Microsecond timestamp when this task finished (SWQOS returned); for per-SWQOS event→submit timing.
|
||||
submit_done_us: i64,
|
||||
}
|
||||
|
||||
/// Check if an error indicates the transaction landed on-chain (vs network/timeout error)
|
||||
@@ -54,7 +264,7 @@ fn is_landed_error(error: &anyhow::Error) -> bool {
|
||||
struct ResultCollector {
|
||||
results: Arc<ArrayQueue<TaskResult>>,
|
||||
success_flag: Arc<AtomicBool>,
|
||||
landed_failed_flag: Arc<AtomicBool>, // 🔧 Tx landed on-chain but failed (nonce consumed)
|
||||
landed_failed_flag: Arc<AtomicBool>, // 🔧 Tx landed on-chain but failed (nonce consumed)
|
||||
completed_count: Arc<AtomicUsize>,
|
||||
total_tasks: usize,
|
||||
}
|
||||
@@ -87,7 +297,9 @@ impl ResultCollector {
|
||||
self.completed_count.fetch_add(1, Ordering::Release);
|
||||
}
|
||||
|
||||
async fn wait_for_success(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
async fn wait_for_success(
|
||||
&self,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
let start = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(5);
|
||||
let poll_interval = std::time::Duration::from_millis(1000);
|
||||
@@ -96,14 +308,16 @@ impl ResultCollector {
|
||||
if self.success_flag.load(Ordering::Acquire) {
|
||||
let mut signatures = Vec::new();
|
||||
let mut has_success = false;
|
||||
let mut submit_timings = Vec::new();
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
if result.success {
|
||||
has_success = true;
|
||||
}
|
||||
}
|
||||
if has_success && !signatures.is_empty() {
|
||||
return Some((true, signatures, None));
|
||||
return Some((true, signatures, None, submit_timings));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,25 +326,29 @@ impl ResultCollector {
|
||||
if self.landed_failed_flag.load(Ordering::Acquire) {
|
||||
let mut signatures = Vec::new();
|
||||
let mut landed_error = None;
|
||||
let mut submit_timings = Vec::new();
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
// Prefer the error from the tx that actually landed
|
||||
if result.landed_on_chain && result.error.is_some() {
|
||||
landed_error = result.error;
|
||||
}
|
||||
}
|
||||
if !signatures.is_empty() {
|
||||
return Some((false, signatures, landed_error));
|
||||
return Some((false, signatures, landed_error, submit_timings));
|
||||
}
|
||||
}
|
||||
|
||||
let completed = self.completed_count.load(Ordering::Acquire);
|
||||
if completed >= self.total_tasks {
|
||||
if completed >= self.total_tasks {
|
||||
let mut signatures = Vec::new();
|
||||
let mut last_error = None;
|
||||
let mut any_success = false;
|
||||
let mut submit_timings = Vec::new();
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
if result.success {
|
||||
any_success = true;
|
||||
}
|
||||
@@ -139,7 +357,7 @@ impl ResultCollector {
|
||||
}
|
||||
}
|
||||
if !signatures.is_empty() {
|
||||
return Some((any_success, signatures, last_error));
|
||||
return Some((any_success, signatures, last_error, submit_timings));
|
||||
}
|
||||
return None;
|
||||
}
|
||||
@@ -151,13 +369,17 @@ impl ResultCollector {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_first(&self) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
fn get_first(
|
||||
&self,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
let mut signatures = Vec::new();
|
||||
let mut has_success = false;
|
||||
let mut last_error = None;
|
||||
|
||||
let mut submit_timings = Vec::new();
|
||||
|
||||
while let Some(result) = self.results.pop() {
|
||||
signatures.push(result.signature);
|
||||
submit_timings.push((result.swqos_type, result.submit_done_us));
|
||||
if result.success {
|
||||
has_success = true;
|
||||
}
|
||||
@@ -165,34 +387,56 @@ impl ResultCollector {
|
||||
last_error = result.error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !signatures.is_empty() {
|
||||
Some((has_success, signatures, last_error))
|
||||
Some((has_success, signatures, last_error, submit_timings))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 等待全部任务完成(不等待链上确认),然后收集并返回所有签名。用于「多路提交」时返回多笔签名。
|
||||
async fn wait_for_all_submitted(&self, timeout_secs: u64) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
/// 轮询间隔 2ms,避免 50ms 间隔在最后一笔返回时多等几十 ms 拉高 submit 耗时。
|
||||
async fn wait_for_all_submitted(
|
||||
&self,
|
||||
timeout_secs: u64,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
let start = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||
let poll_interval = std::time::Duration::from_millis(50);
|
||||
let primary = Duration::from_secs(timeout_secs);
|
||||
let poll_interval = Duration::from_millis(2);
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
if start.elapsed() > timeout {
|
||||
if start.elapsed() > primary {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
}
|
||||
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
|
||||
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
|
||||
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
|
||||
if all_submitted {
|
||||
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
|
||||
tokio::time::sleep(Duration::from_millis(35)).await;
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
if start.elapsed() > primary + Duration::from_secs(6) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(120)).await;
|
||||
}
|
||||
self.get_first()
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute trade on multiple SWQOS clients in parallel; returns success flag, all signatures, and last error.
|
||||
///
|
||||
/// `sender_config` merges sender_thread_cores, effective_core_ids, max_sender_concurrency (precomputed at SDK init; no get_core_ids on hot path).
|
||||
pub async fn execute_parallel(
|
||||
swqos_clients: &[Arc<SwqosClient>],
|
||||
payer: Arc<Keypair>,
|
||||
rpc: Option<Arc<SolanaRpcClient>>,
|
||||
rpc: Option<&Arc<SolanaRpcClient>>,
|
||||
instructions: Vec<Instruction>,
|
||||
address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
recent_blockhash: Option<Hash>,
|
||||
@@ -203,11 +447,10 @@ pub async fn execute_parallel(
|
||||
wait_transaction_confirmed: bool,
|
||||
with_tip: bool,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
use_core_affinity: bool,
|
||||
use_dedicated_sender_threads: bool,
|
||||
sender_config: SenderConcurrencyConfig,
|
||||
check_min_tip: bool,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
let _exec_start = Instant::now();
|
||||
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
if swqos_clients.is_empty() {
|
||||
return Err(anyhow!("swqos_clients is empty"));
|
||||
}
|
||||
@@ -221,48 +464,37 @@ pub async fn execute_parallel(
|
||||
return Err(anyhow!("No Rpc Default Swqos configured."));
|
||||
}
|
||||
|
||||
let cores = core_affinity::get_core_ids().unwrap_or_default();
|
||||
let instructions = Arc::new(instructions);
|
||||
|
||||
// Precompute all valid (client, gas config) combinations
|
||||
let task_configs: Vec<_> = swqos_clients
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, swqos_client)| {
|
||||
with_tip || matches!(swqos_client.get_swqos_type(), SwqosType::Default)
|
||||
})
|
||||
.flat_map(|(i, swqos_client)| {
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let gas_fee_strategy_configs = gas_fee_strategy.get_strategies(if is_buy {
|
||||
TradeType::Buy
|
||||
} else {
|
||||
TradeType::Sell
|
||||
});
|
||||
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
|
||||
let min_tip = if check_tip {
|
||||
swqos_client.min_tip_sol()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
gas_fee_strategy_configs
|
||||
.into_iter()
|
||||
.filter(move |config| config.0 == swqos_type)
|
||||
.filter(move |config| {
|
||||
if check_tip {
|
||||
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
||||
config.0, config.2.tip, min_tip
|
||||
);
|
||||
}
|
||||
config.2.tip >= min_tip
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.map(move |config| (i, swqos_client.clone(), config))
|
||||
})
|
||||
.collect();
|
||||
// One get_strategies call per batch (avoid N calls in loop).
|
||||
let gas_fee_configs =
|
||||
gas_fee_strategy.get_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
|
||||
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
|
||||
for (i, swqos_client) in swqos_clients.iter().enumerate() {
|
||||
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
|
||||
continue;
|
||||
}
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let check_tip = with_tip && !matches!(swqos_type, SwqosType::Default) && check_min_tip;
|
||||
let min_tip = if check_tip { swqos_client.min_tip_sol() } else { 0.0 };
|
||||
for config in &gas_fee_configs {
|
||||
if config.0 != swqos_type {
|
||||
continue;
|
||||
}
|
||||
if check_tip {
|
||||
if config.2.tip < min_tip && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(
|
||||
"⚠️ Config filtered: {:?} tip {} is below minimum required {}",
|
||||
config.0, config.2.tip, min_tip
|
||||
);
|
||||
}
|
||||
if config.2.tip < min_tip {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
task_configs.push((i, swqos_client.clone(), *config));
|
||||
}
|
||||
}
|
||||
|
||||
if task_configs.is_empty() {
|
||||
return Err(anyhow!("No available gas fee strategy configs"));
|
||||
@@ -272,126 +504,96 @@ pub async fn execute_parallel(
|
||||
return Err(anyhow!("Multiple swqos transactions require durable_nonce to be set.",));
|
||||
}
|
||||
|
||||
// Task preparation completed
|
||||
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
|
||||
let channel_count = task_configs.len().max(1);
|
||||
let collector = Arc::new(ResultCollector::new(channel_count));
|
||||
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
|
||||
let submit_timeout_secs: u64 =
|
||||
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
|
||||
let shared = Arc::new(SwqosSharedContext {
|
||||
payer,
|
||||
instructions,
|
||||
rpc: rpc.cloned(),
|
||||
address_lookup_table_account,
|
||||
recent_blockhash,
|
||||
durable_nonce,
|
||||
middleware_manager,
|
||||
protocol_name,
|
||||
is_buy,
|
||||
wait_transaction_confirmed,
|
||||
with_tip,
|
||||
collector: collector.clone(),
|
||||
});
|
||||
|
||||
let collector = Arc::new(ResultCollector::new(task_configs.len()));
|
||||
let _spawn_start = Instant::now();
|
||||
let (queue, notify) = if use_dedicated_sender_threads {
|
||||
ensure_dedicated_pool(
|
||||
sender_config.sender_thread_cores.as_ref().map(|a| a.as_slice()),
|
||||
sender_config.max_sender_concurrency,
|
||||
)
|
||||
} else {
|
||||
let q = SWQOS_QUEUE.get_or_init(|| Arc::new(ArrayQueue::new(SWQOS_QUEUE_CAP)));
|
||||
ensure_swqos_pool(q.clone(), sender_config.max_sender_concurrency);
|
||||
(q.clone(), SWQOS_NOTIFY.get_or_init(|| Arc::new(Notify::new())).clone())
|
||||
};
|
||||
|
||||
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
|
||||
let core_id = cores.get(i % cores.len().max(1)).copied();
|
||||
let use_affinity = use_core_affinity;
|
||||
let payer = payer.clone();
|
||||
let instructions = instructions.clone();
|
||||
let middleware_manager = middleware_manager.clone();
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let tip_account_str = swqos_client.get_tip_account()?;
|
||||
let tip_account = Arc::new(Pubkey::from_str(&tip_account_str).unwrap_or_default());
|
||||
let collector = collector.clone();
|
||||
|
||||
let tip = gas_fee_strategy_config.2.tip;
|
||||
let unit_limit = gas_fee_strategy_config.2.cu_limit;
|
||||
let unit_price = gas_fee_strategy_config.2.cu_price;
|
||||
let rpc = rpc.clone();
|
||||
let durable_nonce = durable_nonce.clone();
|
||||
let address_lookup_table_account = address_lookup_table_account.clone();
|
||||
let recent_blockhash_task = recent_blockhash.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _task_start = Instant::now();
|
||||
if use_affinity {
|
||||
if let Some(cid) = core_id {
|
||||
core_affinity::set_for_current(cid);
|
||||
{
|
||||
let effective_core_ids = sender_config.effective_core_ids.as_slice();
|
||||
let core_len = effective_core_ids.len().max(1);
|
||||
let mut tip_cache: FnvHashMap<*const (), Arc<Pubkey>> =
|
||||
FnvHashMap::with_capacity_and_hasher(task_configs.len(), BuildHasherDefault::default());
|
||||
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
|
||||
let core_id = effective_core_ids.get(i % core_len).copied();
|
||||
let swqos_type = swqos_client.get_swqos_type();
|
||||
let key = Arc::as_ptr(&swqos_client) as *const ();
|
||||
let tip_account = match tip_cache.get(&key) {
|
||||
Some(t) => t.clone(),
|
||||
None => {
|
||||
let s = swqos_client.get_tip_account()?;
|
||||
let tip = Arc::new(Pubkey::from_str(&s).unwrap_or_default());
|
||||
tip_cache.insert(key, tip.clone());
|
||||
tip
|
||||
}
|
||||
}
|
||||
|
||||
let tip_amount = if with_tip { tip } else { 0.0 };
|
||||
|
||||
let _build_start = Instant::now();
|
||||
let transaction = match build_transaction(
|
||||
payer,
|
||||
rpc,
|
||||
};
|
||||
let (tip, unit_limit, unit_price) = (
|
||||
gas_fee_strategy_config.2.tip,
|
||||
gas_fee_strategy_config.2.cu_limit,
|
||||
gas_fee_strategy_config.2.cu_price,
|
||||
);
|
||||
let job = SwqosJob {
|
||||
shared: shared.clone(),
|
||||
tip,
|
||||
unit_limit,
|
||||
unit_price,
|
||||
instructions.as_ref(),
|
||||
address_lookup_table_account,
|
||||
recent_blockhash_task,
|
||||
middleware_manager,
|
||||
protocol_name,
|
||||
is_buy,
|
||||
swqos_type != SwqosType::Default,
|
||||
&tip_account,
|
||||
tip_amount,
|
||||
durable_nonce,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tx) => tx,
|
||||
Err(e) => {
|
||||
// Build transaction failed
|
||||
collector.submit(TaskResult {
|
||||
success: false,
|
||||
signature: Signature::default(),
|
||||
error: Some(e),
|
||||
swqos_type,
|
||||
landed_on_chain: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Transaction built
|
||||
|
||||
let _send_start = Instant::now();
|
||||
let mut err: Option<anyhow::Error> = None;
|
||||
#[allow(unused_assignments)]
|
||||
let mut landed_on_chain = false;
|
||||
let success = match swqos_client
|
||||
.send_transaction(
|
||||
if is_buy { TradeType::Buy } else { TradeType::Sell },
|
||||
&transaction,
|
||||
wait_transaction_confirmed,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
landed_on_chain = true; // Success means tx confirmed on-chain
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
// Check if this error indicates the tx landed but failed (e.g., ExceededSlippage)
|
||||
landed_on_chain = is_landed_error(&e);
|
||||
err = Some(e);
|
||||
// Send transaction failed
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
let sig = transaction.signatures.first().copied().unwrap_or_default();
|
||||
collector.submit(TaskResult {
|
||||
success,
|
||||
signature: sig,
|
||||
error: err,
|
||||
tip_account,
|
||||
swqos_client,
|
||||
swqos_type,
|
||||
landed_on_chain,
|
||||
});
|
||||
});
|
||||
core_id,
|
||||
use_affinity: !effective_core_ids.is_empty(),
|
||||
};
|
||||
let _ = queue.push(job);
|
||||
}
|
||||
}
|
||||
|
||||
// All tasks spawned
|
||||
notify.notify_waiters();
|
||||
|
||||
// All jobs enqueued (no spawn on hot path)
|
||||
|
||||
if !wait_transaction_confirmed {
|
||||
const SUBMIT_TIMEOUT_SECS: u64 = 30;
|
||||
let (success, signatures, last_error) = collector
|
||||
.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS)
|
||||
.await
|
||||
.unwrap_or((false, vec![], Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS))));
|
||||
return Ok((success, signatures, last_error));
|
||||
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
|
||||
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
|
||||
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
|
||||
vec![],
|
||||
));
|
||||
let (success, signatures, last_error, submit_timings) = ret;
|
||||
return Ok((success, signatures, last_error, submit_timings));
|
||||
}
|
||||
|
||||
if let Some(result) = collector.wait_for_success().await {
|
||||
Ok(result)
|
||||
let (success, signatures, last_error, submit_timings) = result;
|
||||
Ok((success, signatures, last_error, submit_timings))
|
||||
} else {
|
||||
Err(anyhow!("All transactions failed"))
|
||||
}
|
||||
|
||||
@@ -2,16 +2,9 @@
|
||||
//! 执行模块:指令预处理、缓存预取、分支提示。
|
||||
|
||||
use anyhow::Result;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction,
|
||||
pubkey::Pubkey,
|
||||
signature::Keypair,
|
||||
};
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signature::Keypair};
|
||||
|
||||
use crate::perf::{
|
||||
hardware_optimizations::BranchOptimizer,
|
||||
simd::SIMDMemory,
|
||||
};
|
||||
use crate::perf::{hardware_optimizations::BranchOptimizer, simd::SIMDMemory};
|
||||
|
||||
/// Solana account key size in bytes (Pubkey). 每个账户(Pubkey)的字节数。
|
||||
pub const BYTES_PER_ACCOUNT: usize = 32;
|
||||
@@ -150,4 +143,4 @@ impl ExecutionPath {
|
||||
slow_path()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+199
-71
@@ -1,13 +1,19 @@
|
||||
use anyhow::Result;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
||||
signature::Keypair, signature::Signature,
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{sync::Arc, time::{Duration, Instant}};
|
||||
#[allow(unused_imports)]
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
use super::{params::SwapParams, traits::InstructionBuilder};
|
||||
use crate::swqos::SwqosType;
|
||||
use crate::swqos::TradeType;
|
||||
use crate::{
|
||||
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
|
||||
perf::syscall_bypass::SystemCallBypassManager,
|
||||
@@ -20,8 +26,6 @@ use crate::{
|
||||
trading::MiddlewareManager,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::swqos::TradeType;
|
||||
use super::{params::SwapParams, traits::InstructionBuilder};
|
||||
|
||||
/// Global syscall bypass manager (reserved for future time/IO optimizations).
|
||||
/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
|
||||
@@ -49,7 +53,10 @@ impl GenericTradeExecutor {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl TradeExecutor for GenericTradeExecutor {
|
||||
async fn swap(&self, params: SwapParams) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
|
||||
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
|
||||
let timing_start_us: Option<i64> = if params.log_enabled {
|
||||
@@ -58,7 +65,8 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
None
|
||||
};
|
||||
|
||||
let is_buy = params.trade_type == TradeType::Buy || params.trade_type == TradeType::CreateAndBuy;
|
||||
let is_buy =
|
||||
params.trade_type == TradeType::Buy || params.trade_type == TradeType::CreateAndBuy;
|
||||
|
||||
Prefetch::keypair(¶ms.payer);
|
||||
|
||||
@@ -69,7 +77,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
} else {
|
||||
self.instruction_builder.build_sell_instructions(¶ms).await?
|
||||
};
|
||||
let build_elapsed = build_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||
let _build_elapsed = build_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||
|
||||
InstructionProcessor::preprocess(&instructions)?;
|
||||
|
||||
@@ -77,13 +85,18 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
Some(middleware_manager) => middleware_manager
|
||||
.apply_middlewares_process_protocol_instructions(
|
||||
instructions,
|
||||
self.protocol_name.to_string(),
|
||||
self.protocol_name,
|
||||
is_buy,
|
||||
)?,
|
||||
None => instructions,
|
||||
};
|
||||
|
||||
let before_submit_elapsed = total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||
let build_end_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
|
||||
.then(crate::common::clock::now_micros);
|
||||
let _before_submit_elapsed =
|
||||
total_start.as_ref().map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||
let before_submit_us = (params.log_enabled && crate::common::sdk_log::sdk_log_enabled())
|
||||
.then(crate::common::clock::now_micros);
|
||||
|
||||
if params.simulate {
|
||||
let send_start = crate::common::sdk_log::sdk_log_enabled().then(Instant::now);
|
||||
@@ -106,18 +119,50 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
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);
|
||||
println!();
|
||||
if let (Some(start_us), Some(end_us)) = (timing_start_us, build_end_us) {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
(end_us - start_us) as f64 / 1000.0,
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
if let (Some(start_us), Some(end_us)) = (timing_start_us, before_submit_us) {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
(end_us - start_us) as f64 / 1000.0,
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
println!(
|
||||
" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
send_elapsed.as_secs_f64() * 1000.0,
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} total: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
total_elapsed.as_secs_f64() * 1000.0,
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let need_confirm = params.wait_transaction_confirmed;
|
||||
let send_start = params.log_enabled.then(Instant::now);
|
||||
let need_confirm = params.wait_tx_confirmed;
|
||||
let sender_config = params.sender_concurrency_config();
|
||||
let result = execute_parallel(
|
||||
¶ms.swqos_clients,
|
||||
params.swqos_clients.as_slice(),
|
||||
params.payer,
|
||||
params.rpc.clone(),
|
||||
params.rpc.as_ref(),
|
||||
final_instructions,
|
||||
params.address_lookup_table_account,
|
||||
params.recent_blockhash,
|
||||
@@ -128,64 +173,67 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
false, // submit only here; confirmation and log timing handled below
|
||||
if is_buy { true } else { params.with_tip },
|
||||
params.gas_fee_strategy,
|
||||
params.use_core_affinity,
|
||||
params.use_dedicated_sender_threads,
|
||||
sender_config,
|
||||
params.check_min_tip,
|
||||
)
|
||||
.await;
|
||||
let send_elapsed = send_start.map(|s| s.elapsed()).unwrap_or(Duration::ZERO);
|
||||
|
||||
if params.log_enabled && crate::common::sdk_log::sdk_log_enabled() {
|
||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||
let build_ms = build_elapsed.as_secs_f64() * 1000.0;
|
||||
let before_ms = before_submit_elapsed.as_secs_f64() * 1000.0;
|
||||
let send_ms = send_elapsed.as_secs_f64() * 1000.0;
|
||||
if let Some(start_us) = timing_start_us {
|
||||
let now_us = crate::common::clock::now_micros();
|
||||
let start_to_submit_us = (now_us - start_us).max(0);
|
||||
println!(" [SDK] {} timing(after_submit) build_instructions: {:.2}ms before_submit: {:.2}ms submit: {:.2}ms start_to_submit: {} μs", dir, build_ms, before_ms, send_ms, start_to_submit_us);
|
||||
} else {
|
||||
println!(" [SDK] {} timing(after_submit) build_instructions: {:.2}ms before_submit: {:.2}ms submit: {:.2}ms", dir, build_ms, before_ms, send_ms);
|
||||
let log_enabled = params.log_enabled && crate::common::sdk_log::sdk_log_enabled();
|
||||
|
||||
let (ok, signatures, err, submit_timings) = match result {
|
||||
Ok((success, sigs, last_error, timings)) => {
|
||||
(success, sigs, last_error.map(|e| anyhow::anyhow!("{}", e)), timings)
|
||||
}
|
||||
}
|
||||
Err(e) => (false, vec![], Some(anyhow::anyhow!("{}", e)), vec![]),
|
||||
};
|
||||
// submit_timings 为完成先后顺序(先完成的先 push),打印不排序、不增加延迟
|
||||
let submit_timings_ref: &[(crate::swqos::SwqosType, i64)] = submit_timings.as_slice();
|
||||
|
||||
let result = if need_confirm {
|
||||
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)
|
||||
if signatures.is_empty() {
|
||||
(ok, signatures, 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)),
|
||||
}
|
||||
let poll_res = poll_any_transaction_confirmation(rpc, &signatures, true).await;
|
||||
let confirm_done_us = log_enabled.then(crate::common::clock::now_micros);
|
||||
if log_enabled {
|
||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||
crate::common::sdk_log::print_sdk_timing_block(
|
||||
dir,
|
||||
timing_start_us,
|
||||
build_end_us,
|
||||
before_submit_us,
|
||||
submit_timings_ref,
|
||||
confirm_done_us,
|
||||
);
|
||||
}
|
||||
match poll_res {
|
||||
Ok(_) => (true, signatures, None),
|
||||
Err(e) => (false, signatures, Some(e)),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(ok, sigs, err)
|
||||
(ok, signatures, err)
|
||||
};
|
||||
Ok(confirm_result)
|
||||
|
||||
//就是把confirm_result 拆开 再加上 submit_timings
|
||||
Ok((confirm_result.0, confirm_result.1, confirm_result.2, submit_timings))
|
||||
} 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;
|
||||
// Not waiting for confirmation: confirmed is not measured (-); total is per-channel submit time only.
|
||||
if log_enabled {
|
||||
let dir = if is_buy { "Buy" } else { "Sell" };
|
||||
println!(" [SDK] {} timing total: {:.2}ms", dir, total_ms);
|
||||
crate::common::sdk_log::print_sdk_timing_block(
|
||||
dir,
|
||||
timing_start_us,
|
||||
build_end_us,
|
||||
before_submit_us,
|
||||
submit_timings_ref,
|
||||
None,
|
||||
);
|
||||
}
|
||||
result
|
||||
|
||||
Ok((ok, signatures, err, submit_timings))
|
||||
};
|
||||
|
||||
result
|
||||
@@ -210,7 +258,7 @@ async fn simulate_transaction(
|
||||
is_buy: bool,
|
||||
with_tip: bool,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
use crate::trading::common::build_transaction;
|
||||
use solana_client::rpc_config::RpcSimulateTransactionConfig;
|
||||
use solana_commitment_config::CommitmentLevel;
|
||||
@@ -232,22 +280,21 @@ async fn simulate_transaction(
|
||||
let unit_limit = default_config.2.cu_limit;
|
||||
let unit_price = default_config.2.cu_price;
|
||||
|
||||
// Build transaction for simulation
|
||||
let transaction = build_transaction(
|
||||
payer.clone(),
|
||||
Some(rpc.clone()),
|
||||
&payer,
|
||||
Some(&rpc),
|
||||
unit_limit,
|
||||
unit_price,
|
||||
&instructions,
|
||||
address_lookup_table_account,
|
||||
address_lookup_table_account.as_ref(),
|
||||
recent_blockhash,
|
||||
middleware_manager,
|
||||
middleware_manager.as_ref(),
|
||||
protocol_name,
|
||||
is_buy,
|
||||
false, // simulate doesn't need tip instruction
|
||||
false,
|
||||
&Pubkey::default(),
|
||||
tip,
|
||||
durable_nonce,
|
||||
durable_nonce.as_ref(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -257,14 +304,14 @@ async fn simulate_transaction(
|
||||
.simulate_transaction_with_config(
|
||||
&transaction,
|
||||
RpcSimulateTransactionConfig {
|
||||
sig_verify: false, // Don't verify signature during simulation for speed
|
||||
sig_verify: false, // Don't verify signature during simulation for speed
|
||||
replace_recent_blockhash: false, // Use actual blockhash from transaction
|
||||
commitment: Some(CommitmentConfig {
|
||||
commitment: CommitmentLevel::Processed, // Use Processed level to get latest state
|
||||
}),
|
||||
encoding: Some(UiTransactionEncoding::Base64), // Base64 encoding
|
||||
accounts: None, // Don't return specific account states (can be specified if needed)
|
||||
min_context_slot: None, // Don't specify minimum context slot
|
||||
accounts: None, // Don't return specific account states (can be specified if needed)
|
||||
min_context_slot: None, // Don't specify minimum context slot
|
||||
inner_instructions: true, // Enable inner instructions for debugging and detailed execution flow
|
||||
},
|
||||
)
|
||||
@@ -287,7 +334,7 @@ async fn simulate_transaction(
|
||||
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)), Vec::new()));
|
||||
}
|
||||
|
||||
// Simulation succeeded
|
||||
@@ -302,5 +349,86 @@ async fn simulate_transaction(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((true, vec![signature], None))
|
||||
Ok((true, vec![signature], None, Vec::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::swqos::SwqosType;
|
||||
|
||||
/// 运行 `cargo test -p sol-trade-sdk log_timing_preview -- --nocapture` 查看日志打印效果
|
||||
#[test]
|
||||
fn log_timing_preview() {
|
||||
let dir = "Buy";
|
||||
let build_ms = 12.34;
|
||||
let before_submit_ms = 15.67;
|
||||
let w = 12usize; // same as crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
println!("\n--- 1. 构建指令耗时 / 提交前耗时(各打印一次,统一 ms,保留 4 位小数)---\n");
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
build_ms,
|
||||
width = w
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
before_submit_ms,
|
||||
width = w
|
||||
);
|
||||
|
||||
println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
|
||||
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
|
||||
(SwqosType::Jito, 45.12, 83.38, 128.50),
|
||||
(SwqosType::Helius, 52.30, 76.20, 128.50),
|
||||
(SwqosType::ZeroSlot, 48.90, 79.60, 128.50),
|
||||
] {
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: {:.4} ms, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
dir,
|
||||
submit_ms,
|
||||
confirmed_ms,
|
||||
total_ms,
|
||||
width = w
|
||||
);
|
||||
}
|
||||
|
||||
println!(
|
||||
"\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n"
|
||||
);
|
||||
for (swqos_type, submit_ms, total_ms) in
|
||||
[(SwqosType::Jito, 44.20, 44.20), (SwqosType::Helius, 51.80, 51.80)]
|
||||
{
|
||||
println!(
|
||||
" [SDK][{:width$}] {} submit_done: {:.4} ms, confirmed: -, total: {:.4} ms",
|
||||
swqos_type.as_str(),
|
||||
dir,
|
||||
submit_ms,
|
||||
total_ms,
|
||||
width = w
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n--- 4. Simulate 模式(build/before_submit 仍从 grpc_recv_us 起算)---\n");
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
build_ms,
|
||||
width = w
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
before_submit_ms,
|
||||
width = w
|
||||
);
|
||||
println!(" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms", "-", dir, 8.50, width = w);
|
||||
println!(" [SDK][{:width$}] {} total: {:.4} ms", "-", dir, 36.51, width = w);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod async_executor;
|
||||
pub mod execution;
|
||||
pub mod executor;
|
||||
pub mod params;
|
||||
pub mod traits;
|
||||
pub mod executor;
|
||||
pub mod async_executor;
|
||||
pub mod transaction_pool;
|
||||
pub mod execution;
|
||||
@@ -1,775 +0,0 @@
|
||||
use crate::common::bonding_curve::BondingCurveAccount;
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||
use crate::constants::TOKEN_PROGRAM;
|
||||
use crate::instruction::utils::pumpfun::global_constants::MAYHEM_FEE_RECIPIENT;
|
||||
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
use crate::swqos::{SwqosClient, TradeType};
|
||||
use crate::trading::common::get_multi_token_balances;
|
||||
use crate::trading::MiddlewareManager;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::message::AddressLookupTableAccount;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
||||
#[derive(Clone)]
|
||||
pub enum DexParamEnum {
|
||||
PumpFun(PumpFunParams),
|
||||
PumpSwap(PumpSwapParams),
|
||||
Bonk(BonkParams),
|
||||
RaydiumCpmm(RaydiumCpmmParams),
|
||||
RaydiumAmmV4(RaydiumAmmV4Params),
|
||||
MeteoraDammV2(MeteoraDammV2Params),
|
||||
}
|
||||
|
||||
impl DexParamEnum {
|
||||
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
||||
#[inline]
|
||||
pub fn as_any(&self) -> &dyn std::any::Any {
|
||||
match self {
|
||||
DexParamEnum::PumpFun(p) => p,
|
||||
DexParamEnum::PumpSwap(p) => p,
|
||||
DexParamEnum::Bonk(p) => p,
|
||||
DexParamEnum::RaydiumCpmm(p) => p,
|
||||
DexParamEnum::RaydiumAmmV4(p) => p,
|
||||
DexParamEnum::MeteoraDammV2(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap parameters
|
||||
#[derive(Clone)]
|
||||
pub struct SwapParams {
|
||||
pub rpc: Option<Arc<SolanaRpcClient>>,
|
||||
pub payer: Arc<Keypair>,
|
||||
pub trade_type: TradeType,
|
||||
pub input_mint: Pubkey,
|
||||
pub input_token_program: Option<Pubkey>,
|
||||
pub output_mint: Pubkey,
|
||||
pub output_token_program: Option<Pubkey>,
|
||||
pub input_amount: Option<u64>,
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
pub wait_transaction_confirmed: bool,
|
||||
pub protocol_params: DexParamEnum,
|
||||
pub open_seed_optimize: bool,
|
||||
pub swqos_clients: Vec<Arc<SwqosClient>>,
|
||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
pub with_tip: bool,
|
||||
pub create_input_mint_ata: bool,
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
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,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||
/// 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.
|
||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||
pub use_exact_sol_amount: Option<bool>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SwapParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SwapParams: ...")
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
pub creator_vault: Pubkey,
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
pub fn immediate_sell(
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
close_token_account_when_sell: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
creator_vault: creator_vault,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
creator: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
bonding_curve,
|
||||
&mint,
|
||||
token_amount,
|
||||
max_sol_cost,
|
||||
creator,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
mint: Pubkey,
|
||||
creator: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
virtual_token_reserves: u64,
|
||||
virtual_sol_reserves: u64,
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
bonding_curve,
|
||||
mint,
|
||||
creator,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account =
|
||||
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
||||
let mint_account = rpc.get_account(&mint).await?;
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: account.1,
|
||||
virtual_token_reserves: account.0.virtual_token_reserves,
|
||||
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
||||
real_token_reserves: account.0.real_token_reserves,
|
||||
real_sol_reserves: account.0.real_sol_reserves,
|
||||
token_total_supply: account.0.token_total_supply,
|
||||
complete: account.0.complete,
|
||||
creator: account.0.creator,
|
||||
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(
|
||||
&bonding_curve.account,
|
||||
mint,
|
||||
&mint_account.owner,
|
||||
);
|
||||
let creator_vault =
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator);
|
||||
Ok(Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault.unwrap(),
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
/// token configuration, and transaction amounts.
|
||||
///
|
||||
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
||||
/// retrieve the relevant information from RPC, which will increase transaction time.
|
||||
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpSwapParams {
|
||||
/// Liquidity pool address
|
||||
pub pool: Pubkey,
|
||||
/// Base token mint address
|
||||
/// The mint account address of the base token in the trading pair
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
||||
pub quote_mint: Pubkey,
|
||||
/// Pool base token account
|
||||
pub pool_base_token_account: Pubkey,
|
||||
/// Pool quote token account
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
/// Token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Whether the pool is in mayhem mode
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Whether the pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
}
|
||||
|
||||
impl PumpSwapParams {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
Self {
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
||||
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
||||
/// lookups and associated latency. Token program IDs should be sourced from
|
||||
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
||||
/// differences.
|
||||
///
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
||||
/// from the event so that buy/sell instructions include the correct remaining
|
||||
/// accounts for cashback.
|
||||
pub fn from_trade(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
fee_recipient,
|
||||
is_cashback_coin,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(&pool_data, rpc).await?;
|
||||
let creator = pool_data.coin_creator;
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
&pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
&pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: pool_address.clone(),
|
||||
base_mint: pool_data.base_mint,
|
||||
quote_mint: pool_data.quote_mint,
|
||||
pool_base_token_account: pool_data.pool_base_token_account,
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves: pool_base_token_reserves,
|
||||
pool_quote_token_reserves: pool_quote_token_reserves,
|
||||
coin_creator_vault_ata: coin_creator_vault_ata,
|
||||
coin_creator_vault_authority: coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
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 {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Bonk protocol specific parameters
|
||||
/// Configuration parameters specific to Bonk trading protocol
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BonkParams {
|
||||
pub virtual_base: u128,
|
||||
pub virtual_quote: u128,
|
||||
pub real_base: u128,
|
||||
pub real_quote: u128,
|
||||
pub pool_state: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
/// Token program ID
|
||||
pub mint_token_program: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub platform_associated_account: Pubkey,
|
||||
pub creator_associated_account: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
}
|
||||
|
||||
impl BonkParams {
|
||||
pub fn immediate_sell(
|
||||
mint_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
mint_token_program,
|
||||
platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_trade(
|
||||
virtual_base: u64,
|
||||
virtual_quote: u64,
|
||||
real_base_after: u64,
|
||||
real_quote_after: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
virtual_base: virtual_base as u128,
|
||||
virtual_quote: virtual_quote as u128,
|
||||
real_base: real_base_after as u128,
|
||||
real_quote: real_quote_after as u128,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dev_trade(
|
||||
is_exact_in: bool,
|
||||
amount_in: u64,
|
||||
amount_out: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||
let _amount_in = if is_exact_in {
|
||||
amount_in
|
||||
} else {
|
||||
crate::instruction::utils::bonk::get_amount_in(
|
||||
amount_out,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
) as u128;
|
||||
let _amount_out = if is_exact_in {
|
||||
crate::instruction::utils::bonk::get_amount_out(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
) as u128
|
||||
} else {
|
||||
amount_out as u128
|
||||
};
|
||||
let real_base = _amount_out;
|
||||
Self {
|
||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||
real_base: real_base,
|
||||
real_quote: real_quote,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
usd1_pool: bool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||
mint,
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pool_data =
|
||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||
let platform_associated_account =
|
||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||
&pool_data.platform_config,
|
||||
);
|
||||
let creator_associated_account =
|
||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||
let platform_associated_account = platform_associated_account.unwrap();
|
||||
let creator_associated_account = creator_associated_account.unwrap();
|
||||
Ok(Self {
|
||||
virtual_base: pool_data.virtual_base as u128,
|
||||
virtual_quote: pool_data.virtual_quote as u128,
|
||||
real_base: pool_data.real_base as u128,
|
||||
real_quote: pool_data.real_quote as u128,
|
||||
pool_state: pool_address,
|
||||
base_vault: pool_data.base_vault,
|
||||
quote_vault: pool_data.quote_vault,
|
||||
mint_token_program: token_account.owner,
|
||||
platform_config: pool_data.platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config: pool_data.global_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumCpmmParams {
|
||||
/// Pool address
|
||||
pub pool_state: Pubkey,
|
||||
/// Amm config address
|
||||
pub amm_config: Pubkey,
|
||||
/// Base token mint address
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
pub quote_mint: Pubkey,
|
||||
/// Base token reserve amount in the pool
|
||||
pub base_reserve: u64,
|
||||
/// Quote token reserve amount in the pool
|
||||
pub quote_reserve: u64,
|
||||
/// Base token vault address
|
||||
pub base_vault: Pubkey,
|
||||
/// Quote token vault address
|
||||
pub quote_vault: Pubkey,
|
||||
/// Base token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Observation state account
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
impl RaydiumCpmmParams {
|
||||
pub fn from_trade(
|
||||
pool_state: Pubkey,
|
||||
amm_config: Pubkey,
|
||||
input_token_mint: Pubkey,
|
||||
output_token_mint: Pubkey,
|
||||
input_vault: Pubkey,
|
||||
output_vault: Pubkey,
|
||||
input_token_program: Pubkey,
|
||||
output_token_program: Pubkey,
|
||||
observation_state: Pubkey,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool_state: pool_state,
|
||||
amm_config: amm_config,
|
||||
base_mint: input_token_mint,
|
||||
quote_mint: output_token_mint,
|
||||
base_reserve: base_reserve,
|
||||
quote_reserve: quote_reserve,
|
||||
base_vault: input_vault,
|
||||
quote_vault: output_vault,
|
||||
base_token_program: input_token_program,
|
||||
quote_token_program: output_token_program,
|
||||
observation_state: observation_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool =
|
||||
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
||||
let (token0_balance, token1_balance) =
|
||||
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
||||
rpc,
|
||||
pool_address,
|
||||
&pool.token0_mint,
|
||||
&pool.token1_mint,
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
pool_state: pool_address.clone(),
|
||||
amm_config: pool.amm_config,
|
||||
base_mint: pool.token0_mint,
|
||||
quote_mint: pool.token1_mint,
|
||||
base_reserve: token0_balance,
|
||||
quote_reserve: token1_balance,
|
||||
base_vault: pool.token0_vault,
|
||||
quote_vault: pool.token1_vault,
|
||||
base_token_program: pool.token0_program,
|
||||
quote_token_program: pool.token1_program,
|
||||
observation_state: pool.observation_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumAmmV4Params {
|
||||
/// AMM pool address
|
||||
pub amm: Pubkey,
|
||||
/// Base token (coin) mint address
|
||||
pub coin_mint: Pubkey,
|
||||
/// Quote token (pc) mint address
|
||||
pub pc_mint: Pubkey,
|
||||
/// Pool's coin token account address
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
pub pc_reserve: u64,
|
||||
}
|
||||
|
||||
impl RaydiumAmmV4Params {
|
||||
pub fn new(
|
||||
amm: Pubkey,
|
||||
coin_mint: Pubkey,
|
||||
pc_mint: Pubkey,
|
||||
token_coin: Pubkey,
|
||||
token_pc: Pubkey,
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
}
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
amm,
|
||||
coin_mint: amm_info.coin_mint,
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// MeteoraDammV2 protocol specific parameters
|
||||
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct MeteoraDammV2Params {
|
||||
pub pool: Pubkey,
|
||||
pub token_a_vault: Pubkey,
|
||||
pub token_b_vault: Pubkey,
|
||||
pub token_a_mint: Pubkey,
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
token_a_vault: Pubkey,
|
||||
token_b_vault: Pubkey,
|
||||
token_a_mint: Pubkey,
|
||||
token_b_mint: Pubkey,
|
||||
token_a_program: Pubkey,
|
||||
token_b_program: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
token_a_vault,
|
||||
token_b_vault,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data =
|
||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||
Ok(Self {
|
||||
pool: pool_address.clone(),
|
||||
token_a_vault: pool_data.token_a_vault,
|
||||
token_b_vault: pool_data.token_b_vault,
|
||||
token_a_mint: pool_data.token_a_mint,
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program: TOKEN_PROGRAM,
|
||||
token_b_program: TOKEN_PROGRAM,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Bonk protocol specific parameters
|
||||
/// Configuration parameters specific to Bonk trading protocol
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BonkParams {
|
||||
pub virtual_base: u128,
|
||||
pub virtual_quote: u128,
|
||||
pub real_base: u128,
|
||||
pub real_quote: u128,
|
||||
pub pool_state: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
/// Token program ID
|
||||
pub mint_token_program: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub platform_associated_account: Pubkey,
|
||||
pub creator_associated_account: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
}
|
||||
|
||||
impl BonkParams {
|
||||
pub fn immediate_sell(
|
||||
mint_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
mint_token_program,
|
||||
platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_trade(
|
||||
virtual_base: u64,
|
||||
virtual_quote: u64,
|
||||
real_base_after: u64,
|
||||
real_quote_after: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
virtual_base: virtual_base as u128,
|
||||
virtual_quote: virtual_quote as u128,
|
||||
real_base: real_base_after as u128,
|
||||
real_quote: real_quote_after as u128,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dev_trade(
|
||||
is_exact_in: bool,
|
||||
amount_in: u64,
|
||||
amount_out: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||
let _amount_in = if is_exact_in {
|
||||
amount_in
|
||||
} else {
|
||||
crate::instruction::utils::bonk::get_amount_in(
|
||||
amount_out,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
) as u128;
|
||||
let _amount_out = if is_exact_in {
|
||||
crate::instruction::utils::bonk::get_amount_out(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
) as u128
|
||||
} else {
|
||||
amount_out as u128
|
||||
};
|
||||
let real_base = _amount_out;
|
||||
Self {
|
||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||
real_base: real_base,
|
||||
real_quote: real_quote,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
usd1_pool: bool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||
mint,
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pool_data =
|
||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||
let platform_associated_account =
|
||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||
&pool_data.platform_config,
|
||||
);
|
||||
let creator_associated_account =
|
||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||
let platform_associated_account = platform_associated_account.unwrap();
|
||||
let creator_associated_account = creator_associated_account.unwrap();
|
||||
Ok(Self {
|
||||
virtual_base: pool_data.virtual_base as u128,
|
||||
virtual_quote: pool_data.virtual_quote as u128,
|
||||
real_base: pool_data.real_base as u128,
|
||||
real_quote: pool_data.real_quote as u128,
|
||||
pool_state: pool_address,
|
||||
base_vault: pool_data.base_vault,
|
||||
quote_vault: pool_data.quote_vault,
|
||||
mint_token_program: token_account.owner,
|
||||
platform_config: pool_data.platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config: pool_data.global_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user