Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f6cc6fb08 | ||
|
|
5e4977f6a6 | ||
|
|
c27e479659 | ||
|
|
9a8e3ffb2d | ||
|
|
43d1369d4e | ||
|
|
a048f3b6ad | ||
|
|
feaac5ddd2 | ||
|
|
710a8d482f |
+1
-1
@@ -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>",
|
||||||
|
|||||||
@@ -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 |
|
| Seed trading example | `cargo run --package seed_trading` | [examples/seed_trading](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/seed_trading/src/main.rs) |
|
||||||
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
| Gas fee strategy example | `cargo run --package gas_fee_strategy` | [examples/gas_fee_strategy](https://github.com/0xfnzero/sol-trade-sdk/tree/main/examples/gas_fee_strategy/src/main.rs) |
|
||||||
|
|
||||||
### ⚙️ SWQOS Service Configuration
|
### ⚙️ SWQoS Service Configuration
|
||||||
|
|
||||||
When configuring SWQOS services, note the different parameter requirements for each service:
|
When configuring SWQoS services, note the different parameter requirements for each service:
|
||||||
|
|
||||||
- **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`)
|
- **Jito**: The first parameter is UUID (if no UUID, pass an empty string `""`)
|
||||||
- **Other MEV services**: The first parameter is the API Token
|
- **Other MEV services**: The first parameter is the API Token
|
||||||
|
|
||||||
#### Custom URL Support
|
#### Custom URL Support
|
||||||
|
|
||||||
Each SWQOS service now supports an optional custom URL parameter:
|
Each SWQoS service now supports an optional custom URL parameter:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// Using custom URL (third parameter)
|
// Using custom URL (third parameter)
|
||||||
@@ -273,6 +278,17 @@ Address Lookup Tables (ALT) allow you to optimize transaction size and reduce fe
|
|||||||
|
|
||||||
Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md).
|
Use Durable Nonce to implement transaction replay protection and optimize transaction processing. For detailed information, see the [Durable Nonce Guide](docs/NONCE_CACHE.md).
|
||||||
|
|
||||||
|
## 💰 Cashback Support (PumpFun / PumpSwap)
|
||||||
|
|
||||||
|
PumpFun and PumpSwap support **cashback** for eligible tokens: part of the trading fee can be returned to the user. The SDK **must know** whether the token has cashback enabled so that buy/sell instructions include the correct accounts (e.g. `UserVolumeAccumulator` as remaining account for cashback coins).
|
||||||
|
|
||||||
|
- **When params come from RPC**: If you use `PumpFunParams::from_mint_by_rpc` or `PumpSwapParams::from_pool_address_by_rpc` / `from_mint_by_rpc`, the SDK reads `is_cashback_coin` from chain—no extra step.
|
||||||
|
- **When params come from event/parser**: If you build params from trade events (e.g. [sol-parser-sdk](https://github.com/0xfnzero/sol-parser-sdk)), you **must** pass the cashback flag into the SDK:
|
||||||
|
- **PumpFun**: `PumpFunParams::from_trade(..., is_cashback_coin)` and `PumpFunParams::from_dev_trade(..., is_cashback_coin)` take an `is_cashback_coin` parameter. Set it from the parsed event (e.g. CreateEvent’s `is_cashback_enabled` or BondingCurve’s `is_cashback_coin`).
|
||||||
|
- **PumpSwap**: `PumpSwapParams` has a field `is_cashback_coin`. When constructing params manually (e.g. from pool/trade events), set it from the parsed pool or event data.
|
||||||
|
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
|
||||||
|
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
|
||||||
|
|
||||||
## 🛡️ MEV Protection Services
|
## 🛡️ MEV Protection Services
|
||||||
|
|
||||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||||
|
|||||||
+40
-20
@@ -47,10 +47,11 @@
|
|||||||
- [📋 使用示例](#-使用示例)
|
- [📋 使用示例](#-使用示例)
|
||||||
- [⚡ 交易参数](#-交易参数)
|
- [⚡ 交易参数](#-交易参数)
|
||||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||||
- [⚙️ SWQOS 服务配置说明](#️-swqos-服务配置说明)
|
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||||
- [🔍 地址查找表](#-地址查找表)
|
- [🔍 地址查找表](#-地址查找表)
|
||||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||||
|
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||||
- [📁 项目结构](#-项目结构)
|
- [📁 项目结构](#-项目结构)
|
||||||
- [📄 许可证](#-许可证)
|
- [📄 许可证](#-许可证)
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||||
|
11. **共享基础设施**: 多钱包可共享同一套 RPC 与 SWQoS 客户端,降低资源占用
|
||||||
|
|
||||||
## 📦 安装
|
## 📦 安装
|
||||||
|
|
||||||
@@ -87,14 +89,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
# 添加到您的 Cargo.toml
|
# 添加到您的 Cargo.toml
|
||||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.4.0" }
|
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.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)
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"url": "https://context7.com/0xfnzero/sol-trade-sdk",
|
||||||
|
"public_key": "pk_ShleAZazFTUV8ORpmH4jy"
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Pump Cashback 集成说明
|
||||||
|
|
||||||
|
本 SDK 已支持 [Pump Cashback Rewards](https://github.com/pump-fun/pump-public-docs/blob/main/docs/PUMP_CASHBACK_README.md):在启用 cashback 的币种上交易时,用户可获得手续费返还而非支付给创作者。
|
||||||
|
|
||||||
|
## 行为概览
|
||||||
|
|
||||||
|
- **Bonding Curve (Pump)**
|
||||||
|
- **Buy**:无需改指令,若币种启用 cashback 会自动累计。
|
||||||
|
- **Sell**:当 `bonding_curve.is_cashback_coin == true` 时,SDK 会在指令中追加 `UserVolumeAccumulator` PDA(remaining account),用于累计可领取的 cashback。
|
||||||
|
- **Pump Swap**
|
||||||
|
- **Buy**:当 `PumpSwapParams.is_cashback_coin == true` 时,会追加 UserVolumeAccumulator 的 **WSOL ATA** 作为 remaining account。
|
||||||
|
- **Sell**:当 `is_cashback_coin == true` 时,会追加 **WSOL ATA**(0th)和 **UserVolumeAccumulator PDA**(1st)作为 remaining accounts。
|
||||||
|
|
||||||
|
`PumpFunParams` 通过 `from_mint_by_rpc` 拉取 bonding curve 时会解析链上 `is_cashback_coin`;`PumpSwapParams` 通过 `from_pool_address_by_rpc` / `from_mint_by_rpc` 拉取 pool 时会解析 `is_cashback_coin`。
|
||||||
|
|
||||||
|
## 领取 Cashback
|
||||||
|
|
||||||
|
### 推荐:使用 TradingClient 一键领取
|
||||||
|
|
||||||
|
若已有 `TradingClient`(例如用于买卖的同一个客户端),可直接调用以下方法,内部会完成构建交易、签名与发送:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// 领取 Pump 曲线产生的 Cashback(到账为 native SOL)
|
||||||
|
let sig = client.claim_cashback_pumpfun().await?;
|
||||||
|
|
||||||
|
// 领取 PumpSwap 产生的 Cashback(到账为 WSOL,自动确保用户 WSOL ATA 存在)
|
||||||
|
let sig = client.claim_cashback_pumpswap().await?;
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`claim_cashback_pumpfun()`**:领取 Bonding Curve (Pump) 的返还,到账为钱包 SOL。
|
||||||
|
- **`claim_cashback_pumpswap()`**:领取 PumpSwap (AMM) 的返还,到账为用户的 WSOL ATA;若用户尚无 WSOL ATA 会先自动创建再领取。
|
||||||
|
|
||||||
|
### 仅构建指令(自行组交易时使用)
|
||||||
|
|
||||||
|
#### Bonding Curve (Pump)
|
||||||
|
|
||||||
|
将 native lamports 从 UserVolumeAccumulator 转到用户钱包:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sol_trade_sdk::instruction::pumpfun;
|
||||||
|
|
||||||
|
let ix = pumpfun::claim_cashback_pumpfun_instruction(&payer.pubkey());
|
||||||
|
// 将 ix 放入交易并发送
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pump Swap (AMM)
|
||||||
|
|
||||||
|
将 WSOL 从 UserVolumeAccumulator 的 WSOL ATA 转到用户的 WSOL ATA。**调用前需确保用户 WSOL ATA 已存在**(或使用上面的 `claim_cashback_pumpswap()` 会自动处理):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use sol_trade_sdk::instruction::pumpswap;
|
||||||
|
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||||
|
use sol_trade_sdk::constants::TOKEN_PROGRAM;
|
||||||
|
|
||||||
|
let ix = pumpswap::claim_cashback_pumpswap_instruction(
|
||||||
|
&payer.pubkey(),
|
||||||
|
WSOL_TOKEN_ACCOUNT,
|
||||||
|
TOKEN_PROGRAM,
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 读取未领取金额
|
||||||
|
|
||||||
|
- **Pump (Bonding Curve)**:读 Pump 程序的 `UserVolumeAccumulator` PDA 的 lamports,减去维持账户所需的 rent-exempt 金额,即为未领取 cashback(lamports)。
|
||||||
|
- **Pump Swap**:读 Pump AMM 程序的 UserVolumeAccumulator 的 **WSOL ATA** 的 token balance,即为未领取 cashback(WSOL 数量)。
|
||||||
|
|
||||||
|
PDA 推导(本 SDK 已实现):
|
||||||
|
|
||||||
|
- Pump:`instruction::utils::pumpfun::get_user_volume_accumulator_pda(user)`
|
||||||
|
- Pump AMM:`instruction::utils::pumpswap::get_user_volume_accumulator_pda(user)`,WSOL ATA:`instruction::utils::pumpswap::get_user_volume_accumulator_wsol_ata(user)`
|
||||||
|
|
||||||
|
## IDL 来源
|
||||||
|
|
||||||
|
根目录 `idl/` 下的 `pump.json`、`pump_amm.json`、`pump_fees.json` 从 [pump-fun/pump-public-docs](https://github.com/pump-fun/pump-public-docs) 的 `idl` 目录同步,便于与官方 IDL 对照和后续升级。
|
||||||
@@ -143,13 +143,14 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
trade_info.mint,
|
trade_info.mint,
|
||||||
trade_info.creator,
|
trade_info.creator,
|
||||||
trade_info.creator_vault,
|
trade_info.creator_vault,
|
||||||
trade_info.virtual_sol_reserves,
|
trade_info.virtual_token_reserves,
|
||||||
trade_info.virtual_sol_reserves,
|
trade_info.virtual_sol_reserves,
|
||||||
trade_info.real_token_reserves,
|
trade_info.real_token_reserves,
|
||||||
trade_info.real_sol_reserves,
|
trade_info.real_sol_reserves,
|
||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
trade_info.fee_recipient,
|
||||||
trade_info.token_program,
|
trade_info.token_program,
|
||||||
|
false, // is_cashback_coin: set from event/parser when available
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: address_lookup_table_account,
|
address_lookup_table_account: address_lookup_table_account,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
trade_info.fee_recipient,
|
||||||
trade_info.token_program,
|
trade_info.token_program,
|
||||||
|
false, // is_cashback_coin: set from event/parser when available
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
solana-streamer-sdk = "0.5.0"
|
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
use std::sync::{
|
//! PumpFun 跟单示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
|
||||||
atomic::{AtomicBool, Ordering},
|
//!
|
||||||
Arc,
|
//! 收到 PumpFun 买卖事件后,用事件中的参数(含 is_cashback_coin)构造交易并执行一次买+卖。
|
||||||
};
|
|
||||||
|
|
||||||
|
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||||
|
|
||||||
|
use sol_parser_sdk::grpc::{
|
||||||
|
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||||
|
TransactionFilter, YellowstoneGrpc,
|
||||||
|
};
|
||||||
|
use sol_parser_sdk::DexEvent;
|
||||||
use sol_trade_sdk::common::{
|
use sol_trade_sdk::common::{
|
||||||
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
|
fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig,
|
||||||
};
|
};
|
||||||
@@ -16,143 +22,128 @@ use sol_trade_sdk::{
|
|||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
use solana_sdk::signer::Signer;
|
use solana_sdk::signer::Signer;
|
||||||
use solana_streamer_sdk::match_event;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::parser::PUMPFUN_PROGRAM_ID;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
|
||||||
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
|
|
||||||
use solana_streamer_sdk::streaming::YellowstoneGrpc;
|
|
||||||
|
|
||||||
// Global static flag to ensure transaction is executed only once
|
|
||||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Subscribing to GRPC events...");
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
println!("PumpFun 跟单示例(sol-parser-sdk gRPC)...");
|
||||||
|
|
||||||
let grpc = YellowstoneGrpc::new(
|
let config = ClientConfig {
|
||||||
"https://solana-yellowstone-grpc.publicnode.com:443".to_string(),
|
enable_metrics: false,
|
||||||
None,
|
connection_timeout_ms: 10000,
|
||||||
)?;
|
request_timeout_ms: 30000,
|
||||||
|
enable_tls: true,
|
||||||
let callback = create_event_callback();
|
order_mode: OrderMode::Unordered,
|
||||||
let protocols = vec![Protocol::PumpFun];
|
..Default::default()
|
||||||
// Filter accounts
|
|
||||||
let account_include = vec![
|
|
||||||
PUMPFUN_PROGRAM_ID.to_string(), // Listen to pumpfun program ID
|
|
||||||
];
|
|
||||||
let account_exclude = vec![];
|
|
||||||
let account_required = vec![];
|
|
||||||
|
|
||||||
// Listen to transaction data
|
|
||||||
let transaction_filter = TransactionFilter {
|
|
||||||
account_include: account_include.clone(),
|
|
||||||
account_exclude,
|
|
||||||
account_required,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to account data belonging to owner programs -> account event monitoring
|
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||||
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
|
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
|
||||||
|
let grpc = YellowstoneGrpc::new_with_config(
|
||||||
|
grpc_endpoint.clone(),
|
||||||
|
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||||
|
config,
|
||||||
|
)?;
|
||||||
|
|
||||||
// listen to specific event type
|
let protocols = vec![Protocol::PumpFun];
|
||||||
let event_type_filter =
|
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||||
EventTypeFilter { include: vec![EventType::PumpFunBuy, EventType::PumpFunSell] };
|
let account_filter = AccountFilter::for_protocols(&protocols);
|
||||||
|
let event_filter = EventTypeFilter::include_only(vec![
|
||||||
|
EventType::PumpFunBuy,
|
||||||
|
EventType::PumpFunSell,
|
||||||
|
EventType::PumpFunBuyExactSolIn,
|
||||||
|
EventType::PumpFunTrade,
|
||||||
|
]);
|
||||||
|
|
||||||
grpc.subscribe_events_immediate(
|
let queue = grpc
|
||||||
protocols,
|
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||||
None,
|
.await?;
|
||||||
vec![transaction_filter],
|
|
||||||
vec![account_filter],
|
println!("订阅已启动,等待一条 PumpFun 交易后执行跟单(仅一次)...\n");
|
||||||
Some(event_type_filter),
|
|
||||||
None,
|
loop {
|
||||||
callback,
|
if let Some(event) = queue.pop() {
|
||||||
)
|
let run = match &event {
|
||||||
.await?;
|
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunSell(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||||
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
Some(e.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DexEvent::PumpFunTrade(e) => {
|
||||||
|
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
Some(e.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(e) = run {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = pumpfun_copy_trade(e).await {
|
||||||
|
eprintln!("跟单执行错误: {:?}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await?;
|
tokio::signal::ctrl_c().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an event callback function that handles different types of events
|
|
||||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
|
||||||
|event: Box<dyn UnifiedEvent>| {
|
|
||||||
match_event!(event, {
|
|
||||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
|
||||||
// Test code, only test one transaction
|
|
||||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
|
||||||
let event_clone = e.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = pumpfun_copy_trade_with_grpc(event_clone).await {
|
|
||||||
eprintln!("Error in copy trade: {:?}", err);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create SolanaTrade client
|
|
||||||
/// Initializes a new SolanaTrade client with configuration
|
|
||||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||||
println!("🚀 Initializing SolanaTrade client...");
|
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||||
let commitment = CommitmentConfig::confirmed();
|
let commitment = CommitmentConfig::confirmed();
|
||||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||||
println!("✅ SolanaTrade client initialized successfully!");
|
|
||||||
Ok(solana_trade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PumpFun sniper trade
|
async fn pumpfun_copy_trade(
|
||||||
/// This function demonstrates how to snipe a new token from a PumpFun trade event
|
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||||
async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
) -> AnyResult<()> {
|
||||||
println!("Testing PumpFun trading...");
|
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
let client = create_solana_trade_client().await?;
|
||||||
let mint_pubkey = trade_info.mint;
|
let mint_pubkey = e.mint;
|
||||||
let slippage_basis_points = Some(100);
|
let slippage_basis_points = Some(100u64);
|
||||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||||
|
|
||||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||||
gas_fee_strategy.set_global_fee_strategy(
|
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||||
150000,
|
|
||||||
150000,
|
|
||||||
500000,
|
|
||||||
500000,
|
|
||||||
0.001,
|
|
||||||
0.001,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Buy tokens
|
// 买入:使用事件参数,含 is_cashback_coin(来自 sol-parser-sdk 解析)
|
||||||
println!("Buying tokens from PumpFun...");
|
let buy_sol_amount = 100_000u64;
|
||||||
let buy_sol_amount = 100_000;
|
|
||||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
input_token_type: TradeTokenType::SOL,
|
input_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: buy_sol_amount,
|
input_token_amount: buy_sol_amount,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
trade_info.bonding_curve,
|
e.bonding_curve,
|
||||||
trade_info.associated_bonding_curve,
|
e.associated_bonding_curve,
|
||||||
trade_info.mint,
|
e.mint,
|
||||||
trade_info.creator,
|
e.creator,
|
||||||
trade_info.creator_vault,
|
e.creator_vault,
|
||||||
trade_info.virtual_token_reserves,
|
e.virtual_token_reserves,
|
||||||
trade_info.virtual_sol_reserves,
|
e.virtual_sol_reserves,
|
||||||
trade_info.real_token_reserves,
|
e.real_token_reserves,
|
||||||
trade_info.real_sol_reserves,
|
e.real_sol_reserves,
|
||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
e.fee_recipient,
|
||||||
trade_info.token_program,
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
@@ -167,43 +158,40 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
// Sell tokens
|
// 卖出:查询余额后卖出,同样传入 is_cashback_coin
|
||||||
println!("Selling tokens from PumpFun...");
|
|
||||||
|
|
||||||
let rpc = client.infrastructure.rpc.clone();
|
let rpc = client.infrastructure.rpc.clone();
|
||||||
let payer = client.payer.pubkey();
|
let payer = client.payer.pubkey();
|
||||||
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
let account = get_associated_token_address_with_program_id_fast_use_seed(
|
||||||
&payer,
|
&payer,
|
||||||
&mint_pubkey,
|
&mint_pubkey,
|
||||||
&trade_info.token_program,
|
&e.token_program,
|
||||||
client.use_seed_optimize,
|
client.use_seed_optimize,
|
||||||
);
|
);
|
||||||
let balance = rpc.get_token_account_balance(&account).await?;
|
let balance = rpc.get_token_account_balance(&account).await?;
|
||||||
println!("Balance: {:?}", balance);
|
|
||||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||||
|
|
||||||
println!("Selling {} tokens", amount_token);
|
|
||||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
output_token_type: TradeTokenType::SOL,
|
output_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: amount_token,
|
input_token_amount: amount_token,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
with_tip: false,
|
with_tip: false,
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
trade_info.bonding_curve,
|
e.bonding_curve,
|
||||||
trade_info.associated_bonding_curve,
|
e.associated_bonding_curve,
|
||||||
trade_info.mint,
|
e.mint,
|
||||||
trade_info.creator,
|
e.creator,
|
||||||
trade_info.creator_vault,
|
e.creator_vault,
|
||||||
trade_info.virtual_token_reserves,
|
e.virtual_token_reserves,
|
||||||
trade_info.virtual_sol_reserves,
|
e.virtual_sol_reserves,
|
||||||
trade_info.real_token_reserves,
|
e.real_token_reserves,
|
||||||
trade_info.real_sol_reserves,
|
e.real_sol_reserves,
|
||||||
Some(true),
|
Some(true),
|
||||||
trade_info.fee_recipient,
|
e.fee_recipient,
|
||||||
trade_info.token_program,
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
@@ -212,14 +200,11 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul
|
|||||||
close_mint_token_ata: false,
|
close_mint_token_ata: false,
|
||||||
durable_nonce: None,
|
durable_nonce: None,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell)
|
println!("跟单一次买+卖完成");
|
||||||
// creator_vault can be obtained from the trade event
|
Ok(())
|
||||||
|
|
||||||
// Exit program
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sol-trade-sdk = { path = "../.." }
|
sol-trade-sdk = { path = "../.." }
|
||||||
solana-streamer-sdk = "0.5.0"
|
sol-parser-sdk = { path = "../../../sol-parser-sdk" }
|
||||||
solana-sdk = "3.0.0"
|
solana-sdk = "3.0.0"
|
||||||
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
//! PumpFun 狙击示例(仅使用 sol-parser-sdk 订阅 gRPC 事件)
|
||||||
|
//!
|
||||||
|
//! 监听创建者首次买入(Create 后同笔/首笔 Buy,is_created_buy == true),
|
||||||
|
//! 用事件参数(含 is_cashback_coin)构造 from_dev_trade 并执行一次买+卖。
|
||||||
|
|
||||||
|
use std::sync::{atomic::{AtomicBool, Ordering}, Arc};
|
||||||
|
|
||||||
|
use sol_parser_sdk::grpc::{
|
||||||
|
AccountFilter, ClientConfig, EventType, EventTypeFilter, OrderMode, Protocol,
|
||||||
|
TransactionFilter, YellowstoneGrpc,
|
||||||
|
};
|
||||||
|
use sol_parser_sdk::DexEvent;
|
||||||
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address;
|
||||||
use sol_trade_sdk::common::TradeConfig;
|
use sol_trade_sdk::common::TradeConfig;
|
||||||
use sol_trade_sdk::TradeTokenType;
|
use sol_trade_sdk::TradeTokenType;
|
||||||
@@ -10,108 +22,120 @@ use sol_trade_sdk::{
|
|||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
use solana_sdk::signer::Signer;
|
use solana_sdk::signer::Signer;
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::filter::EventTypeFilter;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::common::EventType;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
|
|
||||||
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
|
|
||||||
use solana_streamer_sdk::{match_event, streaming::ShredStreamGrpc};
|
|
||||||
use std::sync::{
|
|
||||||
atomic::{AtomicBool, Ordering},
|
|
||||||
Arc,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Atomic flag to ensure the sniper trade is executed only once
|
|
||||||
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
static ALREADY_EXECUTED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
/// Main entry point - subscribes to PumpFun events and executes sniper trades on token creation
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("Subscribing to ShredStream events...");
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
let shred_stream = ShredStreamGrpc::new("use_your_shred_stream_url_here".to_string()).await?;
|
println!("PumpFun 狙击示例(sol-parser-sdk gRPC)...");
|
||||||
let callback = create_event_callback();
|
|
||||||
let protocols = vec![Protocol::PumpFun];
|
let config = ClientConfig {
|
||||||
let event_type_filter = EventTypeFilter {
|
enable_metrics: false,
|
||||||
include: vec![EventType::PumpFunBuy, EventType::PumpFunSell, EventType::PumpFunCreateToken],
|
connection_timeout_ms: 10000,
|
||||||
|
request_timeout_ms: 30000,
|
||||||
|
enable_tls: true,
|
||||||
|
order_mode: OrderMode::Unordered,
|
||||||
|
..Default::default()
|
||||||
};
|
};
|
||||||
println!("Starting to listen for events, press Ctrl+C to stop...");
|
|
||||||
shred_stream.shredstream_subscribe(protocols, None, Some(event_type_filter), callback).await?;
|
let grpc_endpoint = std::env::var("GRPC_ENDPOINT")
|
||||||
|
.unwrap_or_else(|_| "https://solana-yellowstone-grpc.publicnode.com:443".to_string());
|
||||||
|
let grpc = YellowstoneGrpc::new_with_config(
|
||||||
|
grpc_endpoint,
|
||||||
|
std::env::var("GRPC_AUTH_TOKEN").ok(),
|
||||||
|
config,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let protocols = vec![Protocol::PumpFun];
|
||||||
|
let transaction_filter = TransactionFilter::for_protocols(&protocols);
|
||||||
|
let account_filter = AccountFilter::for_protocols(&protocols);
|
||||||
|
let event_filter = EventTypeFilter::include_only(vec![
|
||||||
|
EventType::PumpFunCreate,
|
||||||
|
EventType::PumpFunBuy,
|
||||||
|
EventType::PumpFunBuyExactSolIn,
|
||||||
|
]);
|
||||||
|
|
||||||
|
let queue = grpc
|
||||||
|
.subscribe_dex_events(vec![transaction_filter], vec![account_filter], Some(event_filter))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
println!("订阅已启动,等待创建者首次买入(is_created_buy)后执行狙击(仅一次)...\n");
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(event) = queue.pop() {
|
||||||
|
let run = match &event {
|
||||||
|
DexEvent::PumpFunBuy(e) | DexEvent::PumpFunBuyExactSolIn(e) => {
|
||||||
|
if e.is_created_buy && !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
||||||
|
Some(e.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(e) = run {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = pumpfun_sniper_trade(e).await {
|
||||||
|
eprintln!("狙击执行错误: {:?}", err);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
std::process::exit(0);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tokio::signal::ctrl_c().await?;
|
tokio::signal::ctrl_c().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an event callback function that handles different types of events
|
|
||||||
fn create_event_callback() -> impl Fn(Box<dyn UnifiedEvent>) {
|
|
||||||
|event: Box<dyn UnifiedEvent>| {
|
|
||||||
match_event!(event, {
|
|
||||||
PumpFunTradeEvent => |e: PumpFunTradeEvent| {
|
|
||||||
// Only process developer token creation events
|
|
||||||
if !e.is_dev_create_token_trade {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Ensure we only execute the trade once using atomic compare-and-swap
|
|
||||||
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
|
|
||||||
let event_clone = e.clone();
|
|
||||||
// Spawn a new task to handle the trading operation
|
|
||||||
tokio::spawn(async move {
|
|
||||||
if let Err(err) = pumpfun_sniper_trade_with_shreds(event_clone).await {
|
|
||||||
eprintln!("Error in copy trade: {:?}", err);
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create SolanaTrade client
|
|
||||||
/// Initializes a new SolanaTrade client with configuration
|
|
||||||
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||||
println!("🚀 Initializing SolanaTrade client...");
|
|
||||||
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
let payer = Keypair::from_base58_string("use_your_payer_keypair_here");
|
||||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
let rpc_url = std::env::var("RPC_URL").unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||||
let commitment = CommitmentConfig::confirmed();
|
let commitment = CommitmentConfig::confirmed();
|
||||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||||
println!("✅ SolanaTrade client initialized successfully!");
|
|
||||||
Ok(solana_trade)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute PumpFun sniper trading strategy based on received token creation event
|
async fn pumpfun_sniper_trade(
|
||||||
/// This function buys tokens immediately after creation and then sells all tokens
|
e: sol_parser_sdk::core::events::PumpFunTradeEvent,
|
||||||
async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyResult<()> {
|
) -> AnyResult<()> {
|
||||||
println!("Testing PumpFun trading...");
|
|
||||||
|
|
||||||
let client = create_solana_trade_client().await?;
|
let client = create_solana_trade_client().await?;
|
||||||
let mint_pubkey = trade_info.mint;
|
let mint_pubkey = e.mint;
|
||||||
let slippage_basis_points = Some(300);
|
let slippage_basis_points = Some(300u64);
|
||||||
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
let recent_blockhash = client.infrastructure.rpc.get_latest_blockhash().await?;
|
||||||
|
|
||||||
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new();
|
||||||
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
gas_fee_strategy.set_global_fee_strategy(150000, 150000, 500000, 500000, 0.001, 0.001);
|
||||||
|
|
||||||
// Buy tokens
|
// 创建者首次买入:用 from_dev_trade,max_sol_cost 用事件中的 sol_amount(可酌情加滑点)
|
||||||
println!("Buying tokens from PumpFun...");
|
let buy_sol_amount = 100_000u64;
|
||||||
let buy_sol_amount = 100_000;
|
let max_sol_cost = e.sol_amount.saturating_add(e.sol_amount / 10); // 约 +10% 作为上限
|
||||||
|
|
||||||
let buy_params = sol_trade_sdk::TradeBuyParams {
|
let buy_params = sol_trade_sdk::TradeBuyParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
input_token_type: TradeTokenType::SOL,
|
input_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: buy_sol_amount,
|
input_token_amount: buy_sol_amount,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade(
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_dev_trade(
|
||||||
trade_info.mint,
|
e.mint,
|
||||||
trade_info.token_amount,
|
e.token_amount,
|
||||||
trade_info.max_sol_cost,
|
max_sol_cost,
|
||||||
trade_info.creator,
|
e.creator,
|
||||||
trade_info.bonding_curve,
|
e.bonding_curve,
|
||||||
trade_info.associated_bonding_curve,
|
e.associated_bonding_curve,
|
||||||
trade_info.creator_vault,
|
e.creator_vault,
|
||||||
None,
|
None,
|
||||||
trade_info.fee_recipient,
|
e.fee_recipient,
|
||||||
trade_info.token_program,
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
)),
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
@@ -126,26 +150,35 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
|
|||||||
};
|
};
|
||||||
client.buy(buy_params).await?;
|
client.buy(buy_params).await?;
|
||||||
|
|
||||||
// Sell tokens
|
|
||||||
println!("Selling tokens from PumpFun...");
|
|
||||||
|
|
||||||
let rpc = client.infrastructure.rpc.clone();
|
let rpc = client.infrastructure.rpc.clone();
|
||||||
let payer = client.payer.pubkey();
|
let payer = client.payer.pubkey();
|
||||||
let account = get_associated_token_address(&payer, &mint_pubkey);
|
let account = get_associated_token_address(&payer, &mint_pubkey);
|
||||||
let balance = rpc.get_token_account_balance(&account).await?;
|
let balance = rpc.get_token_account_balance(&account).await?;
|
||||||
println!("Balance: {:?}", balance);
|
|
||||||
let amount_token = balance.amount.parse::<u64>().unwrap();
|
let amount_token = balance.amount.parse::<u64>().unwrap();
|
||||||
|
|
||||||
println!("Selling {} tokens", amount_token);
|
|
||||||
let sell_params = sol_trade_sdk::TradeSellParams {
|
let sell_params = sol_trade_sdk::TradeSellParams {
|
||||||
dex_type: DexType::PumpFun,
|
dex_type: DexType::PumpFun,
|
||||||
output_token_type: TradeTokenType::SOL,
|
output_token_type: TradeTokenType::SOL,
|
||||||
mint: mint_pubkey,
|
mint: mint_pubkey,
|
||||||
input_token_amount: amount_token,
|
input_token_amount: amount_token,
|
||||||
slippage_basis_points: slippage_basis_points,
|
slippage_basis_points,
|
||||||
recent_blockhash: Some(recent_blockhash),
|
recent_blockhash: Some(recent_blockhash),
|
||||||
with_tip: false,
|
with_tip: false,
|
||||||
extension_params: DexParamEnum::PumpFun(PumpFunParams::immediate_sell(trade_info.creator_vault, trade_info.token_program, true)),
|
extension_params: DexParamEnum::PumpFun(PumpFunParams::from_trade(
|
||||||
|
e.bonding_curve,
|
||||||
|
e.associated_bonding_curve,
|
||||||
|
e.mint,
|
||||||
|
e.creator,
|
||||||
|
e.creator_vault,
|
||||||
|
e.virtual_token_reserves,
|
||||||
|
e.virtual_sol_reserves,
|
||||||
|
e.real_token_reserves,
|
||||||
|
e.real_sol_reserves,
|
||||||
|
Some(true),
|
||||||
|
e.fee_recipient,
|
||||||
|
e.token_program,
|
||||||
|
e.is_cashback_coin,
|
||||||
|
)),
|
||||||
address_lookup_table_account: None,
|
address_lookup_table_account: None,
|
||||||
wait_transaction_confirmed: true,
|
wait_transaction_confirmed: true,
|
||||||
create_output_token_ata: true,
|
create_output_token_ata: true,
|
||||||
@@ -153,14 +186,11 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR
|
|||||||
close_mint_token_ata: false,
|
close_mint_token_ata: false,
|
||||||
durable_nonce: None,
|
durable_nonce: None,
|
||||||
fixed_output_token_amount: None,
|
fixed_output_token_amount: None,
|
||||||
gas_fee_strategy: gas_fee_strategy,
|
gas_fee_strategy,
|
||||||
simulate: false,
|
simulate: false,
|
||||||
};
|
};
|
||||||
client.sell(sell_params).await?;
|
client.sell(sell_params).await?;
|
||||||
|
|
||||||
// PumpFunParams can also be set as PumpFunParams::immediate_sell(creator_vault, close_token_account_when_sell)
|
println!("狙击一次买+卖完成");
|
||||||
// creator_vault can be obtained from the trade event
|
Ok(())
|
||||||
|
|
||||||
// Exit program after completing the trade
|
|
||||||
std::process::exit(0);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7098
File diff suppressed because it is too large
Load Diff
+6268
File diff suppressed because it is too large
Load Diff
+2761
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(¶ms.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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(¶ms.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(¶ms.payer.pubkey()),
|
||||||
|
get_user_volume_accumulator_pda(¶ms.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,
|
||||||
|
"e_mint,
|
||||||
|
"e_token_program,
|
||||||
|
);
|
||||||
|
// IDL order: user, user_volume_accumulator, quote_mint, quote_token_program,
|
||||||
|
// user_volume_accumulator_wsol_token_account, user_wsol_token_account, system_program, event_authority, program
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new(*payer, true), // user (signer, writable)
|
||||||
|
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable)
|
||||||
|
AccountMeta::new_readonly(quote_mint, false),
|
||||||
|
AccountMeta::new_readonly(quote_token_program, false),
|
||||||
|
AccountMeta::new(user_volume_accumulator_wsol_ata, false), // writable
|
||||||
|
AccountMeta::new(user_wsol_ata, false), // writable
|
||||||
|
crate::constants::SYSTEM_PROGRAM_META,
|
||||||
|
accounts::EVENT_AUTHORITY_META,
|
||||||
|
accounts::AMM_PROGRAM_META,
|
||||||
|
];
|
||||||
|
Some(solana_sdk::instruction::Instruction::new_with_bytes(
|
||||||
|
accounts::AMM_PROGRAM,
|
||||||
|
&CLAIM_CASHBACK_DISCRIMINATOR,
|
||||||
|
accounts,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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(
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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!({
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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!({
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user