Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a391539000 | |||
| c75ca4b034 | |||
| b13b4fda0a | |||
| 4dec087ea6 | |||
| 9c9ecf3e5b | |||
| 4b451af5ff | |||
| 8f2f99f3d9 | |||
| 971ef41fad | |||
| 35bfa93516 | |||
| c8f9f9f6aa | |||
| 99846a21c2 | |||
| 062c5415c3 | |||
| eaa3214e4d | |||
| ae890ad976 | |||
| e10ee0de3b | |||
| 5e1fb1a66b | |||
| a5ea26afaf | |||
| ee4a8f685b | |||
| 322acf8baf | |||
| 378b8fc324 |
+21
-19
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.2"
|
||||
version = "4.0.3"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -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"
|
||||
|
||||
@@ -61,6 +61,17 @@
|
||||
|
||||
---
|
||||
|
||||
## 📦 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
|
||||
@@ -90,14 +101,14 @@ Add the dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.2" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "4.0.2"
|
||||
sol-trade-sdk = "4.0.3"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -130,11 +141,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
), // QUIC
|
||||
];
|
||||
// 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 protection (Astralane port 9000 / BlockRazor sandwichMitigation)
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
+21
-7
@@ -61,6 +61,17 @@
|
||||
|
||||
---
|
||||
|
||||
## 📦 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 交易**: 支持`购买`、`卖出`功能
|
||||
@@ -90,14 +101,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.2" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "4.0.2"
|
||||
sol-trade-sdk = "4.0.3"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -130,11 +141,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
), // QUIC
|
||||
];
|
||||
// 创建 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 端口 9000 / BlockRazor sandwichMitigation)
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
@@ -109,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)
|
||||
@@ -157,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,
|
||||
|
||||
@@ -113,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)
|
||||
|
||||
@@ -81,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)
|
||||
|
||||
@@ -1392,7 +1392,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -63,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)
|
||||
|
||||
@@ -108,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)
|
||||
@@ -153,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,
|
||||
|
||||
@@ -114,7 +114,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -150,6 +157,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -199,6 +207,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -104,7 +104,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
.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)
|
||||
}
|
||||
|
||||
@@ -140,6 +147,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -183,6 +191,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -104,7 +104,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)
|
||||
|
||||
@@ -133,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)
|
||||
|
||||
@@ -109,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -58,11 +58,10 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
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;
|
||||
|
||||
@@ -84,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)
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
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)?;
|
||||
|
||||
// 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: lookup_table.addresses.to_vec(),
|
||||
addresses,
|
||||
};
|
||||
Ok(address_lookup_table_account)
|
||||
}
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@ use anyhow::anyhow;
|
||||
use fnv::FnvHasher;
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction::create_account_with_seed;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::hash::Hasher;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -92,9 +92,10 @@ 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)
|
||||
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
|
||||
// 否则创建的账户地址与 ata_like 不匹配,导致 initializeAccount3 失败
|
||||
let create_acc =
|
||||
create_account_with_seed(payer, &ata_like, owner, seed, rent, len, token_program);
|
||||
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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,14 @@ 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(
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use solana_program::pubkey;
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
+115
-22
@@ -11,6 +11,10 @@ pub struct InfrastructureConfig {
|
||||
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 port 9000, BlockRazor revert_protection) will use their MEV-protected
|
||||
/// endpoints/modes. Default false.
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
|
||||
impl InfrastructureConfig {
|
||||
@@ -24,6 +28,7 @@ impl InfrastructureConfig {
|
||||
swqos_configs,
|
||||
commitment,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +39,7 @@ impl InfrastructureConfig {
|
||||
swqos_configs: config.swqos_configs.clone(),
|
||||
commitment: config.commitment.clone(),
|
||||
swqos_cores_from_end: config.swqos_cores_from_end,
|
||||
mev_protection: config.mev_protection,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +59,7 @@ impl Hash for InfrastructureConfig {
|
||||
self.swqos_configs.hash(state);
|
||||
format!("{:?}", self.commitment).hash(state);
|
||||
self.swqos_cores_from_end.hash(state);
|
||||
self.mev_protection.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +69,7 @@ impl PartialEq for InfrastructureConfig {
|
||||
&& 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,52 +91,137 @@ pub struct TradeConfig {
|
||||
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 port 9000, BlockRazor sandwichMitigation mode) will use their
|
||||
/// MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||
pub mev_protection: 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,
|
||||
}
|
||||
|
||||
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
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Use the *last* N cores for SWQOS (instead of the first N). Call this when the main thread or tokio workers use low-numbered cores to avoid binding SWQOS to busy cores. Default false.
|
||||
pub fn with_swqos_cores_from_end(mut self, from_end: bool) -> Self {
|
||||
self.swqos_cores_from_end = from_end;
|
||||
/// 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` (MEV-protected endpoint)
|
||||
/// - **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
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
|
||||
|
||||
@@ -152,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] = &[
|
||||
@@ -317,6 +323,19 @@ pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
|
||||
"lim.gateway.astralane.io:7000", // Default
|
||||
];
|
||||
|
||||
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
|
||||
/// Use these when mev_protection=true to route through Astralane's MEV-protected path.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 8] = [
|
||||
"ny.gateway.astralane.io:9000", // NewYork
|
||||
"fr.gateway.astralane.io:9000", // Frankfurt
|
||||
"ams.gateway.astralane.io:9000", // Amsterdam
|
||||
"lim.gateway.astralane.io:9000", // SLC (no slc, use lim)
|
||||
"sg.gateway.astralane.io:9000", // Tokyo (Asia)
|
||||
"ams.gateway.astralane.io:9000", // London (Europe)
|
||||
"la.gateway.astralane.io:9000", // LosAngeles
|
||||
"lim.gateway.astralane.io:9000", // Default
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
|
||||
+54
-50
@@ -8,8 +8,9 @@ use crate::{
|
||||
};
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::{
|
||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
|
||||
get_mayhem_fee_recipient_meta_random, get_user_volume_accumulator_pda,
|
||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
|
||||
get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta,
|
||||
resolve_creator_vault_for_ix,
|
||||
global_constants::{self},
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
},
|
||||
@@ -42,8 +43,20 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
}
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
let creator_vault_pda = protocol_params.creator_vault;
|
||||
let creator = get_creator(&creator_vault_pda);
|
||||
// creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived;
|
||||
// if stream sends a mismatched vault (wrong token / stale), fall back to derived.
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = resolve_creator_vault_for_ix(
|
||||
&creator,
|
||||
protocol_params.creator_vault,
|
||||
¶ms.output_mint,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"creator_vault PDA derivation failed (creator={})",
|
||||
creator
|
||||
)
|
||||
})?;
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
@@ -64,13 +77,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
|
||||
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() {
|
||||
get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
||||
})?
|
||||
} else {
|
||||
bonding_curve.account
|
||||
};
|
||||
// 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子,
|
||||
// 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
|
||||
// Determine token program based on mayhem mode
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
@@ -82,15 +93,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
};
|
||||
|
||||
let associated_bonding_curve =
|
||||
if protocol_params.associated_bonding_curve == Pubkey::default() {
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&bonding_curve_addr,
|
||||
¶ms.output_mint,
|
||||
&token_program,
|
||||
)
|
||||
} else {
|
||||
protocol_params.associated_bonding_curve
|
||||
};
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&bonding_curve_addr,
|
||||
¶ms.output_mint,
|
||||
&token_program,
|
||||
);
|
||||
|
||||
let user_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
@@ -106,6 +113,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
// Hot path: no RPC here (latency). For legacy curves <151 bytes, use
|
||||
// `extend_bonding_curve_account_instruction` from a cold path or separate tx.
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
|
||||
// Create associated token account
|
||||
@@ -142,12 +151,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
buy_data[24..26].copy_from_slice(&track_volume);
|
||||
}
|
||||
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
global_constants::FEE_RECIPIENT_META
|
||||
};
|
||||
// Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
|
||||
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
|
||||
@@ -197,8 +203,18 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
};
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
let creator_vault_pda = protocol_params.creator_vault;
|
||||
let creator = get_creator(&creator_vault_pda);
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = resolve_creator_vault_for_ix(
|
||||
&creator,
|
||||
protocol_params.creator_vault,
|
||||
¶ms.input_mint,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"creator_vault PDA derivation failed (creator={})",
|
||||
creator
|
||||
)
|
||||
})?;
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
@@ -218,13 +234,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
),
|
||||
};
|
||||
|
||||
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() {
|
||||
get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
|
||||
})?
|
||||
} else {
|
||||
bonding_curve.account
|
||||
};
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
|
||||
})?;
|
||||
|
||||
// Determine token program based on mayhem mode
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
@@ -236,15 +248,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
};
|
||||
|
||||
let associated_bonding_curve =
|
||||
if protocol_params.associated_bonding_curve == Pubkey::default() {
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&bonding_curve_addr,
|
||||
¶ms.input_mint,
|
||||
&token_program,
|
||||
)
|
||||
} else {
|
||||
protocol_params.associated_bonding_curve
|
||||
};
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
|
||||
&bonding_curve_addr,
|
||||
¶ms.input_mint,
|
||||
&token_program,
|
||||
);
|
||||
|
||||
let user_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
@@ -264,12 +272,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
|
||||
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
|
||||
let fee_recipient_meta = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
global_constants::FEE_RECIPIENT_META
|
||||
};
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
|
||||
let mut accounts: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
|
||||
@@ -139,8 +139,18 @@ 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 {
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
// --- Aligned with official `@pump-fun/pump-sdk` (npm) ---
|
||||
// - `src/fees.ts` `getFeeRecipient(global, mayhemMode)` — fee recipient pools
|
||||
// - `src/bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient`
|
||||
// - `src/sdk.ts` `BONDING_CURVE_NEW_SIZE` (151) + `extendAccountInstruction` — **not** called from the
|
||||
// trade hot path here (no RPC in `PumpFunInstructionBuilder`); use these helpers from a cold path if needed.
|
||||
|
||||
/// Minimum bonding curve account data length after protocol upgrades (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
/// Anchor discriminator for `extend_account` (`pump.json`); same as `PumpSdk.extendAccountInstruction`.
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
/// Build `extend_account` for bonding curve (cold path / separate tx only — do not add RPC to hot-path builds).
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(bonding_curve: &Pubkey, user: &Pubkey) -> Instruction {
|
||||
Instruction {
|
||||
program_id: accounts::PUMPFUN,
|
||||
accounts: vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
data: EXTEND_ACCOUNT_DISCRIMINATOR.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
/// Seed for bonding curve PDAs
|
||||
@@ -24,6 +55,9 @@ pub mod seeds {
|
||||
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 (`@pump-fun/pump-sdk` `feeSharingConfigPda`)
|
||||
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
|
||||
}
|
||||
|
||||
pub mod global_constants {
|
||||
@@ -172,7 +206,26 @@ 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];
|
||||
|
||||
/// Returns a random Mayhem fee recipient AccountMeta (pump-public-docs: Bonding Curve 2nd account = Mayhem fee recipient; use any one randomly).
|
||||
/// Check if a pubkey is one of the Mayhem fee recipients
|
||||
#[inline]
|
||||
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.iter().any(|p| p == pubkey)
|
||||
}
|
||||
|
||||
/// Check if a pubkey is a Pump.fun AMM protocol fee recipient (PUMPFUN_AMM_FEE_1..7)
|
||||
#[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
|
||||
}
|
||||
|
||||
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
|
||||
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
@@ -181,6 +234,46 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
/// Non-mayhem: random among `Global::fee_recipient` + `Global::fee_recipients[0..7]`.
|
||||
/// Same pubkey set as `bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient` and `fees.ts` `getFeeRecipient` when `mayhemMode === false`.
|
||||
#[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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
|
||||
if from_stream != Pubkey::default() {
|
||||
AccountMeta {
|
||||
pubkey: from_stream,
|
||||
is_signer: false,
|
||||
is_writable: true,
|
||||
}
|
||||
} else if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
get_standard_fee_recipient_meta_random()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Symbol;
|
||||
|
||||
impl Symbol {
|
||||
@@ -242,6 +335,76 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|
||||
)
|
||||
}
|
||||
|
||||
/// `feeSharingConfig` PDA per mint (`pump-sdk` `feeSharingConfigPda` → `pump-fees` program).
|
||||
#[inline]
|
||||
pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
}
|
||||
|
||||
/// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when
|
||||
/// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006.
|
||||
#[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()
|
||||
}
|
||||
|
||||
/// Resolve `creator_vault` for Pump buy/sell account #10.
|
||||
///
|
||||
/// - If `creator` is **missing** in the outer trade-event borsh (`Pubkey::default()`) but
|
||||
/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9),
|
||||
/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache).
|
||||
/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`).
|
||||
/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix).
|
||||
/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use
|
||||
/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right).
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Option<Pubkey> {
|
||||
let phantom = phantom_default_creator_vault();
|
||||
|
||||
if *creator == Pubkey::default() {
|
||||
if creator_vault_from_event == Pubkey::default() {
|
||||
return None;
|
||||
}
|
||||
if creator_vault_from_event == phantom {
|
||||
return None;
|
||||
}
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
|
||||
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
|
||||
if creator_vault_from_event == phantom {
|
||||
return get_creator_vault_pda(creator);
|
||||
}
|
||||
|
||||
let v_derived = get_creator_vault_pda(creator)?;
|
||||
if creator_vault_from_event == Pubkey::default() {
|
||||
return Some(v_derived);
|
||||
}
|
||||
if creator_vault_from_event == v_derived {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
|
||||
let v_sharing = get_creator_vault_pda(&sharing)?;
|
||||
if creator_vault_from_event == v_sharing {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
}
|
||||
Some(v_derived)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -322,4 +485,55 @@ mod tests {
|
||||
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_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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ use rand::seq::IndexedRandom;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
|
||||
/// PumpSwap 池账户总长度(见 pump-public-docs Breaking Change):8 字节 discriminator + 244 字节 Pool。
|
||||
/// 官方文档:pool structure needs to be 244 bytes (was 243),含 is_mayhem_mode。DataSize 必须与此一致,否则 getProgramAccounts 会返回 0。
|
||||
const POOL_ACCOUNT_DATA_LEN: u64 = 8 + 244;
|
||||
// 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 {
|
||||
@@ -301,116 +299,90 @@ 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.
|
||||
// base_mint 在账户布局中的偏移:8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let filters = vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, base_mint.as_ref()),
|
||||
),
|
||||
];
|
||||
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,
|
||||
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,
|
||||
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?;
|
||||
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 first = &pools[0];
|
||||
Ok((first.0, first.1.clone()))
|
||||
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.
|
||||
// quote_mint 在账户布局中的偏移:8 + 1 + 2 + 32 + 32 = 75
|
||||
let filters = vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(75, quote_mint.as_ref()),
|
||||
),
|
||||
];
|
||||
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 first = &pools[0];
|
||||
Ok((first.0, first.1.clone()))
|
||||
Ok((pools[0].0, pools[0].1.clone()))
|
||||
}
|
||||
|
||||
/// 按 mint 查找 PumpSwap 池(本函数仅用于 PumpSwap,其他 DEX 勿用)。
|
||||
@@ -444,20 +416,24 @@ pub async fn find_by_mint(
|
||||
Err(e) => diag.push(format!("canonical get_account/decode 失败: {}", e)),
|
||||
}
|
||||
|
||||
// 3. 回退:getProgramAccounts 按 base_mint / quote_mint
|
||||
match find_by_base_mint(rpc, mint).await {
|
||||
Ok((address, pool)) => return Ok((address, pool)),
|
||||
Err(e) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
|
||||
// 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 find_by_quote_mint(rpc, mint).await {
|
||||
Ok((address, pool)) => return Ok((address, pool)),
|
||||
Err(e) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
|
||||
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 {}. 诊断: {}。若使用自建 RPC 请确认已开启 getProgramAccounts 或换用公共 RPC 重试;若代币未在 PumpSwap 建池请先在 pump.fun/DEX 上确认",
|
||||
"No pool found for mint {}. diag: {}",
|
||||
mint,
|
||||
diag.join("; ")
|
||||
diag_str
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
+8
-7
@@ -152,6 +152,7 @@ impl TradingInfrastructure {
|
||||
config.rpc_url.clone(),
|
||||
config.commitment.clone(),
|
||||
swqos.clone(),
|
||||
config.mev_protection,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -740,7 +741,7 @@ impl TradingClient {
|
||||
pub async fn buy(
|
||||
&self,
|
||||
params: TradeBuyParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
|
||||
if params.recent_blockhash.is_none() && params.durable_nonce.is_none() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)"
|
||||
@@ -814,8 +815,8 @@ impl TradingClient {
|
||||
|
||||
let swap_result = executor.swap(buy_params).await;
|
||||
let result =
|
||||
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
|
||||
return result;
|
||||
swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings));
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute a sell order for a specified token
|
||||
@@ -847,7 +848,7 @@ impl TradingClient {
|
||||
pub async fn sell(
|
||||
&self,
|
||||
params: TradeSellParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
|
||||
#[cfg(feature = "perf-trace")]
|
||||
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
|
||||
debug!(
|
||||
@@ -921,8 +922,8 @@ impl TradingClient {
|
||||
|
||||
let swap_result = executor.swap(sell_params).await;
|
||||
let result =
|
||||
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
|
||||
return result;
|
||||
swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings));
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute a sell order for a percentage of the specified token amount
|
||||
@@ -956,7 +957,7 @@ impl TradingClient {
|
||||
mut params: TradeSellParams,
|
||||
amount_token: u64,
|
||||
percent: u64,
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
|
||||
if percent == 0 || percent > 100 {
|
||||
return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
|
||||
}
|
||||
|
||||
+10
-10
@@ -52,32 +52,32 @@ impl AstralaneQuicClient {
|
||||
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)), 7000),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), 7000),
|
||||
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)), 7000)]
|
||||
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)), 7000),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), 7000),
|
||||
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)), 7000)]
|
||||
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)), 7000)]
|
||||
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)), 7000)]
|
||||
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)), 7000)]
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)]
|
||||
}
|
||||
_ => {
|
||||
let _ = port; // keep signature consistent; fall back to DNS below.
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
+91
-14
@@ -7,6 +7,7 @@ use std::{sync::Arc, time::Instant};
|
||||
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
@@ -85,9 +86,11 @@ pub enum BlockRazorBackend {
|
||||
Grpc {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
grpc_client: Arc<BlockRazorGrpcClient>,
|
||||
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,
|
||||
@@ -95,6 +98,8 @@ pub enum BlockRazorBackend {
|
||||
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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -143,10 +148,10 @@ impl SwqosClientTrait for BlockRazorClient {
|
||||
impl BlockRazorClient {
|
||||
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))
|
||||
Ok(Self::new_http(rpc_url, endpoint, auth_token, false))
|
||||
}
|
||||
|
||||
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
|
||||
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);
|
||||
|
||||
// 配置 Channel,增加连接超时
|
||||
@@ -157,7 +162,10 @@ impl BlockRazorClient {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
|
||||
|
||||
let grpc_client = Arc::new(BlockRazorGrpcClient::new(channel, auth_token.clone()));
|
||||
let 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));
|
||||
|
||||
@@ -169,6 +177,7 @@ impl BlockRazorClient {
|
||||
grpc_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
mev_protection,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,7 +189,7 @@ impl BlockRazorClient {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
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));
|
||||
@@ -194,6 +203,7 @@ impl BlockRazorClient {
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
mev_protection,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -211,27 +221,68 @@ impl BlockRazorClient {
|
||||
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 {
|
||||
if let Err(e) = grpc_client.get_health().await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC ping request failed: {}", e);
|
||||
let mut 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;
|
||||
}
|
||||
if let Err(e) = grpc_client.get_health().await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC ping request failed: {}", e);
|
||||
|
||||
// 健康检查(使用 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +300,7 @@ impl BlockRazorClient {
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
..
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
@@ -307,6 +359,18 @@ impl BlockRazorClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重新建立 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,
|
||||
@@ -318,14 +382,19 @@ impl BlockRazorClient {
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc {
|
||||
grpc_client,
|
||||
mev_protection,
|
||||
..
|
||||
} => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let signature = grpc_client.send_transaction(
|
||||
// 使用 load() 无锁获取客户端引用
|
||||
let client = grpc_client.load();
|
||||
let signature = client.send_transaction(
|
||||
content,
|
||||
"fast".to_string(),
|
||||
// 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;
|
||||
@@ -354,14 +423,22 @@ impl BlockRazorClient {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
mev_protection,
|
||||
..
|
||||
} => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let mut 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(&[("auth", auth_token.as_str())])
|
||||
.query(&query_params)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(content)
|
||||
.send()
|
||||
|
||||
+22
-13
@@ -22,14 +22,14 @@ 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_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
SWQOS_ENDPOINTS_ASTRALANE, 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,
|
||||
@@ -51,11 +51,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Reserved for future per-SWQOS tip account caching (currently unused).
|
||||
#[allow(dead_code)]
|
||||
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
|
||||
@@ -303,6 +300,7 @@ impl SwqosConfig {
|
||||
region: SwqosRegion,
|
||||
url: Option<String>,
|
||||
transport: Option<SwqosTransport>,
|
||||
mev_protection: bool,
|
||||
) -> String {
|
||||
if let Some(custom_url) = url {
|
||||
return custom_url;
|
||||
@@ -329,12 +327,17 @@ impl SwqosConfig {
|
||||
SwqosType::Astralane => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
// MEV protection: port 9000; standard: port 7000
|
||||
if mev_protection {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
}
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
_ => Self::get_endpoint(swqos_type, region, url),
|
||||
_ => Self::get_endpoint(swqos_type, region, None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +345,7 @@ impl SwqosConfig {
|
||||
rpc_url: String,
|
||||
commitment: CommitmentConfig,
|
||||
swqos_config: SwqosConfig,
|
||||
mev_protection: bool,
|
||||
) -> Result<Arc<SwqosClient>> {
|
||||
match swqos_config {
|
||||
SwqosConfig::Jito(auth_token, region, url) => {
|
||||
@@ -398,15 +402,15 @@ impl SwqosConfig {
|
||||
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);
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport, mev_protection);
|
||||
if use_http {
|
||||
let blockrazor_client =
|
||||
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
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).await?;
|
||||
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection).await?;
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
}
|
||||
}
|
||||
@@ -414,7 +418,12 @@ impl SwqosConfig {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
// MEV protection: port 9000; standard: port 7000
|
||||
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)
|
||||
|
||||
+10
-10
@@ -51,7 +51,7 @@ pub mod server_client {
|
||||
}
|
||||
impl<T> ServerClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||
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,
|
||||
@@ -72,13 +72,13 @@ pub mod server_client {
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
ServerClient::new(InterceptedService::new(inner, interceptor))
|
||||
@@ -126,7 +126,7 @@ pub mod server_client {
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/serverpb.Server/SendTransaction",
|
||||
);
|
||||
@@ -147,7 +147,7 @@ pub mod server_client {
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/serverpb.Server/GetHealth",
|
||||
);
|
||||
@@ -244,7 +244,7 @@ pub mod server_server {
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Response = http::Response<tonic::body::BoxBody>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
@@ -283,7 +283,7 @@ pub mod server_server {
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = SendTransactionSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
@@ -326,7 +326,7 @@ pub mod server_server {
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetHealthSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
@@ -344,7 +344,7 @@ pub mod server_server {
|
||||
_ => {
|
||||
Box::pin(async move {
|
||||
let mut response = http::Response::new(
|
||||
tonic::body::Body::default(),
|
||||
tonic::body::BoxBody::default(),
|
||||
);
|
||||
let headers = response.headers_mut();
|
||||
headers
|
||||
|
||||
+61
-2
@@ -8,7 +8,6 @@ use quinn::{
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
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 +24,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]
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dummy self-signed certificate using rcgen
|
||||
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
|
||||
// Generate a new key pair for the certificate
|
||||
let key_pair = rcgen::KeyPair::generate()?;
|
||||
|
||||
let params = rcgen::CertificateParams::default();
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
|
||||
|
||||
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);
|
||||
@@ -43,7 +102,7 @@ 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 (cert, key) = generate_self_signed_cert(&keypair)?;
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
|
||||
@@ -7,7 +7,6 @@ use quinn::{
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
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 _},
|
||||
@@ -26,6 +25,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]
|
||||
}
|
||||
}
|
||||
|
||||
// Generate dummy self-signed certificate using rcgen
|
||||
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
|
||||
// Generate a new key pair for the certificate
|
||||
let key_pair = rcgen::KeyPair::generate()?;
|
||||
|
||||
let params = rcgen::CertificateParams::default();
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
|
||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
||||
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
|
||||
|
||||
Ok((cert_der, key_der))
|
||||
}
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
/// Fallback SNI when endpoint is IP or cannot extract host (keeps legacy behavior).
|
||||
const SPEED_SERVER_FALLBACK: &str = "speed-landing";
|
||||
@@ -66,7 +125,7 @@ impl SpeedlandingClient {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let server_name = Self::server_name_from_endpoint(&endpoint_string);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let (cert, key) = generate_self_signed_cert(&keypair)?;
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
||||
instruction::Instruction, pubkey::Pubkey,
|
||||
signature::Keypair, signer::Signer, transaction::VersionedTransaction,
|
||||
};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
|
||||
@@ -51,7 +52,7 @@ pub async fn build_transaction(
|
||||
|
||||
if with_tip && tip_amount > 0.0 {
|
||||
let tip_lamports = sol_f64_to_lamports(tip_amount);
|
||||
instructions.push(transfer(&payer.pubkey(), tip_account, tip_lamports));
|
||||
instructions.push(system_instruction::transfer(&payer.pubkey(), tip_account, tip_lamports));
|
||||
}
|
||||
|
||||
super::compute_budget_manager::extend_compute_budget_instructions(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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::{
|
||||
@@ -88,7 +88,7 @@ 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?;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::common::{
|
||||
spl_token::close_account,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use solana_sdk::{instruction::Instruction, message::AccountMeta, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_sdk::{instruction::Instruction, instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
|
||||
#[inline]
|
||||
pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]> {
|
||||
@@ -27,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,
|
||||
@@ -91,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,
|
||||
|
||||
@@ -15,7 +15,7 @@ use anyhow::{anyhow, Result};
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::OnceCell;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::message::AddressLookupTableAccount;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
|
||||
};
|
||||
@@ -562,7 +562,7 @@ pub async fn execute_parallel(
|
||||
// All jobs enqueued (no spawn on hot path)
|
||||
|
||||
if !wait_transaction_confirmed {
|
||||
const SUBMIT_TIMEOUT_SECS: u64 = 30;
|
||||
const SUBMIT_TIMEOUT_SECS: u64 = 2;//无需确认的交易,一般2秒合适了 一般2秒内发送全都返回 没返回的也不等了,没返回的就是太慢的swqos
|
||||
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
||||
instruction::Instruction, pubkey::Pubkey,
|
||||
signature::Keypair, signature::Signature,
|
||||
};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
@@ -25,6 +26,7 @@ use crate::{
|
||||
trading::MiddlewareManager,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use crate::swqos::{ SwqosType};
|
||||
|
||||
/// Global syscall bypass manager (reserved for future time/IO optimizations).
|
||||
/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
|
||||
@@ -55,7 +57,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
) -> 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 {
|
||||
@@ -215,7 +217,9 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
} else {
|
||||
(ok, signatures, err)
|
||||
};
|
||||
Ok(confirm_result)
|
||||
|
||||
//就是把confirm_result 拆开 再加上 submit_timings
|
||||
Ok((confirm_result.0, confirm_result.1, confirm_result.2, submit_timings))
|
||||
} else {
|
||||
// Not waiting for confirmation: confirmed is not measured (-); total is per-channel submit time only.
|
||||
if log_enabled {
|
||||
@@ -229,7 +233,11 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
None,
|
||||
);
|
||||
}
|
||||
Ok((ok, signatures, err))
|
||||
|
||||
|
||||
|
||||
Ok((ok, signatures, err, submit_timings))
|
||||
|
||||
};
|
||||
|
||||
result
|
||||
@@ -254,7 +262,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;
|
||||
@@ -330,7 +338,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
|
||||
@@ -345,7 +353,7 @@ async fn simulate_transaction(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((true, vec![signature], None))
|
||||
Ok((true, vec![signature], None, Vec::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+70
-21
@@ -4,7 +4,7 @@ use crate::common::spl_associated_token_account::get_associated_token_address_wi
|
||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||
use crate::constants::TOKEN_PROGRAM;
|
||||
use core_affinity::CoreId;
|
||||
use crate::instruction::utils::pumpfun::global_constants::MAYHEM_FEE_RECIPIENT;
|
||||
use crate::instruction::utils::pumpfun::is_mayhem_fee_recipient;
|
||||
|
||||
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
|
||||
#[derive(Clone)]
|
||||
@@ -18,7 +18,7 @@ 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_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -119,20 +119,22 @@ impl std::fmt::Debug for SwapParams {
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator Rewards Sharing**: Some coins use a dynamic `creator_vault` (fee-sharing config).
|
||||
/// Always use the latest on-chain creator/vault when building params for **sell**; do not reuse
|
||||
/// cached params from buy. Either fetch fresh data via RPC, or pass `creator_vault` from gRPC
|
||||
/// using [`from_trade`](PumpFunParams::from_trade) / [`from_dev_trade`](PumpFunParams::from_dev_trade),
|
||||
/// or override with [`with_creator_vault`](PumpFunParams::with_creator_vault).
|
||||
/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` =
|
||||
/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`].
|
||||
/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct
|
||||
/// does not affect ix building.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// Creator vault PDA. For Creator Rewards Sharing coins this can change; pass latest from gRPC when selling.
|
||||
/// Resolved by [`resolve_creator_vault_for_ix`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix): use ix vault when it matches `PDA(creator)` or fee-sharing vault; else `PDA(creator)`.
|
||||
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>,
|
||||
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
|
||||
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
|
||||
pub fee_recipient: Pubkey,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
@@ -147,11 +149,14 @@ impl PumpFunParams {
|
||||
creator_vault: creator_vault,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
fee_recipient: Pubkey::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
|
||||
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
|
||||
pub fn from_dev_trade(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
@@ -164,8 +169,10 @@ impl PumpFunParams {
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let is_mayhem_mode =
|
||||
mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient));
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
bonding_curve,
|
||||
&mint,
|
||||
@@ -175,17 +182,32 @@ impl PumpFunParams {
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
|
||||
&bonding_curve_account.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
creator_vault: creator_vault_resolved,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// `mayhem_mode`:
|
||||
/// - **`Some(v)`**(推荐):显式使用链上事件中的值。gRPC 日志解析对应 Explorer 的 `tradeEvent.mayhemMode`;
|
||||
/// **不会**再用 `fee_recipient` 覆盖。
|
||||
/// - **`None`**:无该字段时(例如 ShredStream 仅解外层指令、或冷路径),才用 `fee_recipient` 是否落在 Mayhem 静态列表上推断。
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
@@ -200,8 +222,12 @@ impl PumpFunParams {
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let is_mayhem_mode = match mayhem_mode {
|
||||
Some(v) => v,
|
||||
None => is_mayhem_fee_recipient(&fee_recipient),
|
||||
};
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
bonding_curve,
|
||||
mint,
|
||||
@@ -213,12 +239,22 @@ impl PumpFunParams {
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
|
||||
&bonding_curve.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
creator_vault: creator_vault_resolved,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,11 +291,11 @@ impl PumpFunParams {
|
||||
creator_vault: creator_vault.unwrap(),
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
fee_recipient: Pubkey::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Override `creator_vault` with a value from gRPC/event (e.g. for Creator Rewards Sharing).
|
||||
/// Use when selling so the instruction uses the latest on-chain vault and avoids "seeds constraint violated" (2006).
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
@@ -404,8 +440,21 @@ impl PumpSwapParams {
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||
Self::from_pool_data(rpc, pool_address, &pool_data).await
|
||||
}
|
||||
|
||||
/// Build params from an already-decoded Pool, only fetching token balances.
|
||||
///
|
||||
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
|
||||
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
|
||||
/// decoded Pool).
|
||||
pub async fn from_pool_data(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(&pool_data, rpc).await?;
|
||||
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,
|
||||
@@ -415,12 +464,12 @@ impl PumpSwapParams {
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
&pool_address,
|
||||
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_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
@@ -431,10 +480,10 @@ impl PumpSwapParams {
|
||||
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,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::trading::SwapParams;
|
||||
use anyhow::Result;
|
||||
use solana_sdk::{instruction::Instruction, signature::Signature};
|
||||
|
||||
use crate::swqos::{SwqosType};
|
||||
/// 交易执行器trait - 定义了所有交易协议都需要实现的核心方法
|
||||
#[async_trait::async_trait]
|
||||
pub trait TradeExecutor: Send + Sync {
|
||||
@@ -9,10 +9,7 @@ pub trait TradeExecutor: Send + Sync {
|
||||
/// - bool: 是否至少有一个交易成功
|
||||
/// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
|
||||
/// - Option<anyhow::Error>: 最后一个错误(如果全部失败)
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)>;
|
||||
async fn swap(&self, params: SwapParams) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)>;
|
||||
/// 获取协议名称
|
||||
fn protocol_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
@@ -22,9 +22,10 @@ use once_cell::sync::Lazy;
|
||||
use solana_sdk::{
|
||||
hash::Hash,
|
||||
instruction::Instruction,
|
||||
message::{v0, AddressLookupTableAccount, Message, VersionedMessage},
|
||||
message::{v0, Message, VersionedMessage},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use std::sync::Arc;
|
||||
/// 预分配的交易构建器
|
||||
pub struct PreallocatedTxBuilder {
|
||||
|
||||
@@ -28,6 +28,10 @@ pub const fn ceil_div(a: u128, b: u128) -> u128 {
|
||||
(a + b - 1) / b
|
||||
}
|
||||
|
||||
/// Maximum slippage in basis points (99.99% = 9999 bps)
|
||||
/// This prevents the wrap amount from doubling when slippage is 100%
|
||||
pub const MAX_SLIPPAGE_BASIS_POINTS: u64 = 9999;
|
||||
|
||||
/// Calculate buy amount with slippage protection
|
||||
/// Add slippage percentage to the amount to ensure successful purchase
|
||||
///
|
||||
@@ -40,9 +44,18 @@ pub const fn ceil_div(a: u128, b: u128) -> u128 {
|
||||
/// * basis_points = 10 -> 0.1% slippage
|
||||
/// * basis_points = 100 -> 1% slippage
|
||||
/// * basis_points = 500 -> 5% slippage
|
||||
///
|
||||
/// # Note
|
||||
/// Basis points are clamped to MAX_SLIPPAGE_BASIS_POINTS (9999 = 99.99%)
|
||||
/// to prevent the amount from doubling when basis_points = 10000.
|
||||
#[inline(always)]
|
||||
pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 {
|
||||
amount + (amount * basis_points / 10000)
|
||||
let bps = if basis_points > MAX_SLIPPAGE_BASIS_POINTS {
|
||||
MAX_SLIPPAGE_BASIS_POINTS
|
||||
} else {
|
||||
basis_points
|
||||
};
|
||||
amount + (amount * bps / 10000)
|
||||
}
|
||||
|
||||
/// Calculate sell amount with slippage protection
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
use solana_sdk::{native_token::sol_str_to_lamports, pubkey::Pubkey};
|
||||
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
|
||||
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
fn sol_to_lamports(sol: f64) -> u64 {
|
||||
(sol * 1_000_000_000.0) as u64
|
||||
}
|
||||
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
|
||||
utils::calc::common::compute_fee,
|
||||
};
|
||||
|
||||
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
|
||||
#[inline]
|
||||
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
|
||||
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
|
||||
}
|
||||
|
||||
/// Calculates the amount of tokens that can be purchased with a given SOL amount
|
||||
/// using the bonding curve formula.
|
||||
///
|
||||
|
||||
+6
-1
@@ -81,6 +81,7 @@ mkdir -p examples/pumpfun_buy_test/src
|
||||
cat > examples/pumpfun_buy_test/src/main.rs << 'EOF'
|
||||
use sol_trade_sdk::{
|
||||
common::{TradeConfig, AnyResult},
|
||||
constants::TOKEN_PROGRAM,
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
trading::{core::params::PumpFunParams, factory::DexType},
|
||||
SolanaTrade, TradeTokenType, TradeBuyParams,
|
||||
@@ -166,7 +167,7 @@ async fn main() -> AnyResult<()> {
|
||||
println!("📊 滑点: {} basis points", slippage);
|
||||
println!("================================\n");
|
||||
|
||||
// PumpFun买入参数 (买入不需要特殊参数,使用零值)
|
||||
// PumpFun买入参数 (占位;实际路径应使用 RPC 或事件填充)
|
||||
let params = PumpFunParams::from_trade(
|
||||
Pubkey::default(), // bonding_curve
|
||||
Pubkey::default(), // associated_bonding_curve
|
||||
@@ -178,6 +179,10 @@ async fn main() -> AnyResult<()> {
|
||||
0, // real_token_reserves
|
||||
0, // real_sol_reserves
|
||||
None, // close_token_account_when_sell
|
||||
Pubkey::default(), // fee_recipient
|
||||
TOKEN_PROGRAM,
|
||||
false, // is_cashback_coin
|
||||
None, // mayhem_mode(None=按 fee_recipient 推断)
|
||||
);
|
||||
|
||||
let buy_params = TradeBuyParams {
|
||||
|
||||
Reference in New Issue
Block a user