Compare commits

...
8 Commits
Author SHA1 Message Date
WoodandCursor 8f6cc6fb08 chore: release v3.4.1 - bump version and update README crates.io docs
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 21:42:09 +08:00
WoodandCursor 5e4977f6a6 perf(swqos): reduce submit latency and fix high latency after 5 min idle
- Make serialize_transaction_and_encode sync and route through buffer pool
  (serialization::serialize_transaction_sync) to cut allocs on hot path
- Build single-submit body with format! instead of json!+to_string() in bloxroute
- Add serialize_transactions_batch_sync; bloxroute batch uses it and format! for entries
- Fix first-submit cold start: immediate first ping, then 30s keepalive interval
- Fix high latency after ~5 min: pool_max_idle_per_host=4, pool_idle_timeout=300s
  (BlockRazor, Temporal, Node1, Astralane, Stellium) so submit reuses ping connection
- Ping: 1.5s timeout, consume response body so connection returns to pool
- poll_transaction_confirmation: avoid cloning status.value[0], use ref
- Translate all swqos comments to English

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 02:18:02 +08:00
WoodandCursor c27e479659 feat: pass is_cashback_coin from events; PumpFun examples use sol-parser-sdk only
- PumpFunParams/BondingCurve: from_trade/from_dev_trade take is_cashback_coin parameter
- pumpfun_copy_trading and pumpfun_sniper_trading use sol-parser-sdk for gRPC, pass e.is_cashback_coin
- address_lookup/nonce_cache call sites updated; README EN/CN Cashback and example notes

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 01:34:12 +08:00
WoodandCursor 9a8e3ffb2d docs: README 中英文更新 - Cashback 章节与大纲、示例与表格同步
- 大纲增加 Cashback 支持(PumpFun/PumpSwap)入口
- 新增 Cashback 说明:交易侧无需改代码,事件解析可获返现字段
- 英文:Method 1 增加 with_wsol_ata_config 可选示例
- 中文:创建客户端拆为方式一/方式二,表格增加 shared_infrastructure,特性增加共享基础设施

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 01:01:21 +08:00
WoodandGitHub 43d1369d4e Merge pull request #77 from 0xfnzero/feature/pump-cashback
feat: PumpFun & PumpSwap Cashback support
2026-02-19 00:44:47 +08:00
WoodandCursor a048f3b6ad feat: PumpFun & PumpSwap Cashback support
- Sync IDL from pump-public-docs into idl/
- Add is_cashback_coin to BondingCurve and Pool
- Pump sell: append UserVolumeAccumulator as remaining account when cashback coin
- PumpSwap buy/sell: append cashback remaining accounts after fee_config/fee_program
- Add claim_cashback instructions and TradingClient.claim_cashback_pumpfun/pumpswap()
- Add docs/PUMP_CASHBACK_README.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-18 23:01:14 +08:00
WoodandCursor feaac5ddd2 fix: WSOL ATA creation in background with retry/timeout; silence unused and deprecated warnings
- lib: WSOL ATA creation moved to background to avoid blocking startup; ensure_wsol_ata adds 3 retries and 10s timeout
- lib: cfg(feature = "perf-trace") for DEFAULT_SLIPPAGE usage
- gas_fee_strategy, bloxroute, transaction_builder, async_executor, executor: prefix unused vars/params with underscore or allow
- pumpswap: add allow(deprecated) for get_program_accounts_with_config

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 23:38:59 +08:00
WoodandGitHub 710a8d482f Create context7.json 2026-02-09 05:42:27 +08:00
38 changed files with 16941 additions and 444 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "sol-trade-sdk" name = "sol-trade-sdk"
version = "3.4.0" version = "3.4.1"
edition = "2021" edition = "2021"
authors = [ authors = [
"William <byteblock6@gmail.com>", "William <byteblock6@gmail.com>",
+24 -8
View File
@@ -47,10 +47,11 @@
- [📋 Example Usage](#-example-usage) - [📋 Example Usage](#-example-usage)
- [⚡ Trading Parameters](#-trading-parameters) - [⚡ Trading Parameters](#-trading-parameters)
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table) - [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
- [⚙️ SWQOS Service Configuration](#-swqos-service-configuration) - [⚙️ SWQoS Service Configuration](#-swqos-service-configuration)
- [🔧 Middleware System](#-middleware-system) - [🔧 Middleware System](#-middleware-system)
- [🔍 Address Lookup Tables](#-address-lookup-tables) - [🔍 Address Lookup Tables](#-address-lookup-tables)
- [🔍 Nonce Cache](#-nonce-cache) - [🔍 Nonce Cache](#-nonce-cache)
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
- [🛡️ MEV Protection Services](#-mev-protection-services) - [🛡️ MEV Protection Services](#-mev-protection-services)
- [📁 Project Structure](#-project-structure) - [📁 Project Structure](#-project-structure)
- [📄 License](#-license) - [📄 License](#-license)
@@ -71,7 +72,7 @@
8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail 8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations 9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution 10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
11. **Shared Infrastructure**: Share expensive RPC and SWQOS clients across multiple wallets for reduced resource usage 11. **Shared Infrastructure**: Share expensive RPC and SWQoS clients across multiple wallets for reduced resource usage
## 📦 Installation ## 📦 Installation
@@ -88,14 +89,14 @@ Add the dependency to your `Cargo.toml`:
```toml ```toml
# Add to your Cargo.toml # Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" } sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.1" }
``` ```
### Use crates.io ### Use crates.io
```toml ```toml
# Add to your Cargo.toml # Add to your Cargo.toml
sol-trade-sdk = "3.4.0" sol-trade-sdk = "3.4.1"
``` ```
## 🛠️ Usage Examples ## 🛠️ Usage Examples
@@ -113,7 +114,7 @@ let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
// RPC URL // RPC URL
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
let commitment = CommitmentConfig::processed(); let commitment = CommitmentConfig::processed();
// Multiple SWQOS services can be configured // Multiple SWQoS services can be configured
let swqos_configs: Vec<SwqosConfig> = vec![ let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()), SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
@@ -122,6 +123,10 @@ let swqos_configs: Vec<SwqosConfig> = vec![
// Create TradeConfig instance // Create TradeConfig instance
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); 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
// Create TradingClient // Create TradingClient
let client = TradingClient::new(Arc::new(payer), trade_config).await; let client = TradingClient::new(Arc::new(payer), trade_config).await;
``` ```
@@ -218,16 +223,16 @@ Please ensure that the parameters your trading logic depends on are available in
| Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) | | Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) | | Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
### ⚙️ SWQOS Service Configuration ### ⚙️ SWQoS Service Configuration
When configuring SWQOS services, note the different parameter requirements for each service: When configuring SWQoS services, note the different parameter requirements for each service:
- **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`) - **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`)
- **Other MEV services**: The first parameter is the API Token - **Other MEV services**: The first parameter is the API Token
#### Custom URL Support #### Custom URL Support
Each SWQOS service now supports an optional custom URL parameter: Each SWQoS service now supports an optional custom URL parameter:
```rust ```rust
// Using custom URL (third parameter) // Using custom URL (third parameter)
@@ -273,6 +278,17 @@ Address Lookup Tables (ALT) allow you to optimize transaction size and reduce fe
Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md). Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md).
## 💰 Cashback Support (PumpFun / PumpSwap)
PumpFun and PumpSwap support **cashback** for eligible tokens: part of the trading fee can be returned to the user. The SDK **must know** whether the token has cashback enabled so that buy/sell instructions include the correct accounts (e.g. `UserVolumeAccumulator` as remaining account for cashback coins).
- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step.
- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK:
- **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvents `is_cashback_enabled` or BondingCurves `is_cashback_coin`).
- **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data.
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
## 🛡️ MEV Protection Services ## 🛡️ MEV Protection Services
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos) You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
+40 -20
View File
@@ -47,10 +47,11 @@
- [📋 使用示例](#-使用示例) - [📋 使用示例](#-使用示例)
- [⚡ 交易参数](#-交易参数) - [⚡ 交易参数](#-交易参数)
- [📊 使用示例汇总表格](#-使用示例汇总表格) - [📊 使用示例汇总表格](#-使用示例汇总表格)
- [⚙️ SWQOS 服务配置说明](#-swqos-服务配置说明) - [⚙️ SWQoS 服务配置说明](#-swqos-服务配置说明)
- [🔧 中间件系统说明](#-中间件系统说明) - [🔧 中间件系统说明](#-中间件系统说明)
- [🔍 地址查找表](#-地址查找表) - [🔍 地址查找表](#-地址查找表)
- [🔍 Nonce 缓存](#-nonce-缓存) - [🔍 Nonce 缓存](#-nonce-缓存)
- [💰 Cashback 支持(PumpFun / PumpSwap](#-cashback-支持pumpfun--pumpswap)
- [🛡️ MEV 保护服务](#-mev-保护服务) - [🛡️ MEV 保护服务](#-mev-保护服务)
- [📁 项目结构](#-项目结构) - [📁 项目结构](#-项目结构)
- [📄 许可证](#-许可证) - [📄 许可证](#-许可证)
@@ -71,6 +72,7 @@
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败 8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作 9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除 10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
## 📦 安装 ## 📦 安装
@@ -87,14 +89,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml ```toml
# 添加到您的 Cargo.toml # 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" } sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.1" }
``` ```
### 使用 crates.io ### 使用 crates.io
```toml ```toml
# 添加到您的 Cargo.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 实例 #### 1. 创建 TradingClient 实例
参考 [示例:创建 TradingClient 实例](examples/trading_client/src/main.rs)。 可参考 [示例:创建 TradingClient 实例](examples/trading_client/src/main.rs)。
**方式一:简单创建(单钱包)**
```rust ```rust
// 钱包 // 钱包
let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
// RPC 地址 // RPC 地址
let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string(); let rpc_url = "https://mainnet.helius-rpc.com/?api-key=xxxxxx".to_string();
let commitment = CommitmentConfig::processed(); let commitment = CommitmentConfig::processed();
// 可配置多个SWQOS服务 // 可配置多个 SWQoS 服务
let swqos_configs: Vec<SwqosConfig> = vec![ let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()), SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::ZeroSlot("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Astralane("your api_token".to_string(), SwqosRegion::Frankfurt, None),
]; ];
// 创建 TradeConfig 实例 // 创建 TradeConfig 实例
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
// 可选:自定义 WSOL ATA Seed 优化设置 // 可选:自定义 WSOL ATA Seed 优化
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment) // let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
// .with_wsol_ata_config( // .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
// true, // create_wsol_ata_on_startup: 启动时检查并创建 WSOL ATA(默认: true
// true // use_seed_optimize: 全局启用所有 ATA 操作的 seed 优化(默认: true
// );
// 创建 TradingClient 客户端 // 创建 TradingClient
let client = TradingClient::new(Arc::new(payer), trade_config).await; let client = TradingClient::new(Arc::new(payer), trade_config).await;
``` ```
**方式二:共享基础设施(多钱包)**
多钱包场景下可先创建一份基础设施,再复用到多个钱包。参见 [示例:共享基础设施](examples/shared_infrastructure/src/main.rs)。
```rust
// 创建一次基础设施(开销较大)
let infra_config = InfrastructureConfig::new(rpc_url, swqos_configs, commitment);
let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
// 基于同一基础设施创建多个客户端(开销小)
let client1 = TradingClient::from_infrastructure(Arc::new(payer1), infrastructure.clone(), true);
let client2 = TradingClient::from_infrastructure(Arc::new(payer2), infrastructure.clone(), true);
```
#### 2. 配置 Gas Fee 策略 #### 2. 配置 Gas Fee 策略
有关 Gas Fee 策略的详细信息,请参阅 [Gas Fee 策略参考手册](docs/GAS_FEE_STRATEGY_CN.md)。 有关 Gas Fee 策略的详细信息,请参阅 [Gas Fee 策略参考手册](docs/GAS_FEE_STRATEGY_CN.md)。
@@ -198,6 +206,7 @@ client.buy(buy_params).await?;
| 描述 | 运行命令 | 源码路径 | | 描述 | 运行命令 | 源码路径 |
|------|---------|----------| |------|---------|----------|
| 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) | | 创建和配置 TradingClient 实例 | `cargo run --package trading_client` | [examples/trading_client](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/trading_client/src/main.rs) |
| 多钱包共享基础设施 | `cargo run --package shared_infrastructure` | [examples/shared_infrastructure](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/shared_infrastructure/src/main.rs) |
| PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) | | PumpFun 代币狙击交易 | `cargo run --package pumpfun_sniper_trading` | [examples/pumpfun_sniper_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_sniper_trading/src/main.rs) |
| PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) | | PumpFun 代币跟单交易 | `cargo run --package pumpfun_copy_trading` | [examples/pumpfun_copy_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpfun_copy_trading/src/main.rs) |
| PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) | | PumpSwap 交易操作 | `cargo run --package pumpswap_trading` | [examples/pumpswap_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/pumpswap_trading/src/main.rs) |
@@ -213,16 +222,16 @@ client.buy(buy_params).await?;
| Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) | | Seed 优化交易示例 | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
| Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) | | Gas费用策略示例 | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
### ⚙️ SWQOS 服务配置说明 ### ⚙️ SWQoS 服务配置说明
在配置 SWQOS 服务时,需要注意不同服务的参数要求: 在配置 SWQoS 服务时,需要注意不同服务的参数要求:
- **Jito**: 第一个参数为 UUID(如无 UUID 请传入空字符串 `""` - **Jito**: 第一个参数为 UUID(如无 UUID 请传入空字符串 `""`
- 其他的MEV服务,第一个参数为 API Token - 其他的MEV服务,第一个参数为 API Token
#### 自定义 URL 支持 #### 自定义 URL 支持
每个 SWQOS 服务现在都支持可选的自定义 URL 参数: 每个 SWQoS 服务现在都支持可选的自定义 URL 参数:
```rust ```rust
// 使用自定义 URL(第三个参数) // 使用自定义 URL(第三个参数)
@@ -268,6 +277,17 @@ let middleware_manager = MiddlewareManager::new()
使用 Durable Nonce 来实现交易重放保护和优化交易处理。详细信息请参阅 [Nonce 使用指南](docs/NONCE_CACHE_CN.md)。 使用 Durable Nonce 来实现交易重放保护和优化交易处理。详细信息请参阅 [Nonce 使用指南](docs/NONCE_CACHE_CN.md)。
## 💰 Cashback 支持(PumpFun / PumpSwap
PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还给用户。SDK **必须知道**该代币是否开启返现,才能为 buy/sell 指令传入正确的账户(例如返现代币需要把 `UserVolumeAccumulator` 作为 remaining account)。
- **参数来自 RPC 时**:使用 `PumpFunParams::from_mint_by_rpc``PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc` 时,SDK 会从链上读取 `is_cashback_coin`,无需额外传入。
- **参数来自事件/解析器时**:若根据交易事件(如 [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk))构建参数,**必须**把返现标志传给 SDK:
- **PumpFun**`PumpFunParams::from_trade(..., is_cashback_coin)``PumpFunParams::from_dev_trade(..., is_cashback_coin)` 最后一个参数为 `is_cashback_coin`。从解析出的事件传入(如 sol-parser-sdk 的 `PumpFunTradeEvent.is_cashback_coin`)。
- **PumpSwap**`PumpSwapParams` 有字段 `is_cashback_coin`。手动构造参数(如从池/交易事件)时,从解析到的池或事件数据中设置该字段。
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`
- **领取返现**:使用 `client.claim_cashback_pumpfun()``client.claim_cashback_pumpswap(...)` 领取累计的返现。
## 🛡️ MEV 保护服务 ## 🛡️ MEV 保护服务
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos) 可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
+4
View File
@@ -0,0 +1,4 @@
{
"url": "https://context7.com/0xfnzero/sol-trade-sdk",
"public_key": "pk_ShleAZazFTUV8ORpmH4jy"
}
+74
View File
@@ -0,0 +1,74 @@
# Pump Cashback 集成说明
本 SDK 已支持 [Pump Cashback Rewards](https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CASHBACK_README.md):在启用 cashback 的币种上交易时,用户可获得手续费返还而非支付给创作者。
## 行为概览
- **Bonding Curve (Pump)**
- **Buy**:无需改指令,若币种启用 cashback 会自动累计。
- **Sell**:当 `bonding_curve.is_cashback_coin == true` 时,SDK 会在指令中追加 `UserVolumeAccumulator` PDAremaining account),用于累计可领取的 cashback。
- **Pump Swap**
- **Buy**:当 `PumpSwapParams.is_cashback_coin == true` 时,会追加 UserVolumeAccumulator 的 **WSOL ATA** 作为 remaining account。
- **Sell**:当 `is_cashback_coin == true` 时,会追加 **WSOL ATA**0th)和 **UserVolumeAccumulator PDA**1st)作为 remaining accounts。
`PumpFunParams` 通过 `from_mint_by_rpc` 拉取 bonding curve 时会解析链上 `is_cashback_coin``PumpSwapParams` 通过 `from_pool_address_by_rpc` / `from_mint_by_rpc` 拉取 pool 时会解析 `is_cashback_coin`
## 领取 Cashback
### 推荐:使用 TradingClient 一键领取
若已有 `TradingClient`(例如用于买卖的同一个客户端),可直接调用以下方法,内部会完成构建交易、签名与发送:
```rust
// 领取 Pump 曲线产生的 Cashback(到账为 native SOL
let sig = client.claim_cashback_pumpfun().await?;
// 领取 PumpSwap 产生的 Cashback(到账为 WSOL,自动确保用户 WSOL ATA 存在)
let sig = client.claim_cashback_pumpswap().await?;
```
- **`claim_cashback_pumpfun()`**:领取 Bonding Curve (Pump) 的返还,到账为钱包 SOL。
- **`claim_cashback_pumpswap()`**:领取 PumpSwap (AMM) 的返还,到账为用户的 WSOL ATA;若用户尚无 WSOL ATA 会先自动创建再领取。
### 仅构建指令(自行组交易时使用)
#### Bonding Curve (Pump)
将 native lamports 从 UserVolumeAccumulator 转到用户钱包:
```rust
use sol_trade_sdk::instruction::pumpfun;
let ix = pumpfun::claim_cashback_pumpfun_instruction(&payer.pubkey());
// 将 ix 放入交易并发送
```
#### Pump Swap (AMM)
将 WSOL 从 UserVolumeAccumulator 的 WSOL ATA 转到用户的 WSOL ATA。**调用前需确保用户 WSOL ATA 已存在**(或使用上面的 `claim_cashback_pumpswap()` 会自动处理):
```rust
use sol_trade_sdk::instruction::pumpswap;
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
use sol_trade_sdk::constants::TOKEN_PROGRAM;
let ix = pumpswap::claim_cashback_pumpswap_instruction(
&payer.pubkey(),
WSOL_TOKEN_ACCOUNT,
TOKEN_PROGRAM,
);
```
## 读取未领取金额
- **Pump (Bonding Curve)**:读 Pump 程序的 `UserVolumeAccumulator` PDA 的 lamports,减去维持账户所需的 rent-exempt 金额,即为未领取 cashbacklamports)。
- **Pump Swap**:读 Pump AMM 程序的 UserVolumeAccumulator 的 **WSOL ATA** 的 token balance,即为未领取 cashbackWSOL 数量)。
PDA 推导(本 SDK 已实现):
- Pump`instruction::utils::pumpfun::get_user_volume_accumulator_pda(user)`
- Pump AMM`instruction::utils::pumpswap::get_user_volume_accumulator_pda(user)`WSOL ATA`instruction::utils::pumpswap::get_user_volume_accumulator_wsol_ata(user)`
## IDL 来源
根目录 `idl/` 下的 `pump.json``pump_amm.json``pump_fees.json` 从 [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs) 的 `idl` 目录同步,便于与官方 IDL 对照和后续升级。
+2 -1
View File
@@ -143,13 +143,14 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
trade_info.mint, trade_info.mint,
trade_info.creator, trade_info.creator,
trade_info.creator_vault, trade_info.creator_vault,
trade_info.virtual_sol_reserves, trade_info.virtual_token_reserves,
trade_info.virtual_sol_reserves, trade_info.virtual_sol_reserves,
trade_info.real_token_reserves, trade_info.real_token_reserves,
trade_info.real_sol_reserves, trade_info.real_sol_reserves,
None, None,
trade_info.fee_recipient, trade_info.fee_recipient,
trade_info.token_program, trade_info.token_program,
false, // is_cashback_coin: set from event/parser when available
)), )),
address_lookup_table_account: address_lookup_table_account, address_lookup_table_account: address_lookup_table_account,
wait_transaction_confirmed: true, wait_transaction_confirmed: true,
+1
View File
@@ -150,6 +150,7 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
None, None,
trade_info.fee_recipient, trade_info.fee_recipient,
trade_info.token_program, trade_info.token_program,
false, // is_cashback_coin: set from event/parser when available
)), )),
address_lookup_table_account: None, address_lookup_table_account: None,
wait_transaction_confirmed: true, wait_transaction_confirmed: true,
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
sol-trade-sdk = { path = "../.." } sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0" sol-parser-sdk = { path = "../../../sol-parser-sdk" }
solana-sdk = "3.0.0" solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] } solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
+115 -130
View File
@@ -1,8 +1,14 @@
use std::sync::{ //! PumpFun 跟单示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
atomic::{AtomicBool, Ordering}, //!
Arc, //! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。
};
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
use sol_parser_sdk::grpc::{
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
TransactionFilter, YellowstoneGrpc,
};
use sol_parser_sdk::DexEvent;
use sol_trade_sdk::common::{ use sol_trade_sdk::common::{
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig, fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
}; };
@@ -16,143 +22,128 @@ use sol_trade_sdk::{
use solana_commitment_config::CommitmentConfig; use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair; use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer; use solana_sdk::signer::Signer;
use solana_streamer_sdk::match_event;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::YellowstoneGrpc;
// Global static flag to ensure transaction is executed only once
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false); static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to GRPC events..."); let _ = rustls::crypto::ring::default_provider().install_default();
println!("PumpFun 跟单示例(sol-parser-sdk gRPC...");
let grpc = YellowstoneGrpc::new( let config = ClientConfig {
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(), enable_metrics: false,
None, connection_timeout_ms: 10000,
)?; request_timeout_ms: 30000,
enable_tls: true,
let callback = create_event_callback(); order_mode: OrderMode::Unordered,
let protocols = vec![Protocol::PumpFun]; ..Default::default()
// Filter accounts
let account_include = vec![
PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID
];
let account_exclude = vec![];
let account_required = vec![];
// Listen to transaction data
let transaction_filter = TransactionFilter {
account_include: account_include.clone(),
account_exclude,
account_required,
}; };
// Listen to account data belonging to owner programs -> account event monitoring let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] }; .unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
let grpc = YellowstoneGrpc::new_with_config(
grpc_endpoint.clone(),
std::env::var("GRPC_AUTH_TOKEN").ok(),
config,
)?;
// listen to specific event type let protocols = vec![Protocol::PumpFun];
let event_type_filter = let transaction_filter = TransactionFilter::for_protocols(&protocols);
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] }; let account_filter = AccountFilter::for_protocols(&protocols);
let event_filter = EventTypeFilter::include_only(vec![
EventType::PumpFunBuy,
EventType::PumpFunSell,
EventType::PumpFunBuyExactSolIn,
EventType::PumpFunTrade,
]);
grpc.subscribe_events_immediate( let queue = grpc
protocols, .subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
None, .await?;
vec![transaction_filter],
vec![account_filter], println!("订阅已启动,等待一条 PumpFun 交易后执行跟单(仅一次)...\n");
Some(event_type_filter),
None, loop {
callback, if let Some(event) = queue.pop() {
) let run = match &event {
.await?; DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
DexEvent::PumpFunTrade(e) => {
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
_ => None,
};
if let Some(e) = run {
tokio::spawn(async move {
if let Err(err) = pumpfun_copy_trade(e).await {
eprintln!("跟单执行错误: {:?}", err);
std::process::exit(1);
}
std::process::exit(0);
});
break;
}
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
}
}
tokio::signal::ctrl_c().await?; tokio::signal::ctrl_c().await?;
Ok(()) Ok(())
} }
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
tokio::spawn(async move {
if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
}
},
});
}
}
/// Create SolanaTrade client
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> { async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed(); let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())]; let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
println!("✅ SolanaTrade client initialized successfully!");
Ok(solana_trade)
} }
/// PumpFun sniper trade async fn pumpfun_copy_trade(
/// This function demonstrates how to snipe a new token from a PumpFun trade event e: sol_parser_sdk::core::events::PumpFunTradeEvent,
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> { ) -> AnyResult<()> {
println!("Testing PumpFun trading...");
let client = create_solana_trade_client().await?; let client = create_solana_trade_client().await?;
let mint_pubkey = trade_info.mint; let mint_pubkey = e.mint;
let slippage_basis_points = Some(100); let slippage_basis_points = Some(100u64);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy( gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
150000,
150000,
500000,
500000,
0.001,
0.001,
);
// Buy tokens // 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析)
println!("Buying tokens from PumpFun..."); let buy_sol_amount = 100_000u64;
let buy_sol_amount = 100_000;
let buy_params = sol_trade_sdk::TradeBuyParams { let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpFun, dex_type: DexType::PumpFun,
input_token_type: TradeTokenType::SOL, input_token_type: TradeTokenType::SOL,
mint: mint_pubkey, mint: mint_pubkey,
input_token_amount: buy_sol_amount, input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points, slippage_basis_points,
recent_blockhash: Some(recent_blockhash), recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade( extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
trade_info.bonding_curve, e.bonding_curve,
trade_info.associated_bonding_curve, e.associated_bonding_curve,
trade_info.mint, e.mint,
trade_info.creator, e.creator,
trade_info.creator_vault, e.creator_vault,
trade_info.virtual_token_reserves, e.virtual_token_reserves,
trade_info.virtual_sol_reserves, e.virtual_sol_reserves,
trade_info.real_token_reserves, e.real_token_reserves,
trade_info.real_sol_reserves, e.real_sol_reserves,
None, None,
trade_info.fee_recipient, e.fee_recipient,
trade_info.token_program, e.token_program,
e.is_cashback_coin,
)), )),
address_lookup_table_account: None, address_lookup_table_account: None,
wait_transaction_confirmed: true, wait_transaction_confirmed: true,
@@ -167,43 +158,40 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
}; };
client.buy(buy_params).await?; client.buy(buy_params).await?;
// Sell tokens // 卖出:查询余额后卖出,同样传入 is_cashback_coin
println!("Selling tokens from PumpFun...");
let rpc = client.infrastructure.rpc.clone(); let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey(); let payer = client.payer.pubkey();
let account = get_associated_token_address_with_program_id_fast_use_seed( let account = get_associated_token_address_with_program_id_fast_use_seed(
&payer, &payer,
&mint_pubkey, &mint_pubkey,
&trade_info.token_program, &e.token_program,
client.use_seed_optimize, client.use_seed_optimize,
); );
let balance = rpc.get_token_account_balance(&account).await?; let balance = rpc.get_token_account_balance(&account).await?;
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap(); let amount_token = balance.amount.parse::<u64>().unwrap();
println!("Selling {} tokens", amount_token);
let sell_params = sol_trade_sdk::TradeSellParams { let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpFun, dex_type: DexType::PumpFun,
output_token_type: TradeTokenType::SOL, output_token_type: TradeTokenType::SOL,
mint: mint_pubkey, mint: mint_pubkey,
input_token_amount: amount_token, input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points, slippage_basis_points,
recent_blockhash: Some(recent_blockhash), recent_blockhash: Some(recent_blockhash),
with_tip: false, with_tip: false,
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade( extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
trade_info.bonding_curve, e.bonding_curve,
trade_info.associated_bonding_curve, e.associated_bonding_curve,
trade_info.mint, e.mint,
trade_info.creator, e.creator,
trade_info.creator_vault, e.creator_vault,
trade_info.virtual_token_reserves, e.virtual_token_reserves,
trade_info.virtual_sol_reserves, e.virtual_sol_reserves,
trade_info.real_token_reserves, e.real_token_reserves,
trade_info.real_sol_reserves, e.real_sol_reserves,
Some(true), Some(true),
trade_info.fee_recipient, e.fee_recipient,
trade_info.token_program, e.token_program,
e.is_cashback_coin,
)), )),
address_lookup_table_account: None, address_lookup_table_account: None,
wait_transaction_confirmed: true, wait_transaction_confirmed: true,
@@ -212,14 +200,11 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
close_mint_token_ata: false, close_mint_token_ata: false,
durable_nonce: None, durable_nonce: None,
fixed_output_token_amount: None, fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy, gas_fee_strategy,
simulate: false, simulate: false,
}; };
client.sell(sell_params).await?; client.sell(sell_params).await?;
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell) println!("跟单一次买+卖完成");
// creator_vault can be obtained from the trade event Ok(())
// Exit program
std::process::exit(0);
} }
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies] [dependencies]
sol-trade-sdk = { path = "../.." } sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0" sol-parser-sdk = { path = "../../../sol-parser-sdk" }
solana-sdk = "3.0.0" solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] } solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
+114 -84
View File
@@ -1,3 +1,15 @@
//! PumpFun 狙击示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
//!
//! 监听创建者首次买入(Create 后同笔/首笔 Buyis_created_buy == true),
//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
use sol_parser_sdk::grpc::{
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
TransactionFilter, YellowstoneGrpc,
};
use sol_parser_sdk::DexEvent;
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address; use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
use sol_trade_sdk::common::TradeConfig; use sol_trade_sdk::common::TradeConfig;
use sol_trade_sdk::TradeTokenType; use sol_trade_sdk::TradeTokenType;
@@ -10,108 +22,120 @@ use sol_trade_sdk::{
use solana_commitment_config::CommitmentConfig; use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair; use solana_sdk::signature::Keypair;
use solana_sdk::signer::Signer; use solana_sdk::signer::Signer;
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
use solana_streamer_sdk::streaming::event_parser::common::EventType;
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
/// Atomic flag to ensure the sniper trade is executed only once
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false); static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
/// Main entry point - subscribes to PumpFun events and executes sniper trades on token creation
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Subscribing to ShredStream events..."); let _ = rustls::crypto::ring::default_provider().install_default();
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?; println!("PumpFun 狙击示例(sol-parser-sdk gRPC...");
let callback = create_event_callback();
let protocols = vec![Protocol::PumpFun]; let config = ClientConfig {
let event_type_filter = EventTypeFilter { enable_metrics: false,
include: vec![EventType::PumpFunBuy, EventType::PumpFunSell, EventType::PumpFunCreateToken], connection_timeout_ms: 10000,
request_timeout_ms: 30000,
enable_tls: true,
order_mode: OrderMode::Unordered,
..Default::default()
}; };
println!("Starting to listen for events, press Ctrl+C to stop...");
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?; let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
let grpc = YellowstoneGrpc::new_with_config(
grpc_endpoint,
std::env::var("GRPC_AUTH_TOKEN").ok(),
config,
)?;
let protocols = vec![Protocol::PumpFun];
let transaction_filter = TransactionFilter::for_protocols(&protocols);
let account_filter = AccountFilter::for_protocols(&protocols);
let event_filter = EventTypeFilter::include_only(vec![
EventType::PumpFunCreate,
EventType::PumpFunBuy,
EventType::PumpFunBuyExactSolIn,
]);
let queue = grpc
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
.await?;
println!("订阅已启动,等待创建者首次买入(is_created_buy)后执行狙击(仅一次)...\n");
loop {
if let Some(event) = queue.pop() {
let run = match &event {
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
if e.is_created_buy && !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
Some(e.clone())
} else {
None
}
}
_ => None,
};
if let Some(e) = run {
tokio::spawn(async move {
if let Err(err) = pumpfun_sniper_trade(e).await {
eprintln!("狙击执行错误: {:?}", err);
std::process::exit(1);
}
std::process::exit(0);
});
break;
}
} else {
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
}
}
tokio::signal::ctrl_c().await?; tokio::signal::ctrl_c().await?;
Ok(()) Ok(())
} }
/// Create an event callback function that handles different types of events
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|event: Box<dyn UnifiedEvent>| {
match_event!(event, {
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
// Only process developer token creation events
if !e.is_dev_create_token_trade {
return;
}
// Ensure we only execute the trade once using atomic compare-and-swap
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
// Spawn a new task to handle the trading operation
tokio::spawn(async move {
if let Err(err) = pumpfun_sniper_trade_with_shreds(event_clone).await {
eprintln!("Error in copy trade: {:?}", err);
std::process::exit(0);
}
});
}
},
});
}
}
/// Create SolanaTrade client
/// Initializes a new SolanaTrade client with configuration
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> { async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
println!("🚀 Initializing SolanaTrade client...");
let payer = Keypair::from_base58_string("use_your_payer_keypair_here"); let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed(); let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())]; let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment); let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await; Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
println!("✅ SolanaTrade client initialized successfully!");
Ok(solana_trade)
} }
/// Execute PumpFun sniper trading strategy based on received token creation event async fn pumpfun_sniper_trade(
/// This function buys tokens immediately after creation and then sells all tokens e: sol_parser_sdk::core::events::PumpFunTradeEvent,
async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyResult<()> { ) -> AnyResult<()> {
println!("Testing PumpFun trading...");
let client = create_solana_trade_client().await?; let client = create_solana_trade_client().await?;
let mint_pubkey = trade_info.mint; let mint_pubkey = e.mint;
let slippage_basis_points = Some(300); let slippage_basis_points = Some(300u64);
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?; let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001); gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
// Buy tokens // 创建者首次买入:用 from_dev_trademax_sol_cost 用事件中的 sol_amount(可酌情加滑点)
println!("Buying tokens from PumpFun..."); let buy_sol_amount = 100_000u64;
let buy_sol_amount = 100_000; let max_sol_cost = e.sol_amount.saturating_add(e.sol_amount / 10); // 约 +10% 作为上限
let buy_params = sol_trade_sdk::TradeBuyParams { let buy_params = sol_trade_sdk::TradeBuyParams {
dex_type: DexType::PumpFun, dex_type: DexType::PumpFun,
input_token_type: TradeTokenType::SOL, input_token_type: TradeTokenType::SOL,
mint: mint_pubkey, mint: mint_pubkey,
input_token_amount: buy_sol_amount, input_token_amount: buy_sol_amount,
slippage_basis_points: slippage_basis_points, slippage_basis_points,
recent_blockhash: Some(recent_blockhash), recent_blockhash: Some(recent_blockhash),
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade( extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade(
trade_info.mint, e.mint,
trade_info.token_amount, e.token_amount,
trade_info.max_sol_cost, max_sol_cost,
trade_info.creator, e.creator,
trade_info.bonding_curve, e.bonding_curve,
trade_info.associated_bonding_curve, e.associated_bonding_curve,
trade_info.creator_vault, e.creator_vault,
None, None,
trade_info.fee_recipient, e.fee_recipient,
trade_info.token_program, e.token_program,
e.is_cashback_coin,
)), )),
address_lookup_table_account: None, address_lookup_table_account: None,
wait_transaction_confirmed: true, wait_transaction_confirmed: true,
@@ -126,26 +150,35 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
}; };
client.buy(buy_params).await?; client.buy(buy_params).await?;
// Sell tokens
println!("Selling tokens from PumpFun...");
let rpc = client.infrastructure.rpc.clone(); let rpc = client.infrastructure.rpc.clone();
let payer = client.payer.pubkey(); let payer = client.payer.pubkey();
let account = get_associated_token_address(&payer, &mint_pubkey); let account = get_associated_token_address(&payer, &mint_pubkey);
let balance = rpc.get_token_account_balance(&account).await?; let balance = rpc.get_token_account_balance(&account).await?;
println!("Balance: {:?}", balance);
let amount_token = balance.amount.parse::<u64>().unwrap(); let amount_token = balance.amount.parse::<u64>().unwrap();
println!("Selling {} tokens", amount_token);
let sell_params = sol_trade_sdk::TradeSellParams { let sell_params = sol_trade_sdk::TradeSellParams {
dex_type: DexType::PumpFun, dex_type: DexType::PumpFun,
output_token_type: TradeTokenType::SOL, output_token_type: TradeTokenType::SOL,
mint: mint_pubkey, mint: mint_pubkey,
input_token_amount: amount_token, input_token_amount: amount_token,
slippage_basis_points: slippage_basis_points, slippage_basis_points,
recent_blockhash: Some(recent_blockhash), recent_blockhash: Some(recent_blockhash),
with_tip: false, with_tip: false,
extension_params: DexParamEnum::PumpFun(PumpFunParams::immediate_sell(trade_info.creator_vault, trade_info.token_program, true)), extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
e.bonding_curve,
e.associated_bonding_curve,
e.mint,
e.creator,
e.creator_vault,
e.virtual_token_reserves,
e.virtual_sol_reserves,
e.real_token_reserves,
e.real_sol_reserves,
Some(true),
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
)),
address_lookup_table_account: None, address_lookup_table_account: None,
wait_transaction_confirmed: true, wait_transaction_confirmed: true,
create_output_token_ata: true, create_output_token_ata: true,
@@ -153,14 +186,11 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
close_mint_token_ata: false, close_mint_token_ata: false,
durable_nonce: None, durable_nonce: None,
fixed_output_token_amount: None, fixed_output_token_amount: None,
gas_fee_strategy: gas_fee_strategy, gas_fee_strategy,
simulate: false, simulate: false,
}; };
client.sell(sell_params).await?; client.sell(sell_params).await?;
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell) println!("狙击一次买+卖完成");
// creator_vault can be obtained from the trade event Ok(())
// Exit program after completing the trade
std::process::exit(0);
} }
+7098
View File
File diff suppressed because it is too large Load Diff
+6268
View File
File diff suppressed because it is too large Load Diff
+2761
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -60,9 +60,13 @@ pub struct BondingCurveAccount {
pub creator: Pubkey, pub creator: Pubkey,
/// Whether this is a mayhem mode token (Token2022) /// Whether this is a mayhem mode token (Token2022)
pub is_mayhem_mode: bool, pub is_mayhem_mode: bool,
/// Whether this coin has cashback enabled (creator fee redirected to users)
pub is_cashback_coin: bool,
} }
impl BondingCurveAccount { impl BondingCurveAccount {
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
pub fn from_dev_trade( pub fn from_dev_trade(
bonding_curve: Pubkey, bonding_curve: Pubkey,
mint: &Pubkey, mint: &Pubkey,
@@ -70,6 +74,7 @@ impl BondingCurveAccount {
dev_sol_amount: u64, dev_sol_amount: u64,
creator: Pubkey, creator: Pubkey,
is_mayhem_mode: bool, is_mayhem_mode: bool,
is_cashback_coin: bool,
) -> Self { ) -> Self {
let account = if bonding_curve != Pubkey::default() { let account = if bonding_curve != Pubkey::default() {
bonding_curve bonding_curve
@@ -87,9 +92,12 @@ impl BondingCurveAccount {
complete: false, complete: false,
creator: creator, creator: creator,
is_mayhem_mode: is_mayhem_mode, is_mayhem_mode: is_mayhem_mode,
is_cashback_coin,
} }
} }
/// When building from event/parser data (e.g. sol-parser-sdk), pass the token's cashback flag
/// so that sell instructions include the correct remaining accounts. From RPC use `from_mint_by_rpc` instead.
pub fn from_trade( pub fn from_trade(
bonding_curve: Pubkey, bonding_curve: Pubkey,
mint: Pubkey, mint: Pubkey,
@@ -99,6 +107,7 @@ impl BondingCurveAccount {
real_token_reserves: u64, real_token_reserves: u64,
real_sol_reserves: u64, real_sol_reserves: u64,
is_mayhem_mode: bool, is_mayhem_mode: bool,
is_cashback_coin: bool,
) -> Self { ) -> Self {
let account = if bonding_curve != Pubkey::default() { let account = if bonding_curve != Pubkey::default() {
bonding_curve bonding_curve
@@ -116,6 +125,7 @@ impl BondingCurveAccount {
complete: false, complete: false,
creator: creator, creator: creator,
is_mayhem_mode: is_mayhem_mode, is_mayhem_mode: is_mayhem_mode,
is_cashback_coin,
} }
} }
+4 -4
View File
@@ -300,7 +300,7 @@ impl GasFeeStrategy {
pub fn update_buy_tip(&self, buy_tip: f64) { pub fn update_buy_tip(&self, buy_tip: f64) {
self.strategies.rcu(|current_map| { self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone(); let mut new_map = (**current_map).clone();
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() { for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy { if *trade_type == TradeType::Buy {
value.tip = buy_tip; value.tip = buy_tip;
} }
@@ -314,7 +314,7 @@ impl GasFeeStrategy {
pub fn update_sell_tip(&self, sell_tip: f64) { pub fn update_sell_tip(&self, sell_tip: f64) {
self.strategies.rcu(|current_map| { self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone(); let mut new_map = (**current_map).clone();
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() { for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell { if *trade_type == TradeType::Sell {
value.tip = sell_tip; value.tip = sell_tip;
} }
@@ -328,7 +328,7 @@ impl GasFeeStrategy {
pub fn update_buy_cu_price(&self, buy_cu_price: u64) { pub fn update_buy_cu_price(&self, buy_cu_price: u64) {
self.strategies.rcu(|current_map| { self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone(); let mut new_map = (**current_map).clone();
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() { for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Buy { if *trade_type == TradeType::Buy {
value.cu_price = buy_cu_price; value.cu_price = buy_cu_price;
} }
@@ -342,7 +342,7 @@ impl GasFeeStrategy {
pub fn update_sell_cu_price(&self, sell_cu_price: u64) { pub fn update_sell_cu_price(&self, sell_cu_price: u64) {
self.strategies.rcu(|current_map| { self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone(); let mut new_map = (**current_map).clone();
for ((swqos_type, trade_type, strategy_type), value) in new_map.iter_mut() { for ((_swqos_type, trade_type, _strategy_type), value) in new_map.iter_mut() {
if *trade_type == TradeType::Sell { if *trade_type == TradeType::Sell {
value.cu_price = sell_cu_price; value.cu_price = sell_cu_price;
} }
+27 -2
View File
@@ -263,7 +263,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
global_constants::FEE_RECIPIENT_META global_constants::FEE_RECIPIENT_META
}; };
let accounts: [AccountMeta; 14] = [ let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META, global_constants::GLOBAL_ACCOUNT_META,
fee_recipient_meta, fee_recipient_meta,
AccountMeta::new_readonly(params.input_mint, false), AccountMeta::new_readonly(params.input_mint, false),
@@ -280,10 +280,17 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
accounts::FEE_PROGRAM_META, accounts::FEE_PROGRAM_META,
]; ];
// Cashback: Bonding Curve Sell expects UserVolumeAccumulator PDA at 0th remaining account (writable)
if bonding_curve.is_cashback_coin {
let user_volume_accumulator =
get_user_volume_accumulator_pda(&params.payer.pubkey()).unwrap();
accounts.push(AccountMeta::new(user_volume_accumulator, false));
}
instructions.push(Instruction::new_with_bytes( instructions.push(Instruction::new_with_bytes(
accounts::PUMPFUN, accounts::PUMPFUN,
&sell_data, &sell_data,
accounts.to_vec(), accounts,
)); ));
// Optional: Close token account // Optional: Close token account
@@ -302,3 +309,21 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
Ok(instructions) Ok(instructions)
} }
} }
/// Claim cashback for Bonding Curve (Pump program). Transfers native lamports from UserVolumeAccumulator to user.
pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option<Instruction> {
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
let accounts = vec![
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable, not signer)
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::PUMPFUN_META,
];
Some(Instruction::new_with_bytes(
accounts::PUMPFUN,
&CLAIM_CASHBACK_DISCRIMINATOR,
accounts,
))
}
+53 -2
View File
@@ -1,7 +1,8 @@
use crate::{ use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE, constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::pumpswap::{ instruction::utils::pumpswap::{
accounts, fee_recipient_ata, get_user_volume_accumulator_pda, BUY_DISCRIMINATOR, accounts, fee_recipient_ata, get_user_volume_accumulator_pda,
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR,
BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
}, },
trading::{ trading::{
@@ -183,6 +184,12 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
} }
accounts.push(accounts::FEE_CONFIG_META); accounts.push(accounts::FEE_CONFIG_META);
accounts.push(accounts::FEE_PROGRAM_META); accounts.push(accounts::FEE_PROGRAM_META);
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator (after named accounts per IDL)
if protocol_params.is_cashback_coin {
if let Some(wsol_ata) = get_user_volume_accumulator_wsol_ata(&params.payer.pubkey()) {
accounts.push(AccountMeta::new(wsol_ata, false));
}
}
// Create instruction data // Create instruction data
let mut data = [0u8; 24]; let mut data = [0u8; 24];
@@ -373,9 +380,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
false, false,
)); ));
} }
accounts.push(accounts::FEE_CONFIG_META); accounts.push(accounts::FEE_CONFIG_META);
accounts.push(accounts::FEE_PROGRAM_META); accounts.push(accounts::FEE_PROGRAM_META);
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator, remaining_accounts[1] = UserVolumeAccumulator PDA
if protocol_params.is_cashback_coin {
if let (Some(wsol_ata), Some(accumulator)) = (
get_user_volume_accumulator_wsol_ata(&params.payer.pubkey()),
get_user_volume_accumulator_pda(&params.payer.pubkey()),
) {
accounts.push(AccountMeta::new(wsol_ata, false));
accounts.push(AccountMeta::new(accumulator, false));
}
}
// Create instruction data // Create instruction data
let mut data = [0u8; 24]; let mut data = [0u8; 24];
@@ -420,3 +436,38 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
Ok(instructions) Ok(instructions)
} }
} }
/// Claim cashback for PumpSwap (AMM). Transfers WSOL from UserVolumeAccumulator's WSOL ATA to user's WSOL ATA.
/// Caller should ensure user's WSOL ATA exists (e.g. create idempotent ATA instruction) before this instruction.
pub fn claim_cashback_pumpswap_instruction(
payer: &Pubkey,
quote_mint: Pubkey,
quote_token_program: Pubkey,
) -> Option<solana_sdk::instruction::Instruction> {
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
let user_volume_accumulator_wsol_ata = get_user_volume_accumulator_wsol_ata(payer)?;
let user_wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
payer,
&quote_mint,
&quote_token_program,
);
// IDL order: user, user_volume_accumulator, quote_mint, quote_token_program,
// user_volume_accumulator_wsol_token_account, user_wsol_token_account, system_program, event_authority, program
let accounts = vec![
AccountMeta::new(*payer, true), // user (signer, writable)
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
AccountMeta::new_readonly(quote_mint, false),
AccountMeta::new_readonly(quote_token_program, false),
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
AccountMeta::new(user_wsol_ata, false), // writable
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::AMM_PROGRAM_META,
];
Some(solana_sdk::instruction::Instruction::new_with_bytes(
accounts::AMM_PROGRAM,
&CLAIM_CASHBACK_DISCRIMINATOR,
accounts,
))
}
+12
View File
@@ -185,6 +185,16 @@ pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
) )
} }
/// WSOL ATA of UserVolumeAccumulator for Pump AMM (used for cashback remaining accounts).
pub fn get_user_volume_accumulator_wsol_ata(user: &Pubkey) -> Option<Pubkey> {
let accumulator = get_user_volume_accumulator_pda(user)?;
Some(crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&accumulator,
&crate::constants::WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
))
}
pub fn get_global_volume_accumulator_pda() -> Option<Pubkey> { pub fn get_global_volume_accumulator_pda() -> Option<Pubkey> {
let seeds: &[&[u8]; 1] = &[&seeds::GLOBAL_VOLUME_ACCUMULATOR_SEED]; let seeds: &[&[u8]; 1] = &[&seeds::GLOBAL_VOLUME_ACCUMULATOR_SEED];
let program_id: &Pubkey = &&accounts::AMM_PROGRAM; let program_id: &Pubkey = &&accounts::AMM_PROGRAM;
@@ -227,6 +237,7 @@ pub async fn find_by_base_mint(
sort_results: None, sort_results: None,
}; };
let program_id = accounts::AMM_PROGRAM; let program_id = accounts::AMM_PROGRAM;
#[allow(deprecated)]
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?; let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
if accounts.is_empty() { if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", base_mint)); return Err(anyhow!("No pool found for mint {}", base_mint));
@@ -277,6 +288,7 @@ pub async fn find_by_quote_mint(
sort_results: None, sort_results: None,
}; };
let program_id = accounts::AMM_PROGRAM; let program_id = accounts::AMM_PROGRAM;
#[allow(deprecated)]
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?; let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
if accounts.is_empty() { if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", quote_mint)); return Err(anyhow!("No pool found for mint {}", quote_mint));
+3 -1
View File
@@ -15,9 +15,11 @@ pub struct Pool {
pub lp_supply: u64, pub lp_supply: u64,
pub coin_creator: Pubkey, pub coin_creator: Pubkey,
pub is_mayhem_mode: bool, pub is_mayhem_mode: bool,
/// Whether this pool's coin has cashback enabled
pub is_cashback_coin: bool,
} }
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1; pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
pub fn pool_decode(data: &[u8]) -> Option<Pool> { pub fn pool_decode(data: &[u8]) -> Option<Pool> {
if data.len() < POOL_SIZE { if data.len() < POOL_SIZE {
+128 -19
View File
@@ -8,6 +8,7 @@ pub mod utils;
use crate::common::nonce_cache::DurableNonceInfo; use crate::common::nonce_cache::DurableNonceInfo;
use crate::common::GasFeeStrategy; use crate::common::GasFeeStrategy;
use crate::common::{TradeConfig, InfrastructureConfig}; use crate::common::{TradeConfig, InfrastructureConfig};
#[cfg(feature = "perf-trace")]
use crate::constants::trade::trade::DEFAULT_SLIPPAGE; use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
use crate::constants::SOL_TOKEN_ACCOUNT; use crate::constants::SOL_TOKEN_ACCOUNT;
use crate::constants::USD1_TOKEN_ACCOUNT; use crate::constants::USD1_TOKEN_ACCOUNT;
@@ -286,7 +287,13 @@ impl TradingClient {
crate::common::fast_fn::fast_init(&payer.pubkey()); crate::common::fast_fn::fast_init(&payer.pubkey());
if create_wsol_ata { if create_wsol_ata {
Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await; // 在后台异步创建 WSOL ATA,不阻塞启动
let payer_clone = payer.clone();
let rpc_clone = infrastructure.rpc.clone();
tokio::spawn(async move {
Self::ensure_wsol_ata(&payer_clone, &rpc_clone).await;
});
println!("ℹ️ WSOL ATA 创建已在后台启动,不阻塞机器人启动");
} }
Self { Self {
@@ -309,6 +316,7 @@ impl TradingClient {
match rpc.get_account(&wsol_ata).await { match rpc.get_account(&wsol_ata).await {
Ok(_) => { Ok(_) => {
println!("✅ WSOL ATA已存在: {}", wsol_ata); println!("✅ WSOL ATA已存在: {}", wsol_ata);
return;
} }
Err(_) => { Err(_) => {
println!("🔨 创建WSOL ATA: {}", wsol_ata); println!("🔨 创建WSOL ATA: {}", wsol_ata);
@@ -317,35 +325,87 @@ impl TradingClient {
if !create_ata_ixs.is_empty() { if !create_ata_ixs.is_empty() {
use solana_sdk::transaction::Transaction; use solana_sdk::transaction::Transaction;
let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
let tx = Transaction::new_signed_with_payer(
&create_ata_ixs,
Some(&payer.pubkey()),
&[payer.as_ref()],
recent_blockhash,
);
match rpc.send_and_confirm_transaction(&tx).await { // 重试逻辑:最多尝试3次,每次超时10秒
Ok(signature) => { const MAX_RETRIES: usize = 3;
println!("✅ WSOL ATA创建成功: {}", signature); const TIMEOUT_SECS: u64 = 10;
let mut last_error = None;
for attempt in 1..=MAX_RETRIES {
if attempt > 1 {
println!("🔄 重试创建WSOL ATA (第{}/{}次)...", attempt, MAX_RETRIES);
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
} }
Err(e) => {
match rpc.get_account(&wsol_ata).await { let recent_blockhash = match rpc.get_latest_blockhash().await {
Ok(_) => { Ok(hash) => hash,
Err(e) => {
eprintln!("⚠️ 获取最新blockhash失败: {}", e);
last_error = Some(format!("获取blockhash失败: {}", e));
continue;
}
};
let tx = Transaction::new_signed_with_payer(
&create_ata_ixs,
Some(&payer.pubkey()),
&[payer.as_ref()],
recent_blockhash,
);
// 使用超时包装 send_and_confirm_transaction
let send_result = tokio::time::timeout(
tokio::time::Duration::from_secs(TIMEOUT_SECS),
rpc.send_and_confirm_transaction(&tx)
).await;
match send_result {
Ok(Ok(signature)) => {
println!("✅ WSOL ATA创建成功: {}", signature);
return;
}
Ok(Err(e)) => {
last_error = Some(format!("{}", e));
// 检查账户是否实际已存在
if let Ok(_) = rpc.get_account(&wsol_ata).await {
println!( println!(
"✅ WSOL ATA已存在(交易失败但账户存在): {}", "✅ WSOL ATA已存在(交易失败但账户存在): {}",
wsol_ata wsol_ata
); );
return;
} }
Err(_) => {
panic!( if attempt < MAX_RETRIES {
"❌ WSOL ATA创建失败且账户不存在: {}. 错误: {}", eprintln!("⚠️ 第{}次尝试失败: {}", attempt, e);
wsol_ata, e
);
} }
} }
Err(_) => {
last_error = Some(format!("交易确认超时({}秒)", TIMEOUT_SECS));
eprintln!("⚠️ 第{}次尝试超时", attempt);
}
} }
} }
// 所有重试都失败了
if let Some(err) = last_error {
eprintln!("❌ WSOL ATA创建失败(已重试{}次): {}", MAX_RETRIES, wsol_ata);
eprintln!(" 错误详情: {}", err);
eprintln!(" 💡 可能原因:");
eprintln!(" 1. 钱包SOL余额不足(需要约0.002 SOL用于租金豁免)");
eprintln!(" 2. RPC节点响应超时或网络拥堵");
eprintln!(" 3. 交易费用不足");
eprintln!(" 🔧 解决方案:");
eprintln!(" 1. 给钱包充值至少0.1 SOL");
eprintln!(" 2. 等待几秒后重试");
eprintln!(" 3. 检查RPC节点连接");
eprintln!(" ⚠️ 程序将在5秒后退出,请解决上述问题后重启");
std::thread::sleep(std::time::Duration::from_secs(5));
panic!(
"❌ WSOL ATA创建失败且账户不存在: {}. 错误: {}",
wsol_ata, err
);
}
} else { } else {
println!("ℹ️ WSOL ATA已存在(无需创建)"); println!("ℹ️ WSOL ATA已存在(无需创建)");
} }
@@ -842,4 +902,53 @@ impl TradingClient {
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string()) Ok(signature.to_string())
} }
/// Claim Bonding Curve (Pump) cashback.
///
/// Transfers native SOL from the user's UserVolumeAccumulator to the wallet.
/// If there is nothing to claim, the transaction may still succeed with no SOL transferred.
///
/// # Returns
/// * `Ok(String)` - Transaction signature
/// * `Err(anyhow::Error)` - Build or send failure (e.g. invalid PDA)
pub async fn claim_cashback_pumpfun(&self) -> Result<String, anyhow::Error> {
use solana_sdk::transaction::Transaction;
let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction(&self.payer.pubkey())
.ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?;
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&[ix], Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
}
/// Claim PumpSwap (AMM) cashback.
///
/// Transfers WSOL from the UserVolumeAccumulator to the user's WSOL ATA.
/// Creates the user's WSOL ATA idempotently if it does not exist, then claims.
///
/// # Returns
/// * `Ok(String)` - Transaction signature
/// * `Err(anyhow::Error)` - Build or send failure
pub async fn claim_cashback_pumpswap(&self) -> Result<String, anyhow::Error> {
use solana_sdk::transaction::Transaction;
let mut instructions = crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
&self.payer.pubkey(),
&self.payer.pubkey(),
&WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
self.use_seed_optimize,
);
let ix = crate::instruction::pumpswap::claim_cashback_pumpswap_instruction(
&self.payer.pubkey(),
WSOL_TOKEN_ACCOUNT,
crate::constants::TOKEN_PROGRAM,
).ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?;
instructions.push(ix);
let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
transaction.sign(&[&*self.payer], recent_blockhash);
let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
}
} }
+14 -17
View File
@@ -52,8 +52,8 @@ impl AstralaneClient {
let rpc_client = SolanaRpcClient::new(rpc_url); let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder() let http_client = Client::builder()
// Optimized connection pool settings for high performance // Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120)) .pool_idle_timeout(Duration::from_secs(300))
.pool_max_idle_per_host(256) // Increased from 64 to 256 .pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60 .tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10)) .http2_keep_alive_interval(Duration::from_secs(10))
@@ -90,17 +90,16 @@ impl AstralaneClient {
let stop_ping = self.stop_ping.clone(); let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds // 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 { loop {
interval.tick().await; interval.tick().await;
if stop_ping.load(Ordering::Relaxed) { if stop_ping.load(Ordering::Relaxed) {
break; break;
} }
// Send ping request
tokio::time::sleep(Duration::from_secs(5)).await;
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await { if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("Astralane ping request failed: {}", e); eprintln!("Astralane ping request failed: {}", e);
} }
@@ -130,25 +129,23 @@ impl AstralaneClient {
format!("{}/gethealth", endpoint) 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) let response = http_client.get(&ping_url)
.header("api_key", auth_token) .header("api_key", auth_token)
.timeout(Duration::from_millis(1500))
.send() .send()
.await?; .await?;
let status = response.status();
if response.status().is_success() { let _ = response.bytes().await;
// ping successful, connection remains active if !status.is_success() {
// println!("send getHealth to keep connection alive"); eprintln!("Astralane ping request returned non-success status: {}", status);
} else {
eprintln!("Astralane ping request returned non-success status: {}", response.status());
} }
Ok(()) Ok(())
} }
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
+16 -18
View File
@@ -52,8 +52,8 @@ impl BlockRazorClient {
let rpc_client = SolanaRpcClient::new(rpc_url); let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder() let http_client = Client::builder()
// Optimized connection pool settings for high performance // Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120)) .pool_idle_timeout(Duration::from_secs(300)) // 5min so ping-kept connection is not evicted early
.pool_max_idle_per_host(256) // Increased from 64 to 256 .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_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10)) .http2_keep_alive_interval(Duration::from_secs(10))
@@ -90,16 +90,16 @@ impl BlockRazorClient {
let stop_ping = self.stop_ping.clone(); let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds // Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("BlockRazor ping request failed: {}", e);
}
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
loop { loop {
interval.tick().await; interval.tick().await;
if stop_ping.load(Ordering::Relaxed) { if stop_ping.load(Ordering::Relaxed) {
break; break;
} }
// Send ping request
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await { if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("BlockRazor ping request failed: {}", e); eprintln!("BlockRazor ping request failed: {}", e);
} }
@@ -137,33 +137,31 @@ impl BlockRazorClient {
headers.insert("apikey", HeaderValue::from_str(auth_token)?); headers.insert("apikey", HeaderValue::from_str(auth_token)?);
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); 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) let response = http_client.get(&ping_url)
.headers(headers) .headers(headers)
.timeout(Duration::from_millis(1500))
.send() .send()
.await?; .await?;
let status = response.status();
if response.status().is_success() { let _ = response.bytes().await;
// ping successful, connection remains active if !status.is_success() {
// Can optionally log, but to reduce noise, not printing here eprintln!("BlockRazor ping request failed with status: {}", status);
} else {
eprintln!("BlockRazor ping request failed with status: {}", response.status());
} }
Ok(()) Ok(())
} }
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// BlockRazor使用fast模式的请求格式 // BlockRazor fast-mode request format
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
"transaction": content, "transaction": content,
"mode": "fast" "mode": "fast"
}))?; }))?;
// BlockRazor使用apikey header // BlockRazor uses apikey header
let response_text = self.http_client.post(&self.endpoint) let response_text = self.http_client.post(&self.endpoint)
.body(request_body) .body(request_body)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
+25 -27
View File
@@ -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 rand::seq::IndexedRandom;
use reqwest::Client; use reqwest::Client;
use std::{sync::Arc, time::Instant}; 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<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let body = serde_json::json!({ // Single format! for body to avoid json! + to_string() double allocation
"transaction": { let body = format!(
"content": content, r#"{{"transaction":{{"content":"{}"}},"frontRunningProtection":false,"useStakedRPCs":true}}"#,
}, content
"frontRunningProtection": false, );
"useStakedRPCs": true,
});
let endpoint = format!("{}/api/v2/submit", self.endpoint); let endpoint = format!("{}/api/v2/submit", self.endpoint);
let response_text = self.http_client.post(&endpoint) let response_text = self.http_client.post(&endpoint)
.body(body.to_string()) .body(body)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Authorization", self.auth_token.clone()) .header("Authorization", self.auth_token.as_str())
.send() .send()
.await? .await?
.text() .text()
.await?; .await?;
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?` // Parse with from_str to avoid extra wait from .json().await
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) { if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() { if response_json.get("result").is_some() {
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed()); println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
@@ -111,27 +111,25 @@ impl BloxrouteClient {
Ok(()) Ok(())
} }
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> { pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let body = serde_json::json!({ let contents = serialization::serialize_transactions_batch_sync(
"entries": transactions transactions.as_slice(),
.iter() UiTransactionEncoding::Base64,
.map(|tx| { )?;
serde_json::json!({ let entries: String = contents
"transaction": { .iter()
"content": tx.to_base64_string(), .map(|c| format!(r#"{{"transaction":{{"content":"{}"}}}}"#, c))
}, .collect::<Vec<_>>()
}) .join(",");
}) let body = format!(r#"{{"entries":[{}]}}"#, entries);
.collect::<Vec<_>>(),
});
let endpoint = format!("{}/api/v2/submit-batch", self.endpoint); let endpoint = format!("{}/api/v2/submit-batch", self.endpoint);
let response_text = self.http_client.post(&endpoint) let response_text = self.http_client.post(&endpoint)
.body(body.to_string()) .body(body)
.header("Content-Type", "application/json") .header("Content-Type", "application/json")
.header("Authorization", self.auth_token.clone()) .header("Authorization", self.auth_token.as_str())
.send() .send()
.await? .await?
.text() .text()
+21 -36
View File
@@ -3,6 +3,7 @@ use anyhow::Result;
use base64::engine::general_purpose::{self, STANDARD}; use base64::engine::general_purpose::{self, STANDARD};
use base64::Engine; use base64::Engine;
use bincode::serialize; use bincode::serialize;
use crate::swqos::serialization;
use reqwest::Client; use reqwest::Client;
use serde_json; use serde_json;
use serde_json::json; use serde_json::json;
@@ -40,7 +41,7 @@ impl From<anyhow::Error> for TradeError {
} }
} }
// 使用高性能序列化 // High-performance serialization
pub trait FormatBase64VersionedTransaction { pub trait FormatBase64VersionedTransaction {
fn to_base64_string(&self) -> String; fn to_base64_string(&self) -> String;
@@ -58,12 +59,12 @@ pub async fn poll_transaction_confirmation(
txt_sig: Signature, txt_sig: Signature,
wait_confirmation: bool, wait_confirmation: bool,
) -> Result<Signature> { ) -> Result<Signature> {
// 如果不需要等待确认,立即返回签名 // If no confirmation needed, return signature immediately
if !wait_confirmation { if !wait_confirmation {
return Ok(txt_sig); 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 interval: Duration = Duration::from_millis(1000);
let start: Instant = Instant::now(); let start: Instant = Instant::now();
let mut poll_count = 0u32; let mut poll_count = 0u32;
@@ -76,33 +77,23 @@ pub async fn poll_transaction_confirmation(
poll_count += 1; poll_count += 1;
let status = rpc.get_signature_statuses(&[txt_sig]).await?; let status = rpc.get_signature_statuses(&[txt_sig]).await?;
match status.value[0].clone() { let first = status.value.get(0).and_then(|o| o.as_ref());
Some(status) => { match first {
if status.err.is_none() Some(s) => {
&& (status.confirmation_status if s.err.is_none()
== Some(TransactionConfirmationStatus::Confirmed) && (s.confirmation_status == Some(TransactionConfirmationStatus::Confirmed)
|| status.confirmation_status || s.confirmation_status == Some(TransactionConfirmationStatus::Finalized))
== Some(TransactionConfirmationStatus::Finalized))
{ {
return Ok(txt_sig); return Ok(txt_sig);
} }
// 如果 getSignatureStatuses 返回了错误,立即获取详细信息
if status.err.is_some() {
// 直接跳转到获取交易详情
}
} }
None => { None => {
// 交易还未上链,继续等待,不调用 getTransaction
sleep(interval).await; sleep(interval).await;
continue; continue;
} }
} }
// 优化:只在以下情况调用 getTransaction let should_get_transaction = first.map(|s| s.err.is_some()).unwrap_or(false) || poll_count >= 10;
// 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;
if !should_get_transaction { if !should_get_transaction {
sleep(interval).await; sleep(interval).await;
@@ -122,7 +113,7 @@ pub async fn poll_transaction_confirmation(
{ {
Ok(details) => details, Ok(details) => details,
Err(_) => { Err(_) => {
// 交易可能还未上链,继续等待 // Tx may not be on chain yet, keep waiting
sleep(interval).await; sleep(interval).await;
continue; continue;
} }
@@ -136,7 +127,7 @@ pub async fn poll_transaction_confirmation(
if meta.err.is_none() { if meta.err.is_none() {
return Ok(txt_sig); return Ok(txt_sig);
} else { } else {
// log_messages 中提取错误信息 // Extract error message from log_messages
let mut error_msg = String::new(); let mut error_msg = String::new();
if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) =
&meta.log_messages &meta.log_messages
@@ -162,12 +153,12 @@ pub async fn poll_transaction_confirmation(
let tx_err: TransactionError = let tx_err: TransactionError =
serde_json::from_value(serde_json::to_value(&ui_err)?)?; serde_json::from_value(serde_json::to_value(&ui_err)?)?;
// 直接使用Solana原生的InstructionError中的错误码 // Use Solana InstructionError codes directly
let mut code = 0u32; let mut code = 0u32;
let mut index = None; let mut index = None;
match &tx_err { match &tx_err {
TransactionError::InstructionError(i, i_error) => { TransactionError::InstructionError(i, i_error) => {
// 直接匹配所有InstructionError类型,Custom也是其中之一 // Match all InstructionError variants including Custom
code = match i_error { code = match i_error {
solana_sdk::instruction::InstructionError::Custom(c) => *c, solana_sdk::instruction::InstructionError::Custom(c) => *c,
solana_sdk::instruction::InstructionError::GenericError => 1, solana_sdk::instruction::InstructionError::GenericError => 1,
@@ -180,7 +171,7 @@ pub async fn poll_transaction_confirmation(
solana_sdk::instruction::InstructionError::MissingRequiredSignature => 8, solana_sdk::instruction::InstructionError::MissingRequiredSignature => 8,
solana_sdk::instruction::InstructionError::AccountAlreadyInitialized => 9, solana_sdk::instruction::InstructionError::AccountAlreadyInitialized => 9,
solana_sdk::instruction::InstructionError::UninitializedAccount => 10, solana_sdk::instruction::InstructionError::UninitializedAccount => 10,
_ => 999, // 其他未知错误 _ => 999, // Other unknown errors
}; };
index = Some(*i); index = Some(*i);
} }
@@ -198,11 +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> { pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
// 序列化交易 // Serialize transaction
let serialized = bincode::serialize(transaction) let serialized = bincode::serialize(transaction)
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?; .map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
// Base64编码 // Base64 encode
let encoded = STANDARD.encode(serialized); let encoded = STANDARD.encode(serialized);
let request_data = json!({ let request_data = json!({
@@ -250,18 +241,12 @@ pub async fn serialize_and_encode(
Ok(serialized) Ok(serialized)
} }
pub async fn serialize_transaction_and_encode( /// Sync serialize and encode; uses buffer pool when possible for lower allocs and latency.
pub fn serialize_transaction_and_encode(
transaction: &impl SerializableTransaction, transaction: &impl SerializableTransaction,
encoding: UiTransactionEncoding, encoding: UiTransactionEncoding,
) -> Result<(String, Signature)> { ) -> Result<(String, Signature)> {
let signature = transaction.get_signature(); serialization::serialize_transaction_sync(transaction, encoding)
let serialized_tx = serialize(transaction)?;
let serialized = match encoding {
UiTransactionEncoding::Base58 => bs58::encode(serialized_tx).into_string(),
UiTransactionEncoding::Base64 => STANDARD.encode(serialized_tx),
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
};
Ok((serialized, *signature))
} }
pub async fn serialize_smart_transaction_and_encode( pub async fn serialize_smart_transaction_and_encode(
+1 -1
View File
@@ -64,7 +64,7 @@ impl FlashBlockClient {
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// FlashBlock API format // FlashBlock API format
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
+1 -1
View File
@@ -67,7 +67,7 @@ impl JitoClient {
pub async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
"id": 1, "id": 1,
+1 -1
View File
@@ -65,7 +65,7 @@ impl LightspeedClient {
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction // Lightspeed uses standard Solana JSON-RPC format for sendTransaction
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
+1 -1
View File
@@ -69,7 +69,7 @@ impl NextBlockClient {
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
"transaction": { "transaction": {
+14 -16
View File
@@ -52,8 +52,8 @@ impl Node1Client {
let rpc_client = SolanaRpcClient::new(rpc_url); let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder() let http_client = Client::builder()
// Optimized connection pool settings for high performance // Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120)) .pool_idle_timeout(Duration::from_secs(300))
.pool_max_idle_per_host(256) // Increased from 64 to 256 .pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60 .tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10)) .http2_keep_alive_interval(Duration::from_secs(10))
@@ -90,16 +90,16 @@ impl Node1Client {
let stop_ping = self.stop_ping.clone(); let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds // Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("Node1 ping request failed: {}", e);
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop { loop {
interval.tick().await; interval.tick().await;
if stop_ping.load(Ordering::Relaxed) { if stop_ping.load(Ordering::Relaxed) {
break; break;
} }
// Send ping request
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await { if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("Node1 ping request failed: {}", e); eprintln!("Node1 ping request failed: {}", e);
} }
@@ -125,24 +125,22 @@ impl Node1Client {
format!("{}/ping", endpoint) format!("{}/ping", endpoint)
}; };
// Send GET request to /ping endpoint (no api-key required) // Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url) let response = http_client.get(&ping_url)
.timeout(Duration::from_millis(1500))
.send() .send()
.await?; .await?;
let status = response.status();
if response.status().is_success() { let _ = response.bytes().await;
// ping successful, connection remains active if !status.is_success() {
// Can optionally log, but to reduce noise, not printing here eprintln!("Node1 ping request returned non-success status: {}", status);
} else {
eprintln!("Node1 ping request returned non-success status: {}", response.status());
} }
Ok(()) Ok(())
} }
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
+56 -21
View File
@@ -1,4 +1,4 @@
//! 交易序列化模块 //! Transaction serialization module.
use anyhow::Result; use anyhow::Result;
use base64::Engine; use base64::Engine;
@@ -14,7 +14,7 @@ use crate::perf::{
compiler_optimization::CompileTimeOptimizedEventProcessor, compiler_optimization::CompileTimeOptimizedEventProcessor,
}; };
/// 零分配序列化器 - 使用缓冲池避免运行时分配 /// Zero-allocation serializer using a buffer pool to avoid runtime allocation.
pub struct ZeroAllocSerializer { pub struct ZeroAllocSerializer {
buffer_pool: Arc<ArrayQueue<Vec<u8>>>, buffer_pool: Arc<ArrayQueue<Vec<u8>>>,
buffer_size: usize, buffer_size: usize,
@@ -24,7 +24,7 @@ impl ZeroAllocSerializer {
pub fn new(pool_size: usize, buffer_size: usize) -> Self { pub fn new(pool_size: usize, buffer_size: usize) -> Self {
let pool = ArrayQueue::new(pool_size); let pool = ArrayQueue::new(pool_size);
// 预分配缓冲区 // Pre-allocate buffers
for _ in 0..pool_size { for _ in 0..pool_size {
let mut buffer = Vec::with_capacity(buffer_size); let mut buffer = Vec::with_capacity(buffer_size);
buffer.resize(buffer_size, 0); buffer.resize(buffer_size, 0);
@@ -38,14 +38,14 @@ impl ZeroAllocSerializer {
} }
pub fn serialize_zero_alloc<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> { pub fn serialize_zero_alloc<T: serde::Serialize>(&self, data: &T, _label: &str) -> Result<Vec<u8>> {
// 尝试从池中获取缓冲区 // Try to get a buffer from the pool
let mut buffer = self.buffer_pool.pop().unwrap_or_else(|| { let mut buffer = self.buffer_pool.pop().unwrap_or_else(|| {
let mut buf = Vec::with_capacity(self.buffer_size); let mut buf = Vec::with_capacity(self.buffer_size);
buf.resize(self.buffer_size, 0); buf.resize(self.buffer_size, 0);
buf buf
}); });
// 序列化到缓冲区 // Serialize into buffer
let serialized = bincode::serialize(data)?; let serialized = bincode::serialize(data)?;
buffer.clear(); buffer.clear();
buffer.extend_from_slice(&serialized); buffer.extend_from_slice(&serialized);
@@ -54,11 +54,11 @@ impl ZeroAllocSerializer {
} }
pub fn return_buffer(&self, buffer: Vec<u8>) { pub fn return_buffer(&self, buffer: Vec<u8>) {
// 归还缓冲区到池中 // Return buffer to the pool
let _ = self.buffer_pool.push(buffer); let _ = self.buffer_pool.push(buffer);
} }
/// 获取池统计信息 /// Get pool statistics.
pub fn get_pool_stats(&self) -> (usize, usize) { pub fn get_pool_stats(&self) -> (usize, usize) {
let available = self.buffer_pool.len(); let available = self.buffer_pool.len();
let capacity = self.buffer_pool.capacity(); let capacity = self.buffer_pool.capacity();
@@ -66,32 +66,32 @@ impl ZeroAllocSerializer {
} }
} }
/// 全局序列化器实例 /// Global serializer instance.
static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| { static SERIALIZER: Lazy<Arc<ZeroAllocSerializer>> = Lazy::new(|| {
Arc::new(ZeroAllocSerializer::new( Arc::new(ZeroAllocSerializer::new(
10_000, // 池大小 10_000, // Pool size
256 * 1024, // 缓冲区大小: 256KB 256 * 1024, // Buffer size: 256KB
)) ))
}); });
/// 🚀 编译时优化的事件处理器 (零运行时开销) /// Compile-time optimized event processor (zero runtime cost).
static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor = static COMPILE_TIME_PROCESSOR: CompileTimeOptimizedEventProcessor =
CompileTimeOptimizedEventProcessor::new(); CompileTimeOptimizedEventProcessor::new();
/// Base64 编码器 /// Base64 encoder.
pub struct Base64Encoder; pub struct Base64Encoder;
impl Base64Encoder { impl Base64Encoder {
#[inline(always)] #[inline(always)]
pub fn encode(data: &[u8]) -> String { pub fn encode(data: &[u8]) -> String {
// 使用编译时优化的哈希进行快速路由 // Use compile-time optimized hash for fast routing
let _route = if !data.is_empty() { let _route = if !data.is_empty() {
COMPILE_TIME_PROCESSOR.route_event_zero_cost(data[0]) COMPILE_TIME_PROCESSOR.route_event_zero_cost(data[0])
} else { } else {
0 0
}; };
// 使用 SIMD 加速的 Base64 编码 // Use SIMD-accelerated Base64 encoding
SIMDSerializer::encode_base64_simd(data) SIMDSerializer::encode_base64_simd(data)
} }
@@ -105,32 +105,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( pub async fn serialize_transaction(
transaction: &impl SerializableTransaction, transaction: &impl SerializableTransaction,
encoding: UiTransactionEncoding, encoding: UiTransactionEncoding,
) -> Result<(String, Signature)> { ) -> Result<(String, Signature)> {
let signature = transaction.get_signature(); let signature = transaction.get_signature();
// 使用零分配序列化 // Use zero-allocation serialization
let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?; let serialized_tx = SERIALIZER.serialize_zero_alloc(transaction, "transaction")?;
let serialized = match encoding { let serialized = match encoding {
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(), UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
UiTransactionEncoding::Base64 => { UiTransactionEncoding::Base64 => {
// 使用 SIMD 优化的 Base64 编码 // Use SIMD-optimized Base64 encoding
STANDARD.encode(&serialized_tx) STANDARD.encode(&serialized_tx)
} }
_ => return Err(anyhow::anyhow!("Unsupported encoding")), _ => return Err(anyhow::anyhow!("Unsupported encoding")),
}; };
// 立即归还缓冲区到池中 // Return buffer to pool immediately
SERIALIZER.return_buffer(serialized_tx); SERIALIZER.return_buffer(serialized_tx);
Ok((serialized, *signature)) Ok((serialized, *signature))
} }
/// 批量交易序列化 /// Sync batch serialize + encode using buffer pool.
pub fn serialize_transactions_batch_sync(
transactions: &[impl SerializableTransaction],
encoding: UiTransactionEncoding,
) -> Result<Vec<String>> {
let mut results = Vec::with_capacity(transactions.len());
for tx in transactions {
let serialized_tx = SERIALIZER.serialize_zero_alloc(tx, "transaction")?;
let encoded = match encoding {
UiTransactionEncoding::Base58 => bs58::encode(&serialized_tx).into_string(),
UiTransactionEncoding::Base64 => 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( pub async fn serialize_transactions_batch(
transactions: &[impl SerializableTransaction], transactions: &[impl SerializableTransaction],
encoding: UiTransactionEncoding, encoding: UiTransactionEncoding,
@@ -153,7 +188,7 @@ pub async fn serialize_transactions_batch(
Ok(results) Ok(results)
} }
/// 获取序列化器统计信息 /// Get serializer statistics.
pub fn get_serializer_stats() -> (usize, usize) { pub fn get_serializer_stats() -> (usize, usize) {
SERIALIZER.get_pool_stats() SERIALIZER.get_pool_stats()
} }
@@ -168,7 +203,7 @@ mod tests {
let encoded = Base64Encoder::encode(data); let encoded = Base64Encoder::encode(data);
assert!(!encoded.is_empty()); assert!(!encoded.is_empty());
// 验证可以正确解码 // Verify it decodes correctly
let decoded = STANDARD.decode(&encoded).unwrap(); let decoded = STANDARD.decode(&encoded).unwrap();
assert_eq!(&decoded[..data.len()], data); assert_eq!(&decoded[..data.len()], data);
} }
+18 -11
View File
@@ -50,8 +50,8 @@ impl StelliumClient {
let rpc_client = SolanaRpcClient::new(rpc_url); let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder() let http_client = Client::builder()
// Optimized connection pool settings for high performance // Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120)) .pool_idle_timeout(Duration::from_secs(300))
.pool_max_idle_per_host(256) // Increased from 64 to 256 .pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60 .tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10)) .http2_keep_alive_interval(Duration::from_secs(10))
@@ -89,21 +89,28 @@ impl StelliumClient {
let stop_ping = self.keep_alive_running.clone(); let stop_ping = self.keep_alive_running.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds // Immediate first ping to warm connection and reduce first-submit cold start latency
let url = format!("{}/{}", endpoint, auth_token);
if let Ok(resp) = http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
let status = resp.status();
let _ = resp.bytes().await;
if !status.is_success() {
eprintln!(" [Stellium] Ping failed with status: {}", status);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop { loop {
interval.tick().await; interval.tick().await;
if stop_ping.load(Ordering::Relaxed) { if stop_ping.load(Ordering::Relaxed) {
break; break;
} }
// Send ping request
let url = format!("{}/{}", endpoint, auth_token); let url = format!("{}/{}", endpoint, auth_token);
match http_client.get(&url).send().await { match http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
Ok(response) => { Ok(response) => {
if !response.status().is_success() { let status = response.status();
eprintln!(" [Stellium] Ping failed with status: {}", response.status()); let _ = response.bytes().await;
if !status.is_success() {
eprintln!(" [Stellium] Ping failed with status: {}", status);
} }
} }
Err(e) => { Err(e) => {
@@ -116,7 +123,7 @@ impl StelliumClient {
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Stellium uses standard Solana sendTransaction format // Stellium uses standard Solana sendTransaction format
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
+14 -16
View File
@@ -78,8 +78,8 @@ impl TemporalClient {
let rpc_client = SolanaRpcClient::new(rpc_url); let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = Client::builder() let http_client = Client::builder()
// Optimized connection pool settings for high performance // Optimized connection pool settings for high performance
.pool_idle_timeout(Duration::from_secs(120)) .pool_idle_timeout(Duration::from_secs(300))
.pool_max_idle_per_host(256) // Increased from 64 to 256 .pool_max_idle_per_host(4)
.tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60 .tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60
.tcp_nodelay(true) // Disable Nagle's algorithm for lower latency .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency
.http2_keep_alive_interval(Duration::from_secs(10)) .http2_keep_alive_interval(Duration::from_secs(10))
@@ -116,16 +116,16 @@ impl TemporalClient {
let stop_ping = self.stop_ping.clone(); let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move { let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds // Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("Temporal ping request failed: {}", e);
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop { loop {
interval.tick().await; interval.tick().await;
if stop_ping.load(Ordering::Relaxed) { if stop_ping.load(Ordering::Relaxed) {
break; break;
} }
// Send ping request
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await { if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
eprintln!("Temporal ping request failed: {}", e); eprintln!("Temporal ping request failed: {}", e);
} }
@@ -151,24 +151,22 @@ impl TemporalClient {
format!("{}/ping", endpoint) format!("{}/ping", endpoint)
}; };
// Send GET request to /ping endpoint // Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url) let response = http_client.get(&ping_url)
.timeout(Duration::from_millis(1500))
.send() .send()
.await?; .await?;
let status = response.status();
if response.status().is_success() { let _ = response.bytes().await;
// ping successful, connection remains active if !status.is_success() {
// Can optionally log, but to reduce noise, not printing here eprintln!("Temporal ping request returned non-success status: {}", status);
} else {
eprintln!("Temporal ping request returned non-success status: {}", response.status());
} }
Ok(()) Ok(())
} }
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Build request body according to Nozomi documentation requirements // Build request body according to Nozomi documentation requirements
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
+1 -1
View File
@@ -64,7 +64,7 @@ impl ZeroSlotClient {
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({ let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0", "jsonrpc": "2.0",
+1 -2
View File
@@ -11,14 +11,13 @@ use super::{
}; };
use crate::{ use crate::{
common::{nonce_cache::DurableNonceInfo, SolanaRpcClient}, common::{nonce_cache::DurableNonceInfo, SolanaRpcClient},
constants::swqos::NODE1_TIP_ACCOUNTS,
trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}}, trading::{MiddlewareManager, core::transaction_pool::{acquire_builder, release_builder}},
}; };
/// Build standard RPC transaction /// Build standard RPC transaction
pub async fn build_transaction( pub async fn build_transaction(
payer: Arc<Keypair>, payer: Arc<Keypair>,
rpc: Option<Arc<SolanaRpcClient>>, _rpc: Option<Arc<SolanaRpcClient>>,
unit_limit: u32, unit_limit: u32,
unit_price: u64, unit_price: u64,
business_instructions: Vec<Instruction>, business_instructions: Vec<Instruction>,
+2
View File
@@ -38,6 +38,7 @@ struct TaskResult {
success: bool, success: bool,
signature: Signature, signature: Signature,
error: Option<anyhow::Error>, error: Option<anyhow::Error>,
#[allow(dead_code)]
swqos_type: SwqosType, // 🔧 增加:记录SWQOS类型 swqos_type: SwqosType, // 🔧 增加:记录SWQOS类型
landed_on_chain: bool, // 🔧 Whether tx landed on-chain (even if failed) landed_on_chain: bool, // 🔧 Whether tx landed on-chain (even if failed)
} }
@@ -349,6 +350,7 @@ pub async fn execute_parallel(
let _send_start = Instant::now(); let _send_start = Instant::now();
let mut err: Option<anyhow::Error> = None; let mut err: Option<anyhow::Error> = None;
#[allow(unused_assignments)]
let mut landed_on_chain = false; let mut landed_on_chain = false;
let success = match swqos_client let success = match swqos_client
.send_transaction( .send_transaction(
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::{
perf::syscall_bypass::SystemCallBypassManager, perf::syscall_bypass::SystemCallBypassManager,
trading::core::{ trading::core::{
async_executor::execute_parallel, async_executor::execute_parallel,
execution::{ExecutionPath, InstructionProcessor, Prefetch}, execution::{InstructionProcessor, Prefetch},
traits::TradeExecutor, traits::TradeExecutor,
}, },
trading::MiddlewareManager, trading::MiddlewareManager,
+13
View File
@@ -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( pub fn from_dev_trade(
mint: Pubkey, mint: Pubkey,
token_amount: u64, token_amount: u64,
@@ -118,6 +120,7 @@ impl PumpFunParams {
close_token_account_when_sell: Option<bool>, close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey, fee_recipient: Pubkey,
token_program: Pubkey, token_program: Pubkey,
is_cashback_coin: bool,
) -> Self { ) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT; let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
let bonding_curve_account = BondingCurveAccount::from_dev_trade( let bonding_curve_account = BondingCurveAccount::from_dev_trade(
@@ -127,6 +130,7 @@ impl PumpFunParams {
max_sol_cost, max_sol_cost,
creator, creator,
is_mayhem_mode, is_mayhem_mode,
is_cashback_coin,
); );
Self { Self {
bonding_curve: Arc::new(bonding_curve_account), bonding_curve: Arc::new(bonding_curve_account),
@@ -137,6 +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( pub fn from_trade(
bonding_curve: Pubkey, bonding_curve: Pubkey,
associated_bonding_curve: Pubkey, associated_bonding_curve: Pubkey,
@@ -150,6 +156,7 @@ impl PumpFunParams {
close_token_account_when_sell: Option<bool>, close_token_account_when_sell: Option<bool>,
fee_recipient: Pubkey, fee_recipient: Pubkey,
token_program: Pubkey, token_program: Pubkey,
is_cashback_coin: bool,
) -> Self { ) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT; let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
let bonding_curve = BondingCurveAccount::from_trade( let bonding_curve = BondingCurveAccount::from_trade(
@@ -161,6 +168,7 @@ impl PumpFunParams {
real_token_reserves, real_token_reserves,
real_sol_reserves, real_sol_reserves,
is_mayhem_mode, is_mayhem_mode,
is_cashback_coin,
); );
Self { Self {
bonding_curve: Arc::new(bonding_curve), bonding_curve: Arc::new(bonding_curve),
@@ -189,6 +197,7 @@ impl PumpFunParams {
complete: account.0.complete, complete: account.0.complete,
creator: account.0.creator, creator: account.0.creator,
is_mayhem_mode: account.0.is_mayhem_mode, is_mayhem_mode: account.0.is_mayhem_mode,
is_cashback_coin: account.0.is_cashback_coin,
}; };
let associated_bonding_curve = get_associated_token_address_with_program_id( let associated_bonding_curve = get_associated_token_address_with_program_id(
&bonding_curve.account, &bonding_curve.account,
@@ -243,6 +252,8 @@ pub struct PumpSwapParams {
pub quote_token_program: Pubkey, pub quote_token_program: Pubkey,
/// Whether the pool is in mayhem mode /// Whether the pool is in mayhem mode
pub is_mayhem_mode: bool, pub is_mayhem_mode: bool,
/// Whether the pool's coin has cashback enabled
pub is_cashback_coin: bool,
} }
impl PumpSwapParams { impl PumpSwapParams {
@@ -274,6 +285,7 @@ impl PumpSwapParams {
base_token_program, base_token_program,
quote_token_program, quote_token_program,
is_mayhem_mode, is_mayhem_mode,
is_cashback_coin: false,
} }
} }
@@ -335,6 +347,7 @@ impl PumpSwapParams {
} else { } else {
crate::constants::TOKEN_PROGRAM_2022 crate::constants::TOKEN_PROGRAM_2022
}, },
is_cashback_coin: pool_data.is_cashback_coin,
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata { quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
crate::constants::TOKEN_PROGRAM crate::constants::TOKEN_PROGRAM
} else { } else {