Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f6cc6fb08 | |||
| 5e4977f6a6 | |||
| c27e479659 | |||
| 9a8e3ffb2d | |||
| 43d1369d4e |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "3.4.0"
|
||||
version = "3.4.1"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
|
||||
@@ -47,10 +47,11 @@
|
||||
- [📋 Example Usage](#-example-usage)
|
||||
- [⚡ Trading Parameters](#-trading-parameters)
|
||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||
- [⚙️ SWQOS Service Configuration](#️-swqos-service-configuration)
|
||||
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||
- [🔧 Middleware System](#-middleware-system)
|
||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||
- [🔍 Nonce Cache](#-nonce-cache)
|
||||
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
|
||||
- [🛡️ MEV Protection Services](#️-mev-protection-services)
|
||||
- [📁 Project Structure](#-project-structure)
|
||||
- [📄 License](#-license)
|
||||
@@ -71,7 +72,7 @@
|
||||
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
|
||||
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
|
||||
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
|
||||
11. **Shared Infrastructure**: Share expensive RPC and SWQOS clients across multiple wallets for reduced resource usage
|
||||
11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
@@ -88,14 +89,14 @@ Add the dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.1" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "3.4.0"
|
||||
sol-trade-sdk = "3.4.1"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -113,7 +114,7 @@ let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
// RPC URL
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let commitment = CommitmentConfig::processed();
|
||||
// Multiple SWQOS services can be configured
|
||||
// Multiple SWQoS services can be configured
|
||||
let swqos_configs: Vec<SwqosConfig> = vec |
|
||||
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||
|
||||
### ⚙️ SWQOS Service Configuration
|
||||
### ⚙️ SWQoS Service Configuration
|
||||
|
||||
When configuring SWQOS services, note the different parameter requirements for each service:
|
||||
When configuring SWQoS services, note the different parameter requirements for each service:
|
||||
|
||||
- **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`)
|
||||
- **Other MEV services**: The first parameter is the API Token
|
||||
|
||||
#### Custom URL Support
|
||||
|
||||
Each SWQOS service now supports an optional custom URL parameter:
|
||||
Each SWQoS service now supports an optional custom URL parameter:
|
||||
|
||||
```rust
|
||||
// Using custom URL (third parameter)
|
||||
@@ -273,6 +278,17 @@ Address Lookup Tables (ALT) allow you to optimize transaction size and reduce fe
|
||||
|
||||
Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md).
|
||||
|
||||
## 💰 Cashback Support (PumpFun / PumpSwap)
|
||||
|
||||
PumpFun and PumpSwap support **cashback** for eligible tokens: part of the trading fee can be returned to the user. The SDK **must know** whether the token has cashback enabled so that buy/sell instructions include the correct accounts (e.g. `UserVolumeAccumulator` as remaining account for cashback coins).
|
||||
|
||||
- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step.
|
||||
- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK:
|
||||
- **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `is_cashback_coin`).
|
||||
- **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data.
|
||||
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
|
||||
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
|
||||
|
||||
## 🛡️ MEV Protection Services
|
||||
|
||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||
|
||||
+40
-20
@@ -47,10 +47,11 @@
|
||||
- [📋 使用示例](#-使用示例)
|
||||
- [⚡ 交易参数](#-交易参数)
|
||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||
- [⚙️ SWQOS 服务配置说明](#️-swqos-服务配置说明)
|
||||
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||
- [🔍 地址查找表](#-地址查找表)
|
||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||
- [📁 项目结构](#-项目结构)
|
||||
- [📄 许可证](#-许可证)
|
||||
@@ -71,6 +72,7 @@
|
||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
|
||||
|
||||
## 📦 安装
|
||||
|
||||
@@ -87,14 +89,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.1" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "3.4.0"
|
||||
sol-trade-sdk = "3.4.1"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -103,40 +105,46 @@ sol-trade-sdk = "3.4.0"
|
||||
|
||||
#### 1. 创建 TradingClient 实例
|
||||
|
||||
可以参考 [示例:创建 TradingClient 实例](examples/trading_client/src/main.rs)。
|
||||
可参考 [示例:创建 TradingClient 实例](examples/trading_client/src/main.rs)。
|
||||
|
||||
**方式一:简单创建(单钱包)**
|
||||
```rust
|
||||
// 钱包
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
// RPC 地址
|
||||
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
|
||||
let commitment = CommitmentConfig::processed();
|
||||
// 可以配置多个SWQOS服务
|
||||
// 可配置多个 SWQoS 服务
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Astralane("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
|
||||
// 可选:自定义 WSOL ATA 和 Seed 优化设置
|
||||
// 可选:自定义 WSOL ATA 与 Seed 优化
|
||||
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||
// .with_wsol_ata_config(
|
||||
// true, // create_wsol_ata_on_startup: 启动时检查并创建 WSOL ATA(默认: true)
|
||||
// true // use_seed_optimize: 全局启用所有 ATA 操作的 seed 优化(默认: true)
|
||||
// );
|
||||
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
|
||||
|
||||
// 创建 TradingClient 客户端
|
||||
// 创建 TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
```
|
||||
|
||||
**方式二:共享基础设施(多钱包)**
|
||||
|
||||
多钱包场景下可先创建一份基础设施,再复用到多个钱包。参见 [示例:共享基础设施](examples/shared_infrastructure/src/main.rs)。
|
||||
|
||||
```rust
|
||||
// 创建一次基础设施(开销较大)
|
||||
let infra_config = InfrastructureConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
|
||||
|
||||
// 基于同一基础设施创建多个客户端(开销小)
|
||||
let client1 = TradingClient::from_infrastructure(Arc::new(payer1), infrastructure.clone(), true);
|
||||
let client2 = TradingClient::from_infrastructure(Arc::new(payer2), infrastructure.clone(), true);
|
||||
```
|
||||
|
||||
#### 2. 配置 Gas Fee 策略
|
||||
|
||||
有关 Gas Fee 策略的详细信息,请参阅 [Gas Fee 策略参考手册](docs/GAS_FEE_STRATEGY_CN.md)。
|
||||
@@ -198,6 +206,7 @@ client.buy(buy_params).await?;
|
||||
| 描述 | 运行命令 | 源码路径 |
|
||||
|------|---------|----------|
|
||||
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
|
||||
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
|
||||
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
|
||||
| PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
|
||||
| PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
|
||||
@@ -213,16 +222,16 @@ client.buy(buy_params).await?;
|
||||
| Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
||||
| Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||
|
||||
### ⚙️ SWQOS 服务配置说明
|
||||
### ⚙️ SWQoS 服务配置说明
|
||||
|
||||
在配置 SWQOS 服务时,需要注意不同服务的参数要求:
|
||||
在配置 SWQoS 服务时,需要注意不同服务的参数要求:
|
||||
|
||||
- **Jito**: 第一个参数为 UUID(如无 UUID 请传入空字符串 `""`)
|
||||
- 其他的MEV服务,第一个参数为 API Token
|
||||
|
||||
#### 自定义 URL 支持
|
||||
|
||||
每个 SWQOS 服务现在都支持可选的自定义 URL 参数:
|
||||
每个 SWQoS 服务现在都支持可选的自定义 URL 参数:
|
||||
|
||||
```rust
|
||||
// 使用自定义 URL(第三个参数)
|
||||
@@ -268,6 +277,17 @@ let middleware_manager = MiddlewareManager::new()
|
||||
|
||||
使用 Durable Nonce 来实现交易重放保护和优化交易处理。详细信息请参阅 [Nonce 使用指南](docs/NONCE_CACHE_CN.md)。
|
||||
|
||||
## 💰 Cashback 支持(PumpFun / PumpSwap)
|
||||
|
||||
PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还给用户。SDK **必须知道**该代币是否开启返现,才能为 buy/sell 指令传入正确的账户(例如返现代币需要把 `UserVolumeAccumulator` 作为 remaining account)。
|
||||
|
||||
- **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc` 或 `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。
|
||||
- **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK:
|
||||
- **PumpFun**:`PumpFunParams::from_trade(..., is_cashback_coin)` 与 `PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
|
||||
- **PumpSwap**:`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。
|
||||
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。
|
||||
- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
||||
|
||||
@@ -143,13 +143,14 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
||||
trade_info.mint,
|
||||
trade_info.creator,
|
||||
trade_info.creator_vault,
|
||||
trade_info.virtual_sol_reserves,
|
||||
trade_info.virtual_token_reserves,
|
||||
trade_info.virtual_sol_reserves,
|
||||
trade_info.real_token_reserves,
|
||||
trade_info.real_sol_reserves,
|
||||
None,
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
false, // is_cashback_coin: set from event/parser when available
|
||||
)),
|
||||
address_lookup_table_account: address_lookup_table_account,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -150,6 +150,7 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
||||
None,
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
false, // is_cashback_coin: set from event/parser when available
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
//! PumpFun 跟单示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
|
||||
//!
|
||||
//! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。
|
||||
|
||||
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||
|
||||
use sol_parser_sdk::grpc::{
|
||||
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||
TransactionFilter, YellowstoneGrpc,
|
||||
};
|
||||
use sol_parser_sdk::DexEvent;
|
||||
use sol_trade_sdk::common::{
|
||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
|
||||
};
|
||||
@@ -16,143 +22,128 @@ use sol_trade_sdk::{
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::match_event;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
||||
|
||||
// Global static flag to ensure transaction is executed only once
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Subscribing to GRPC events...");
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
println!("PumpFun 跟单示例(sol-parser-sdk gRPC)...");
|
||||
|
||||
let grpc = YellowstoneGrpc::new(
|
||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let callback = create_event_callback();
|
||||
let protocols = vec![Protocol::PumpFun];
|
||||
// Filter accounts
|
||||
let account_include = vec![
|
||||
PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID
|
||||
];
|
||||
let account_exclude = vec![];
|
||||
let account_required = vec![];
|
||||
|
||||
// Listen to transaction data
|
||||
let transaction_filter = TransactionFilter {
|
||||
account_include: account_include.clone(),
|
||||
account_exclude,
|
||||
account_required,
|
||||
let config = ClientConfig {
|
||||
enable_metrics: false,
|
||||
connection_timeout_ms: 10000,
|
||||
request_timeout_ms: 30000,
|
||||
enable_tls: true,
|
||||
order_mode: OrderMode::Unordered,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// Listen to account data belonging to owner programs -> account event monitoring
|
||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
||||
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
|
||||
let grpc = YellowstoneGrpc::new_with_config(
|
||||
grpc_endpoint.clone(),
|
||||
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||
config,
|
||||
)?;
|
||||
|
||||
// listen to specific event type
|
||||
let event_type_filter =
|
||||
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
|
||||
let protocols = vec![Protocol::PumpFun];
|
||||
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||
let account_filter = AccountFilter::for_protocols(&protocols);
|
||||
let event_filter = EventTypeFilter::include_only(vec![
|
||||
EventType::PumpFunBuy,
|
||||
EventType::PumpFunSell,
|
||||
EventType::PumpFunBuyExactSolIn,
|
||||
EventType::PumpFunTrade,
|
||||
]);
|
||||
|
||||
grpc.subscribe_events_immediate(
|
||||
protocols,
|
||||
None,
|
||||
vec![transaction_filter],
|
||||
vec![account_filter],
|
||||
Some(event_type_filter),
|
||||
None,
|
||||
callback,
|
||||
)
|
||||
.await?;
|
||||
let queue = grpc
|
||||
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||
.await?;
|
||||
|
||||
println!("订阅已启动,等待一条 PumpFun 交易后执行跟单(仅一次)...\n");
|
||||
|
||||
loop {
|
||||
if let Some(event) = queue.pop() {
|
||||
let run = match &event {
|
||||
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
Some(e.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
DexEvent::PumpFunTrade(e) => {
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
Some(e.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(e) = run {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpfun_copy_trade(e).await {
|
||||
eprintln!("跟单执行错误: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
std::process::exit(0);
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
// Test code, only test one transaction
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SolanaTrade client
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||
}
|
||||
|
||||
/// PumpFun sniper trade
|
||||
/// This function demonstrates how to snipe a new token from a PumpFun trade event
|
||||
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
||||
println!("Testing PumpFun trading...");
|
||||
|
||||
async fn pumpfun_copy_trade(
|
||||
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||
) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
let mint_pubkey = trade_info.mint;
|
||||
let slippage_basis_points = Some(100);
|
||||
let mint_pubkey = e.mint;
|
||||
let slippage_basis_points = Some(100u64);
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(
|
||||
150000,
|
||||
150000,
|
||||
500000,
|
||||
500000,
|
||||
0.001,
|
||||
0.001,
|
||||
);
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from PumpFun...");
|
||||
let buy_sol_amount = 100_000;
|
||||
// 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析)
|
||||
let buy_sol_amount = 100_000u64;
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
input_token_type: TradeTokenType::SOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: buy_sol_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||
trade_info.bonding_curve,
|
||||
trade_info.associated_bonding_curve,
|
||||
trade_info.mint,
|
||||
trade_info.creator,
|
||||
trade_info.creator_vault,
|
||||
trade_info.virtual_token_reserves,
|
||||
trade_info.virtual_sol_reserves,
|
||||
trade_info.real_token_reserves,
|
||||
trade_info.real_sol_reserves,
|
||||
e.bonding_curve,
|
||||
e.associated_bonding_curve,
|
||||
e.mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
e.virtual_sol_reserves,
|
||||
e.real_token_reserves,
|
||||
e.real_sol_reserves,
|
||||
None,
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -167,43 +158,40 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from PumpFun...");
|
||||
|
||||
// 卖出:查询余额后卖出,同样传入 is_cashback_coin
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||
&payer,
|
||||
&mint_pubkey,
|
||||
&trade_info.token_program,
|
||||
&e.token_program,
|
||||
client.use_seed_optimize,
|
||||
);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
output_token_type: TradeTokenType::SOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||
trade_info.bonding_curve,
|
||||
trade_info.associated_bonding_curve,
|
||||
trade_info.mint,
|
||||
trade_info.creator,
|
||||
trade_info.creator_vault,
|
||||
trade_info.virtual_token_reserves,
|
||||
trade_info.virtual_sol_reserves,
|
||||
trade_info.real_token_reserves,
|
||||
trade_info.real_sol_reserves,
|
||||
e.bonding_curve,
|
||||
e.associated_bonding_curve,
|
||||
e.mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
e.virtual_sol_reserves,
|
||||
e.real_token_reserves,
|
||||
e.real_sol_reserves,
|
||||
Some(true),
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -212,14 +200,11 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
|
||||
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell)
|
||||
// creator_vault can be obtained from the trade event
|
||||
|
||||
// Exit program
|
||||
std::process::exit(0);
|
||||
println!("跟单一次买+卖完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
sol-trade-sdk = { path = "../.." }
|
||||
solana-streamer-sdk = "0.5.0"
|
||||
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
||||
solana-sdk = "3.0.0"
|
||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
//! PumpFun 狙击示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
|
||||
//!
|
||||
//! 监听创建者首次买入(Create 后同笔/首笔 Buy,is_created_buy == true),
|
||||
//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。
|
||||
|
||||
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||
|
||||
use sol_parser_sdk::grpc::{
|
||||
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||
TransactionFilter, YellowstoneGrpc,
|
||||
};
|
||||
use sol_parser_sdk::DexEvent;
|
||||
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
||||
use sol_trade_sdk::common::TradeConfig;
|
||||
use sol_trade_sdk::TradeTokenType;
|
||||
@@ -10,108 +22,120 @@ use sol_trade_sdk::{
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
|
||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
||||
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
/// Atomic flag to ensure the sniper trade is executed only once
|
||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Main entry point - subscribes to PumpFun events and executes sniper trades on token creation
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Subscribing to ShredStream events...");
|
||||
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?;
|
||||
let callback = create_event_callback();
|
||||
let protocols = vec![Protocol::PumpFun];
|
||||
let event_type_filter = EventTypeFilter {
|
||||
include: vec![EventType::PumpFunBuy, EventType::PumpFunSell, EventType::PumpFunCreateToken],
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
println!("PumpFun 狙击示例(sol-parser-sdk gRPC)...");
|
||||
|
||||
let config = ClientConfig {
|
||||
enable_metrics: false,
|
||||
connection_timeout_ms: 10000,
|
||||
request_timeout_ms: 30000,
|
||||
enable_tls: true,
|
||||
order_mode: OrderMode::Unordered,
|
||||
..Default::default()
|
||||
};
|
||||
println!("Starting to listen for events, press Ctrl+C to stop...");
|
||||
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?;
|
||||
|
||||
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
|
||||
let grpc = YellowstoneGrpc::new_with_config(
|
||||
grpc_endpoint,
|
||||
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||
config,
|
||||
)?;
|
||||
|
||||
let protocols = vec![Protocol::PumpFun];
|
||||
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||
let account_filter = AccountFilter::for_protocols(&protocols);
|
||||
let event_filter = EventTypeFilter::include_only(vec![
|
||||
EventType::PumpFunCreate,
|
||||
EventType::PumpFunBuy,
|
||||
EventType::PumpFunBuyExactSolIn,
|
||||
]);
|
||||
|
||||
let queue = grpc
|
||||
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||
.await?;
|
||||
|
||||
println!("订阅已启动,等待创建者首次买入(is_created_buy)后执行狙击(仅一次)...\n");
|
||||
|
||||
loop {
|
||||
if let Some(event) = queue.pop() {
|
||||
let run = match &event {
|
||||
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||
if e.is_created_buy && !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
Some(e.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
if let Some(e) = run {
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpfun_sniper_trade(e).await {
|
||||
eprintln!("狙击执行错误: {:?}", err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
std::process::exit(0);
|
||||
});
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
tokio::signal::ctrl_c().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an event callback function that handles different types of events
|
||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
||||
|event: Box<dyn UnifiedEvent>| {
|
||||
match_event!(event, {
|
||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
||||
// Only process developer token creation events
|
||||
if !e.is_dev_create_token_trade {
|
||||
return;
|
||||
}
|
||||
// Ensure we only execute the trade once using atomic compare-and-swap
|
||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||
let event_clone = e.clone();
|
||||
// Spawn a new task to handle the trading operation
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = pumpfun_sniper_trade_with_shreds(event_clone).await {
|
||||
eprintln!("Error in copy trade: {:?}", err);
|
||||
std::process::exit(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Create SolanaTrade client
|
||||
/// Initializes a new SolanaTrade client with configuration
|
||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
println!("🚀 Initializing SolanaTrade client...");
|
||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||
}
|
||||
|
||||
/// Execute PumpFun sniper trading strategy based on received token creation event
|
||||
/// This function buys tokens immediately after creation and then sells all tokens
|
||||
async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
||||
println!("Testing PumpFun trading...");
|
||||
|
||||
async fn pumpfun_sniper_trade(
|
||||
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||
) -> AnyResult<()> {
|
||||
let client = create_solana_trade_client().await?;
|
||||
let mint_pubkey = trade_info.mint;
|
||||
let slippage_basis_points = Some(300);
|
||||
let mint_pubkey = e.mint;
|
||||
let slippage_basis_points = Some(300u64);
|
||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||
|
||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||
|
||||
// Buy tokens
|
||||
println!("Buying tokens from PumpFun...");
|
||||
let buy_sol_amount = 100_000;
|
||||
// 创建者首次买入:用 from_dev_trade,max_sol_cost 用事件中的 sol_amount(可酌情加滑点)
|
||||
let buy_sol_amount = 100_000u64;
|
||||
let max_sol_cost = e.sol_amount.saturating_add(e.sol_amount / 10); // 约 +10% 作为上限
|
||||
|
||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
input_token_type: TradeTokenType::SOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: buy_sol_amount,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade(
|
||||
trade_info.mint,
|
||||
trade_info.token_amount,
|
||||
trade_info.max_sol_cost,
|
||||
trade_info.creator,
|
||||
trade_info.bonding_curve,
|
||||
trade_info.associated_bonding_curve,
|
||||
trade_info.creator_vault,
|
||||
e.mint,
|
||||
e.token_amount,
|
||||
max_sol_cost,
|
||||
e.creator,
|
||||
e.bonding_curve,
|
||||
e.associated_bonding_curve,
|
||||
e.creator_vault,
|
||||
None,
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -126,26 +150,35 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
// Sell tokens
|
||||
println!("Selling tokens from PumpFun...");
|
||||
|
||||
let rpc = client.infrastructure.rpc.clone();
|
||||
let payer = client.payer.pubkey();
|
||||
let account = get_associated_token_address(&payer, &mint_pubkey);
|
||||
let balance = rpc.get_token_account_balance(&account).await?;
|
||||
println!("Balance: {:?}", balance);
|
||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||
|
||||
println!("Selling {} tokens", amount_token);
|
||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||
dex_type: DexType::PumpFun,
|
||||
output_token_type: TradeTokenType::SOL,
|
||||
mint: mint_pubkey,
|
||||
input_token_amount: amount_token,
|
||||
slippage_basis_points: slippage_basis_points,
|
||||
slippage_basis_points,
|
||||
recent_blockhash: Some(recent_blockhash),
|
||||
with_tip: false,
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::immediate_sell(trade_info.creator_vault, trade_info.token_program, true)),
|
||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||
e.bonding_curve,
|
||||
e.associated_bonding_curve,
|
||||
e.mint,
|
||||
e.creator,
|
||||
e.creator_vault,
|
||||
e.virtual_token_reserves,
|
||||
e.virtual_sol_reserves,
|
||||
e.real_token_reserves,
|
||||
e.real_sol_reserves,
|
||||
Some(true),
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
create_output_token_ata: true,
|
||||
@@ -153,14 +186,11 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
|
||||
close_mint_token_ata: false,
|
||||
durable_nonce: None,
|
||||
fixed_output_token_amount: None,
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
};
|
||||
client.sell(sell_params).await?;
|
||||
|
||||
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell)
|
||||
// creator_vault can be obtained from the trade event
|
||||
|
||||
// Exit program after completing the trade
|
||||
std::process::exit(0);
|
||||
println!("狙击一次买+卖完成");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ pub struct BondingCurveAccount {
|
||||
}
|
||||
|
||||
impl BondingCurveAccount {
|
||||
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
|
||||
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
|
||||
pub fn from_dev_trade(
|
||||
bonding_curve: Pubkey,
|
||||
mint: &Pubkey,
|
||||
@@ -72,6 +74,7 @@ impl BondingCurveAccount {
|
||||
dev_sol_amount: u64,
|
||||
creator: Pubkey,
|
||||
is_mayhem_mode: bool,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let account = if bonding_curve != Pubkey::default() {
|
||||
bonding_curve
|
||||
@@ -89,10 +92,12 @@ impl BondingCurveAccount {
|
||||
complete: false,
|
||||
creator: creator,
|
||||
is_mayhem_mode: is_mayhem_mode,
|
||||
is_cashback_coin: false,
|
||||
is_cashback_coin,
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
|
||||
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
mint: Pubkey,
|
||||
@@ -102,6 +107,7 @@ impl BondingCurveAccount {
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
is_mayhem_mode: bool,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let account = if bonding_curve != Pubkey::default() {
|
||||
bonding_curve
|
||||
@@ -119,7 +125,7 @@ impl BondingCurveAccount {
|
||||
complete: false,
|
||||
creator: creator,
|
||||
is_mayhem_mode: is_mayhem_mode,
|
||||
is_cashback_coin: false,
|
||||
is_cashback_coin,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+14
-17
@@ -52,8 +52,8 @@ impl AstralaneClient {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = Client::builder()
|
||||
// Optimized connection pool settings for high performance
|
||||
.pool_idle_timeout(Duration::from_secs(120))
|
||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
||||
.pool_idle_timeout(Duration::from_secs(300))
|
||||
.pool_max_idle_per_host(4)
|
||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
||||
@@ -90,17 +90,16 @@ impl AstralaneClient {
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
||||
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Astralane ping request failed: {}", e);
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Send ping request
|
||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Astralane ping request failed: {}", e);
|
||||
}
|
||||
@@ -130,25 +129,23 @@ impl AstralaneClient {
|
||||
format!("{}/gethealth", endpoint)
|
||||
};
|
||||
|
||||
// Send GET request to /gethealth endpoint with api_key header
|
||||
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||
let response = http_client.get(&ping_url)
|
||||
.header("api_key", auth_token)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
// ping successful, connection remains active
|
||||
// println!("send getHealth to keep connection alive");
|
||||
} else {
|
||||
eprintln!("Astralane ping request returned non-success status: {}", response.status());
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!("Astralane ping request returned non-success status: {}", status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"jsonrpc": "2.0",
|
||||
|
||||
+16
-18
@@ -52,8 +52,8 @@ impl BlockRazorClient {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = Client::builder()
|
||||
// Optimized connection pool settings for high performance
|
||||
.pool_idle_timeout(Duration::from_secs(120))
|
||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
||||
.pool_idle_timeout(Duration::from_secs(300)) // 5min so ping-kept connection is not evicted early
|
||||
.pool_max_idle_per_host(4) // Few connections so submit reuses same connection as ping, avoiding cold connection after ~5min server idle close
|
||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
||||
@@ -90,16 +90,16 @@ impl BlockRazorClient {
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
||||
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("BlockRazor ping request failed: {}", e);
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Send ping request
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("BlockRazor ping request failed: {}", e);
|
||||
}
|
||||
@@ -137,33 +137,31 @@ impl BlockRazorClient {
|
||||
headers.insert("apikey", HeaderValue::from_str(auth_token)?);
|
||||
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
|
||||
|
||||
// Send GET request to /health endpoint with headers
|
||||
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||
let response = http_client.get(&ping_url)
|
||||
.headers(headers)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
// ping successful, connection remains active
|
||||
// Can optionally log, but to reduce noise, not printing here
|
||||
} else {
|
||||
eprintln!("BlockRazor ping request failed with status: {}", response.status());
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!("BlockRazor ping request failed with status: {}", status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// BlockRazor使用fast模式的请求格式
|
||||
// BlockRazor fast-mode request format
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"transaction": content,
|
||||
"mode": "fast"
|
||||
}))?;
|
||||
|
||||
// BlockRazor使用apikey header
|
||||
// BlockRazor uses apikey header
|
||||
let response_text = self.http_client.post(&self.endpoint)
|
||||
.body(request_body)
|
||||
.header("Content-Type", "application/json")
|
||||
|
||||
+24
-26
@@ -1,4 +1,6 @@
|
||||
use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
|
||||
use crate::swqos::common::poll_transaction_confirmation;
|
||||
use crate::swqos::common::serialize_transaction_and_encode;
|
||||
use crate::swqos::serialization;
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
@@ -63,27 +65,25 @@ impl BloxrouteClient {
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let body = serde_json::json!({
|
||||
"transaction": {
|
||||
"content": content,
|
||||
},
|
||||
"frontRunningProtection": false,
|
||||
"useStakedRPCs": true,
|
||||
});
|
||||
// Single format! for body to avoid json! + to_string() double allocation
|
||||
let body = format!(
|
||||
r#"{{"transaction":{{"content":"{}"}},"frontRunningProtection":false,"useStakedRPCs":true}}"#,
|
||||
content
|
||||
);
|
||||
|
||||
let endpoint = format!("{}/api/v2/submit", self.endpoint);
|
||||
let response_text = self.http_client.post(&endpoint)
|
||||
.body(body.to_string())
|
||||
.body(body)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", self.auth_token.clone())
|
||||
.header("Authorization", self.auth_token.as_str())
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
|
||||
// Parse with from_str to avoid extra wait from .json().await
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
@@ -114,24 +114,22 @@ impl BloxrouteClient {
|
||||
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
|
||||
let body = serde_json::json!({
|
||||
"entries": transactions
|
||||
.iter()
|
||||
.map(|tx| {
|
||||
serde_json::json!({
|
||||
"transaction": {
|
||||
"content": tx.to_base64_string(),
|
||||
},
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
});
|
||||
let contents = serialization::serialize_transactions_batch_sync(
|
||||
transactions.as_slice(),
|
||||
UiTransactionEncoding::Base64,
|
||||
)?;
|
||||
let entries: String = contents
|
||||
.iter()
|
||||
.map(|c| format!(r#"{{"transaction":{{"content":"{}"}}}}"#, c))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let body = format!(r#"{{"entries":[{}]}}"#, entries);
|
||||
|
||||
let endpoint = format!("{}/api/v2/submit-batch", self.endpoint);
|
||||
let response_text = self.http_client.post(&endpoint)
|
||||
.body(body.to_string())
|
||||
.body(body)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", self.auth_token.clone())
|
||||
.header("Authorization", self.auth_token.as_str())
|
||||
.send()
|
||||
.await?
|
||||
.text()
|
||||
|
||||
+21
-36
@@ -3,6 +3,7 @@ use anyhow::Result;
|
||||
use base64::engine::general_purpose::{self, STANDARD};
|
||||
use base64::Engine;
|
||||
use bincode::serialize;
|
||||
use crate::swqos::serialization;
|
||||
use reqwest::Client;
|
||||
use serde_json;
|
||||
use serde_json::json;
|
||||
@@ -40,7 +41,7 @@ impl From<anyhow::Error> for TradeError {
|
||||
}
|
||||
}
|
||||
|
||||
// 使用高性能序列化
|
||||
// High-performance serialization
|
||||
|
||||
pub trait FormatBase64VersionedTransaction {
|
||||
fn to_base64_string(&self) -> String;
|
||||
@@ -58,12 +59,12 @@ pub async fn poll_transaction_confirmation(
|
||||
txt_sig: Signature,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<Signature> {
|
||||
// 如果不需要等待确认,立即返回签名
|
||||
// If no confirmation needed, return signature immediately
|
||||
if !wait_confirmation {
|
||||
return Ok(txt_sig);
|
||||
}
|
||||
|
||||
let timeout: Duration = Duration::from_secs(15); // 🔧 增加到15秒,避免网络拥堵时超时
|
||||
let timeout: Duration = Duration::from_secs(15); // 15s to avoid timeout under network congestion
|
||||
let interval: Duration = Duration::from_millis(1000);
|
||||
let start: Instant = Instant::now();
|
||||
let mut poll_count = 0u32;
|
||||
@@ -76,33 +77,23 @@ pub async fn poll_transaction_confirmation(
|
||||
poll_count += 1;
|
||||
|
||||
let status = rpc.get_signature_statuses(&[txt_sig]).await?;
|
||||
match status.value[0].clone() {
|
||||
Some(status) => {
|
||||
if status.err.is_none()
|
||||
&& (status.confirmation_status
|
||||
== Some(TransactionConfirmationStatus::Confirmed)
|
||||
|| status.confirmation_status
|
||||
== Some(TransactionConfirmationStatus::Finalized))
|
||||
let first = status.value.get(0).and_then(|o| o.as_ref());
|
||||
match first {
|
||||
Some(s) => {
|
||||
if s.err.is_none()
|
||||
&& (s.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|
||||
|| s.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
|
||||
{
|
||||
return Ok(txt_sig);
|
||||
}
|
||||
// 如果 getSignatureStatuses 返回了错误,立即获取详细信息
|
||||
if status.err.is_some() {
|
||||
// 直接跳转到获取交易详情
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// 交易还未上链,继续等待,不调用 getTransaction
|
||||
sleep(interval).await;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 优化:只在以下情况调用 getTransaction
|
||||
// 1. getSignatureStatuses 返回了错误
|
||||
// 2. 或者已经轮询了较长时间(超过10次,即10秒)
|
||||
let should_get_transaction = status.value[0].as_ref().map(|s| s.err.is_some()).unwrap_or(false)
|
||||
|| poll_count >= 10;
|
||||
let should_get_transaction = first.map(|s| s.err.is_some()).unwrap_or(false) || poll_count >= 10;
|
||||
|
||||
if !should_get_transaction {
|
||||
sleep(interval).await;
|
||||
@@ -122,7 +113,7 @@ pub async fn poll_transaction_confirmation(
|
||||
{
|
||||
Ok(details) => details,
|
||||
Err(_) => {
|
||||
// 交易可能还未上链,继续等待
|
||||
// Tx may not be on chain yet, keep waiting
|
||||
sleep(interval).await;
|
||||
continue;
|
||||
}
|
||||
@@ -136,7 +127,7 @@ pub async fn poll_transaction_confirmation(
|
||||
if meta.err.is_none() {
|
||||
return Ok(txt_sig);
|
||||
} else {
|
||||
// 从 log_messages 中提取错误信息
|
||||
// Extract error message from log_messages
|
||||
let mut error_msg = String::new();
|
||||
if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) =
|
||||
&meta.log_messages
|
||||
@@ -162,12 +153,12 @@ pub async fn poll_transaction_confirmation(
|
||||
let tx_err: TransactionError =
|
||||
serde_json::from_value(serde_json::to_value(&ui_err)?)?;
|
||||
|
||||
// 直接使用Solana原生的InstructionError中的错误码
|
||||
// Use Solana InstructionError codes directly
|
||||
let mut code = 0u32;
|
||||
let mut index = None;
|
||||
match &tx_err {
|
||||
TransactionError::InstructionError(i, i_error) => {
|
||||
// 直接匹配所有InstructionError类型,Custom也是其中之一
|
||||
// Match all InstructionError variants including Custom
|
||||
code = match i_error {
|
||||
solana_sdk::instruction::InstructionError::Custom(c) => *c,
|
||||
solana_sdk::instruction::InstructionError::GenericError => 1,
|
||||
@@ -180,7 +171,7 @@ pub async fn poll_transaction_confirmation(
|
||||
solana_sdk::instruction::InstructionError::MissingRequiredSignature => 8,
|
||||
solana_sdk::instruction::InstructionError::AccountAlreadyInitialized => 9,
|
||||
solana_sdk::instruction::InstructionError::UninitializedAccount => 10,
|
||||
_ => 999, // 其他未知错误
|
||||
_ => 999, // Other unknown errors
|
||||
};
|
||||
index = Some(*i);
|
||||
}
|
||||
@@ -198,11 +189,11 @@ pub async fn poll_transaction_confirmation(
|
||||
}
|
||||
|
||||
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
|
||||
// 序列化交易
|
||||
// Serialize transaction
|
||||
let serialized = bincode::serialize(transaction)
|
||||
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
|
||||
|
||||
// Base64编码
|
||||
// Base64 encode
|
||||
let encoded = STANDARD.encode(serialized);
|
||||
|
||||
let request_data = json!({
|
||||
@@ -250,18 +241,12 @@ pub async fn serialize_and_encode(
|
||||
Ok(serialized)
|
||||
}
|
||||
|
||||
pub async fn serialize_transaction_and_encode(
|
||||
/// Sync serialize and encode; uses buffer pool when possible for lower allocs and latency.
|
||||
pub fn serialize_transaction_and_encode(
|
||||
transaction: &impl SerializableTransaction,
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<(String, Signature)> {
|
||||
let signature = transaction.get_signature();
|
||||
let serialized_tx = serialize(transaction)?;
|
||||
let serialized = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
|
||||
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
Ok((serialized, *signature))
|
||||
serialization::serialize_transaction_sync(transaction, encoding)
|
||||
}
|
||||
|
||||
pub async fn serialize_smart_transaction_and_encode(
|
||||
|
||||
@@ -64,7 +64,7 @@ impl FlashBlockClient {
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// FlashBlock API format
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ impl JitoClient {
|
||||
|
||||
pub async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"id": 1,
|
||||
|
||||
@@ -65,7 +65,7 @@ impl LightspeedClient {
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
|
||||
@@ -69,7 +69,7 @@ impl NextBlockClient {
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"transaction": {
|
||||
|
||||
+14
-16
@@ -52,8 +52,8 @@ impl Node1Client {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = Client::builder()
|
||||
// Optimized connection pool settings for high performance
|
||||
.pool_idle_timeout(Duration::from_secs(120))
|
||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
||||
.pool_idle_timeout(Duration::from_secs(300))
|
||||
.pool_max_idle_per_host(4)
|
||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
||||
@@ -90,16 +90,16 @@ impl Node1Client {
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
||||
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Node1 ping request failed: {}", e);
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Send ping request
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Node1 ping request failed: {}", e);
|
||||
}
|
||||
@@ -125,24 +125,22 @@ impl Node1Client {
|
||||
format!("{}/ping", endpoint)
|
||||
};
|
||||
|
||||
// Send GET request to /ping endpoint (no api-key required)
|
||||
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||
let response = http_client.get(&ping_url)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
// ping successful, connection remains active
|
||||
// Can optionally log, but to reduce noise, not printing here
|
||||
} else {
|
||||
eprintln!("Node1 ping request returned non-success status: {}", response.status());
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!("Node1 ping request returned non-success status: {}", status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"jsonrpc": "2.0",
|
||||
|
||||
+56
-21
@@ -1,4 +1,4 @@
|
||||
//! 交易序列化模块
|
||||
//! Transaction serialization module.
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::Engine;
|
||||
@@ -14,7 +14,7 @@ use crate::perf::{
|
||||
compiler_optimization::CompileTimeOptimizedEventProcessor,
|
||||
};
|
||||
|
||||
/// 零分配序列化器 - 使用缓冲池避免运行时分配
|
||||
/// Zero-allocation serializer using a buffer pool to avoid runtime allocation.
|
||||
pub struct ZeroAllocSerializer {
|
||||
buffer_pool: Arc<ArrayQueue<Vec<u8>>>,
|
||||
buffer_size: usize,
|
||||
@@ -24,7 +24,7 @@ impl ZeroAllocSerializer {
|
||||
pub fn new(pool_size: usize, buffer_size: usize) -> Self {
|
||||
let pool = ArrayQueue::new(pool_size);
|
||||
|
||||
// 预分配缓冲区
|
||||
// Pre-allocate buffers
|
||||
for _ in 0..pool_size {
|
||||
let mut buffer = Vec::with_capacity(buffer_size);
|
||||
buffer.resize(buffer_size, 0);
|
||||
@@ -38,14 +38,14 @@ impl ZeroAllocSerializer {
|
||||
}
|
||||
|
||||
pub fn serialize_zero_alloc<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> {
|
||||
// 尝试从池中获取缓冲区
|
||||
// Try to get a buffer from the pool
|
||||
let mut buffer = self.buffer_pool.pop().unwrap_or_else(|| {
|
||||
let mut buf = Vec::with_capacity(self.buffer_size);
|
||||
buf.resize(self.buffer_size, 0);
|
||||
buf
|
||||
});
|
||||
|
||||
// 序列化到缓冲区
|
||||
// Serialize into buffer
|
||||
let serialized = bincode::serialize(data)?;
|
||||
buffer.clear();
|
||||
buffer.extend_from_slice(&serialized);
|
||||
@@ -54,11 +54,11 @@ impl ZeroAllocSerializer {
|
||||
}
|
||||
|
||||
pub fn return_buffer(&self, buffer: Vec<u8>) {
|
||||
// 归还缓冲区到池中
|
||||
// Return buffer to the pool
|
||||
let _ = self.buffer_pool.push(buffer);
|
||||
}
|
||||
|
||||
/// 获取池统计信息
|
||||
/// Get pool statistics.
|
||||
pub fn get_pool_stats(&self) -> (usize, usize) {
|
||||
let available = self.buffer_pool.len();
|
||||
let capacity = self.buffer_pool.capacity();
|
||||
@@ -66,32 +66,32 @@ impl ZeroAllocSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
/// 全局序列化器实例
|
||||
/// Global serializer instance.
|
||||
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
|
||||
Arc::new(ZeroAllocSerializer::new(
|
||||
10_000, // 池大小
|
||||
256 * 1024, // 缓冲区大小: 256KB
|
||||
10_000, // Pool size
|
||||
256 * 1024, // Buffer size: 256KB
|
||||
))
|
||||
});
|
||||
|
||||
/// 🚀 编译时优化的事件处理器 (零运行时开销)
|
||||
/// Compile-time optimized event processor (zero runtime cost).
|
||||
static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor =
|
||||
CompileTimeOptimizedEventProcessor::new();
|
||||
|
||||
/// Base64 编码器
|
||||
/// Base64 encoder.
|
||||
pub struct Base64Encoder;
|
||||
|
||||
impl Base64Encoder {
|
||||
#[inline(always)]
|
||||
pub fn encode(data: &[u8]) -> String {
|
||||
// 使用编译时优化的哈希进行快速路由
|
||||
// Use compile-time optimized hash for fast routing
|
||||
let _route = if !data.is_empty() {
|
||||
COMPILE_TIME_PROCESSOR.route_event_zero_cost(data[0])
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// 使用 SIMD 加速的 Base64 编码
|
||||
// Use SIMD-accelerated Base64 encoding
|
||||
SIMDSerializer::encode_base64_simd(data)
|
||||
}
|
||||
|
||||
@@ -105,32 +105,67 @@ impl Base64Encoder {
|
||||
}
|
||||
}
|
||||
|
||||
/// 交易序列化
|
||||
/// Sync serialize + encode using buffer pool; use in hot path to reduce allocs.
|
||||
pub fn serialize_transaction_sync(
|
||||
transaction: &impl SerializableTransaction,
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<(String, Signature)> {
|
||||
let signature = transaction.get_signature();
|
||||
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
||||
let serialized = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||
UiTransactionEncoding::Base64 => STANDARD.encode(&serialized_tx),
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
SERIALIZER.return_buffer(serialized_tx);
|
||||
Ok((serialized, *signature))
|
||||
}
|
||||
|
||||
/// Serialize a transaction (async; no I/O, kept for API compatibility).
|
||||
pub async fn serialize_transaction(
|
||||
transaction: &impl SerializableTransaction,
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<(String, Signature)> {
|
||||
let signature = transaction.get_signature();
|
||||
|
||||
// 使用零分配序列化
|
||||
// Use zero-allocation serialization
|
||||
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
|
||||
|
||||
let serialized = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||
UiTransactionEncoding::Base64 => {
|
||||
// 使用 SIMD 优化的 Base64 编码
|
||||
// Use SIMD-optimized Base64 encoding
|
||||
STANDARD.encode(&serialized_tx)
|
||||
}
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
|
||||
// 立即归还缓冲区到池中
|
||||
// Return buffer to pool immediately
|
||||
SERIALIZER.return_buffer(serialized_tx);
|
||||
|
||||
Ok((serialized, *signature))
|
||||
}
|
||||
|
||||
/// 批量交易序列化
|
||||
/// Sync batch serialize + encode using buffer pool.
|
||||
pub fn serialize_transactions_batch_sync(
|
||||
transactions: &[impl SerializableTransaction],
|
||||
encoding: UiTransactionEncoding,
|
||||
) -> Result<Vec<String>> {
|
||||
let mut results = Vec::with_capacity(transactions.len());
|
||||
for tx in transactions {
|
||||
let serialized_tx = SERIALIZER.serialize_zero_alloc(tx, "transaction")?;
|
||||
let encoded = match encoding {
|
||||
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
|
||||
UiTransactionEncoding::Base64 => STANDARD.encode(&serialized_tx),
|
||||
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
|
||||
};
|
||||
SERIALIZER.return_buffer(serialized_tx);
|
||||
results.push(encoded);
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// Batch transaction serialization.
|
||||
pub async fn serialize_transactions_batch(
|
||||
transactions: &[impl SerializableTransaction],
|
||||
encoding: UiTransactionEncoding,
|
||||
@@ -153,7 +188,7 @@ pub async fn serialize_transactions_batch(
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
/// 获取序列化器统计信息
|
||||
/// Get serializer statistics.
|
||||
pub fn get_serializer_stats() -> (usize, usize) {
|
||||
SERIALIZER.get_pool_stats()
|
||||
}
|
||||
@@ -168,7 +203,7 @@ mod tests {
|
||||
let encoded = Base64Encoder::encode(data);
|
||||
assert!(!encoded.is_empty());
|
||||
|
||||
// 验证可以正确解码
|
||||
// Verify it decodes correctly
|
||||
let decoded = STANDARD.decode(&encoded).unwrap();
|
||||
assert_eq!(&decoded[..data.len()], data);
|
||||
}
|
||||
|
||||
+18
-11
@@ -50,8 +50,8 @@ impl StelliumClient {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = Client::builder()
|
||||
// Optimized connection pool settings for high performance
|
||||
.pool_idle_timeout(Duration::from_secs(120))
|
||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
||||
.pool_idle_timeout(Duration::from_secs(300))
|
||||
.pool_max_idle_per_host(4)
|
||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
||||
@@ -89,21 +89,28 @@ impl StelliumClient {
|
||||
let stop_ping = self.keep_alive_running.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
||||
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
let url = format!("{}/{}", endpoint, auth_token);
|
||||
if let Ok(resp) = http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
|
||||
let status = resp.status();
|
||||
let _ = resp.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!(" [Stellium] Ping failed with status: {}", status);
|
||||
}
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Send ping request
|
||||
let url = format!("{}/{}", endpoint, auth_token);
|
||||
match http_client.get(&url).send().await {
|
||||
match http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
|
||||
Ok(response) => {
|
||||
if !response.status().is_success() {
|
||||
eprintln!(" [Stellium] Ping failed with status: {}", response.status());
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!(" [Stellium] Ping failed with status: {}", status);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -116,7 +123,7 @@ impl StelliumClient {
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Stellium uses standard Solana sendTransaction format
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
|
||||
+14
-16
@@ -78,8 +78,8 @@ impl TemporalClient {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = Client::builder()
|
||||
// Optimized connection pool settings for high performance
|
||||
.pool_idle_timeout(Duration::from_secs(120))
|
||||
.pool_max_idle_per_host(256) // Increased from 64 to 256
|
||||
.pool_idle_timeout(Duration::from_secs(300))
|
||||
.pool_max_idle_per_host(4)
|
||||
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
|
||||
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
|
||||
.http2_keep_alive_interval(Duration::from_secs(10))
|
||||
@@ -116,16 +116,16 @@ impl TemporalClient {
|
||||
let stop_ping = self.stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds
|
||||
|
||||
// Immediate first ping to warm connection and reduce first-submit cold start latency
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Temporal ping request failed: {}", e);
|
||||
}
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Send ping request
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
eprintln!("Temporal ping request failed: {}", e);
|
||||
}
|
||||
@@ -151,24 +151,22 @@ impl TemporalClient {
|
||||
format!("{}/ping", endpoint)
|
||||
};
|
||||
|
||||
// Send GET request to /ping endpoint
|
||||
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
|
||||
let response = http_client.get(&ping_url)
|
||||
.timeout(Duration::from_millis(1500))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
// ping successful, connection remains active
|
||||
// Can optionally log, but to reduce noise, not printing here
|
||||
} else {
|
||||
eprintln!("Temporal ping request returned non-success status: {}", response.status());
|
||||
let status = response.status();
|
||||
let _ = response.bytes().await;
|
||||
if !status.is_success() {
|
||||
eprintln!("Temporal ping request returned non-success status: {}", status);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
// Build request body according to Nozomi documentation requirements
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
|
||||
@@ -64,7 +64,7 @@ impl ZeroSlotClient {
|
||||
|
||||
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
|
||||
let start_time = Instant::now();
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?;
|
||||
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let request_body = serde_json::to_string(&json!({
|
||||
"jsonrpc": "2.0",
|
||||
|
||||
@@ -107,6 +107,8 @@ impl PumpFunParams {
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
pub fn from_dev_trade(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
@@ -118,6 +120,7 @@ impl PumpFunParams {
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
@@ -127,6 +130,7 @@ impl PumpFunParams {
|
||||
max_sol_cost,
|
||||
creator,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
@@ -137,6 +141,8 @@ impl PumpFunParams {
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
@@ -150,6 +156,7 @@ impl PumpFunParams {
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
@@ -161,6 +168,7 @@ impl PumpFunParams {
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
|
||||
Reference in New Issue
Block a user