Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6db55c874 | |||
| 69b4f9bb9f | |||
| 532bc1195d | |||
| 8944c5455c | |||
| 0d846930ad | |||
| 703ae5e4db | |||
| 8e0210af97 | |||
| e605485ecf | |||
| 8dec2f8d8c | |||
| b505bebdac | |||
| 1cc051a874 | |||
| 0311a0b876 | |||
| b49c1c803e | |||
| ce8fb99626 | |||
| 49843d8a20 | |||
| 60d1db4755 | |||
| 7a8d2a9c17 | |||
| 75bfbf6d54 | |||
| 184f1cc583 | |||
| 55c13e2f25 | |||
| fda211ea87 | |||
| 85d2c602f3 | |||
| 02d939b3cf | |||
| e8ec9103ab | |||
| 0fe54f0e94 | |||
| 7ac07247a3 | |||
| 06ed710869 | |||
| d711346c55 | |||
| 06ef2fed84 | |||
| a391539000 | |||
| c75ca4b034 | |||
| b13b4fda0a | |||
| 4dec087ea6 | |||
| 9c9ecf3e5b | |||
| 4b451af5ff | |||
| 8f2f99f3d9 | |||
| 971ef41fad | |||
| 35bfa93516 | |||
| c8f9f9f6aa | |||
| 99846a21c2 | |||
| 062c5415c3 | |||
| eaa3214e4d | |||
| ae890ad976 | |||
| e10ee0de3b | |||
| 5e1fb1a66b | |||
| a5ea26afaf | |||
| ee4a8f685b | |||
| 322acf8baf | |||
| 378b8fc324 |
+25
-22
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.2"
|
||||
version = "4.0.8"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
@@ -8,7 +8,7 @@ authors = [
|
||||
"wei <1415121722@qq.com>",
|
||||
]
|
||||
repository = "https://github.com/0xfnzero/sol-trade-sdk"
|
||||
description = "Rust SDK to interact with the dex trade Solana program."
|
||||
description = "A high-performance Rust SDK for Solana DEX trading."
|
||||
license = "MIT"
|
||||
keywords = ["solana", "memecoins", "pumpfun", "pumpswap", "raydium"]
|
||||
readme = "README.md"
|
||||
@@ -37,7 +37,7 @@ members = [
|
||||
]
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
crate-type = ["rlib"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
@@ -45,23 +45,26 @@ perf-trace = [] # 性能追踪特性,生产环境应禁用以获得最佳性
|
||||
|
||||
[dependencies]
|
||||
solana-sdk = "3.0.0"
|
||||
solana-client = "3.1.9"
|
||||
solana-client = "3.1.12"
|
||||
solana-program = "3.0.0"
|
||||
solana-rpc-client = "3.1.9"
|
||||
solana-rpc-client-api = "3.1.9"
|
||||
solana-transaction-status = "3.1.9"
|
||||
solana-account-decoder = "3.1.9"
|
||||
solana-rpc-client = "3.1.12"
|
||||
solana-rpc-client-api = "3.1.12"
|
||||
solana-transaction-status = "3.1.12"
|
||||
solana-account-decoder = "3.1.12"
|
||||
solana-hash = "3.0.0"
|
||||
solana-entry = "3.1.9"
|
||||
solana-rpc-client-nonce-utils = "3.1.9"
|
||||
solana-perf = "3.1.9"
|
||||
solana-metrics = "3.1.9"
|
||||
solana-nonce = "3.1.0"
|
||||
solana-address-lookup-table-interface = "3.0.1"
|
||||
solana-entry = "3.0.0"
|
||||
solana-rpc-client-nonce-utils = "3.1.12"
|
||||
solana-perf = "3.1.12"
|
||||
solana-metrics = "3.1.12"
|
||||
solana-tls-utils = "3.1.12"
|
||||
solana-nonce = "3.2.0"
|
||||
|
||||
solana-address-lookup-table-interface = "3.0.0"
|
||||
solana-message = "3.1.0"
|
||||
solana-compute-budget-interface = "3.0.0"
|
||||
solana-commitment-config = { version = "3.1.1", features = ["serde"] }
|
||||
solana-transaction-status-client-types = "3.1.9"
|
||||
solana-tls-utils = "3.1.9"
|
||||
solana-transaction-status-client-types = "3.1.12"
|
||||
solana-system-interface = { version = "3.0.0", features = ["bincode"] }
|
||||
|
||||
borsh = { version = "1.5.3", features = ["derive"] }
|
||||
isahc = "1.7.2"
|
||||
@@ -76,7 +79,7 @@ bincode = "1.3.3"
|
||||
anyhow = "1.0.90"
|
||||
reqwest = { version = "0.12.12", features = ["json", "multipart"] }
|
||||
tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]}
|
||||
tonic = { version = "0.14.2", features = ["transport"] }
|
||||
tonic = { version = "0.12", features = ["transport"] }
|
||||
rustls = { version = "0.23.23", features = ["ring"] }
|
||||
rustls-native-certs = "0.8.1"
|
||||
tokio-rustls = "0.26.1"
|
||||
@@ -86,10 +89,9 @@ regex = "1"
|
||||
tracing = "0.1.41"
|
||||
thiserror = "2.0.11"
|
||||
async-trait = "0.1.86"
|
||||
lazy_static = "1.5.0"
|
||||
once_cell = "1.20.3"
|
||||
prost = "0.14.1"
|
||||
prost-types = "0.14.1"
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
num_enum = "0.7.3"
|
||||
num-derive = "0.4.2"
|
||||
num-traits = "0.2.19"
|
||||
@@ -98,7 +100,7 @@ bytemuck = { version = "1.4.0" }
|
||||
arrayref = "0.3.6"
|
||||
borsh-derive = "1.5.5"
|
||||
indicatif = "0.18.0"
|
||||
solana-system-interface = { version = "2.0.0", features = ["bincode"] }
|
||||
|
||||
fnv = "1.0.7"
|
||||
dashmap = "6.1.0"
|
||||
clru = "0.6"
|
||||
@@ -107,7 +109,8 @@ parking_lot = "0.12"
|
||||
arc-swap = "1.7"
|
||||
sha2 = "0.10"
|
||||
tonic-prost = "0.14.2"
|
||||
quinn = { version = "0.11", default-features = false, features = ["rustls"] }
|
||||
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime found,QUIC(Speedlanding/Soyas)无法初始化
|
||||
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
|
||||
rcgen = "0.13"
|
||||
uuid = "1.11"
|
||||
|
||||
|
||||
@@ -39,6 +39,12 @@
|
||||
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
||||
</p>
|
||||
|
||||
> ☕ **Support This Project**
|
||||
>
|
||||
> This SDK is completely free and open source. However, maintaining and continuously updating it requires significant AI computing resources and token consumption. If this SDK helps with your trading development, consider making a monthly SOL donation — any amount is appreciated and helps keep this project alive!
|
||||
>
|
||||
> **Donation Wallet:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [✨ Features](#-features)
|
||||
@@ -48,11 +54,12 @@
|
||||
- [⚡ Trading Parameters](#-trading-parameters)
|
||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||
- [Astralane QUIC (Low-Latency)](#astralane-quic-low-latency)
|
||||
- [Astralane (Binary / Plain / QUIC)](#astralane-binary--plain--quic)
|
||||
- [🔧 Middleware System](#-middleware-system)
|
||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||
- [🔍 Nonce Cache](#-nonce-cache)
|
||||
- [💰 Cashback Support (PumpFun / PumpSwap)](#-cashback-support-pumpfun--pumpswap)
|
||||
- [🔄 PumpFun V1 vs V2 Instructions](#-pumpfun-v1-vs-v2-instructions)
|
||||
- [🛡️ MEV Protection Services](#️-mev-protection-services)
|
||||
- [📁 Project Structure](#-project-structure)
|
||||
- [📄 License](#-license)
|
||||
@@ -61,9 +68,20 @@
|
||||
|
||||
---
|
||||
|
||||
## 📦 SDK Versions
|
||||
|
||||
This SDK is available in multiple languages:
|
||||
|
||||
| Language | Repository | Description |
|
||||
|----------|------------|-------------|
|
||||
| **Rust** | [sol-trade-sdk](https://github.com/0xfnzero/sol-trade-sdk) | Ultra-low latency with zero-copy optimization |
|
||||
| **Node.js** | [sol-trade-sdk-nodejs](https://github.com/0xfnzero/sol-trade-sdk-nodejs) | TypeScript/JavaScript for Node.js |
|
||||
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
|
||||
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
|
||||
|
||||
## ✨ Features
|
||||
|
||||
1. **PumpFun Trading**: Support for `buy` and `sell` operations
|
||||
1. **PumpFun Trading**: Support for `buy`, `sell`, `buy_exact_sol_in`, and the new unified `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` instructions (SOL + USDC)
|
||||
2. **PumpSwap Trading**: Support for PumpSwap pool trading operations
|
||||
3. **Bonk Trading**: Support for Bonk trading operations
|
||||
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
|
||||
@@ -90,14 +108,14 @@ Add the dependency to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.2" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
|
||||
```
|
||||
|
||||
### Use crates.io
|
||||
|
||||
```toml
|
||||
# Add to your Cargo.toml
|
||||
sol-trade-sdk = "4.0.2"
|
||||
sol-trade-sdk = "4.0.8"
|
||||
```
|
||||
|
||||
## 🛠️ Usage Examples
|
||||
@@ -119,22 +137,23 @@ let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane: HTTP (4th param None) or QUIC (Some(SwqosTransport::Quic)); same API key
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
), // QUIC
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// Create TradeConfig instance
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
|
||||
// Optional: customize WSOL ATA and seed optimization
|
||||
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true - check & create WSOL ATA on init
|
||||
// .use_seed_optimize(true) // default: true - seed optimization for ATA ops
|
||||
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
|
||||
// .check_min_tip(false) // default: false - filter SWQOS below min tip
|
||||
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
|
||||
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
|
||||
// .use_pumpfun_v2(false) // default: false - V1 (18 accounts); set true for V2 (27 accounts, quote_mint) when PumpFun deploys V2
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
@@ -252,7 +271,7 @@ let jito_config = SwqosConfig::Jito(
|
||||
);
|
||||
|
||||
// Using default regional endpoint (third parameter is None)
|
||||
let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
let temporal_config = SwqosConfig::Temporal(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::NewYork, // Will use the default endpoint for this region
|
||||
None // No custom URL, uses SwqosRegion
|
||||
@@ -266,28 +285,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||
|
||||
#### Astralane QUIC (Low-Latency)
|
||||
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||
|
||||
Astralane supports both HTTP and **QUIC** transport. QUIC reduces connection overhead and can lower submission latency. To use the QUIC channel, pass `Some(SwqosTransport::Quic)` as the fourth parameter of `SwqosConfig::Astralane`. Astralane’s QUIC service uses a **single endpoint** (no per-region endpoints); the SDK ignores the `region` (and optional custom URL) when QUIC is selected. You can pass the same region as your other SWQoS configs for consistency.
|
||||
Astralane supports **Binary** HTTP (`/irisb`), **Plain** HTTP (`/iris`), and **QUIC** (`host:7000`, or `:9000` when global `mev_protection` is true). Pass `Some(AstralaneTransport::Plain)`, `Some(AstralaneTransport::Quic)`, or use `None` / omit for **Binary** (default). Global `mev_protection` adds `mev-protect=true` on HTTP or selects QUIC port 9000.
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, SwqosTransport};
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
|
||||
|
||||
// Astralane over QUIC (low-latency); region is ignored (single QUIC endpoint)
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt, // same as other services; ignored for QUIC
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
Some(AstralaneTransport::Quic),
|
||||
),
|
||||
];
|
||||
// Then create TradeConfig / TradingClient as usual with swqos_configs
|
||||
```
|
||||
|
||||
- **HTTP** (default): use `None` or `Some(SwqosTransport::Http)`; region and optional custom URL apply.
|
||||
- **QUIC**: use `Some(SwqosTransport::Quic)`; the SDK uses a single QUIC endpoint and ignores region. Same API key as HTTP.
|
||||
- **Binary** (default): `None` or `Some(AstralaneTransport::Binary)` — `/irisb`, bincode body.
|
||||
- **Plain**: `Some(AstralaneTransport::Plain)` — `/iris`.
|
||||
- **QUIC**: `Some(AstralaneTransport::Quic)` — regional `host:7000` / `:9000` (MEV); same API key.
|
||||
|
||||
---
|
||||
|
||||
@@ -321,6 +340,10 @@ PumpFun and PumpSwap support **cashback** for eligible tokens: part of the tradi
|
||||
- The **pumpfun_copy_trading** and **pumpfun_sniper_trading** examples use sol-parser-sdk for gRPC subscription and pass `e.is_cashback_coin` when building params.
|
||||
- **Claim**: Use `client.claim_cashback_pumpfun()` and `client.claim_cashback_pumpswap(...)` to claim accumulated cashback.
|
||||
|
||||
#### PumpFun: troubleshooting (on-chain errors)
|
||||
|
||||
For **Anchor 2006 / `NotAuthorized` (6000) / wrong token program / BuyZeroAmount (6020) / slippage (6042)** and related issues, see **[docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)** (Chinese). An English appendix may be added later.
|
||||
|
||||
#### PumpFun: Creator Rewards Sharing (creator_vault)
|
||||
|
||||
Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vault` can differ from the default derivation. If you reuse cached params from a **buy** when **selling**, you may see program error **2006 (seeds constraint violated)**. To avoid this:
|
||||
@@ -332,25 +355,69 @@ Some PumpFun coins use **Creator Rewards Sharing**, so the on-chain `creator_vau
|
||||
|
||||
The SDK does not fetch creator_vault from RPC on every sell (to avoid latency); pass the up-to-date vault from gRPC/events when available.
|
||||
|
||||
#### PumpFun V1 vs V2 Instructions
|
||||
|
||||
PumpFun has two instruction sets for bonding-curve trading:
|
||||
|
||||
| | V1 (default) | V2 (opt-in) |
|
||||
|---|---|---|
|
||||
| Instructions | `buy` / `buy_exact_sol_in` / `sell` | `buy_v2` / `buy_exact_quote_in_v2` / `sell_v2` |
|
||||
| Account metas | 18 | 27 |
|
||||
| Quote mint | SOL only (legacy) | SOL or USDC (via `quote_mint` field) |
|
||||
| Transaction size | Smaller (fits `PACKET_DATA_SIZE` without LUT) | Larger (requires LUT for most transactions) |
|
||||
|
||||
**Default: V1** (`use_pumpfun_v2 = false`). The SDK uses V1 instructions which produce smaller transactions that fit within the 1232-byte `PACKET_DATA_SIZE` limit without requiring an Address Lookup Table.
|
||||
|
||||
**Key changes in v2 instructions:**
|
||||
- `quote_mint` parameter — pass wrapped SOL for SOL-paired, or USDC mint for USDC-paired
|
||||
- 27 fixed accounts (buy) / 26 fixed accounts (sell) — **no optional accounts**
|
||||
- `buyback_fee_recipient`, `sharing_config`, and 6 `associated_quote_*` ATAs are now mandatory
|
||||
- Same pricing and cost as legacy instructions for SOL-paired coins
|
||||
|
||||
**How to enable V2:**
|
||||
|
||||
**Method 1 — Global runtime flag** (recommended when PumpFun officially deploys V2 on mainnet):
|
||||
|
||||
```rust
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.use_pumpfun_v2(true) // Switch all PumpFun trades to V2 instructions (27 accounts)
|
||||
.build();
|
||||
```
|
||||
|
||||
**Method 2 — Per-trade via `quote_mint`** (for USDC-paired coins or mixed V1/V2 scenarios):
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
// SOL-paired coin with v2 layout
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
|
||||
|
||||
// USDC-paired coin (requires v2)
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(USDC_TOKEN_ACCOUNT);
|
||||
```
|
||||
|
||||
> **Note**: V2 transactions with ATA creation + durable nonce may exceed `PACKET_DATA_SIZE`. Enable an Address Lookup Table (`address_lookup_table_account`) when using V2.
|
||||
|
||||
#### PumpSwap: coin_creator_vault from events (no RPC)
|
||||
|
||||
For **PumpSwap** (Pump AMM), `coin_creator_vault_ata` and `coin_creator_vault_authority` are required in buy/sell instructions. Both are available from parsed events without RPC:
|
||||
|
||||
- **sol-parser-sdk**: Instruction parser sets them from accounts 17 and 18; the account filler also fills them when the event comes from logs. Use `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` with the buy/sell event `e`.
|
||||
- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event’s `coin_creator_vault_ata` and `coin_creator_vault_authority`.
|
||||
- **solana-streamer**: Instruction parser sets them from `accounts.get(17)` and `accounts.get(18)`. Use the same `from_trade` with the event's `coin_creator_vault_ata` and `coin_creator_vault_authority`.
|
||||
|
||||
## 🛡️ MEV Protection Services
|
||||
|
||||
You can apply for a key through the official website: [Community Website](https://fnzero.dev/swqos)
|
||||
|
||||
- **Jito**: High-performance block space
|
||||
- **ZeroSlot**: Zero-latency transactions
|
||||
- **Temporal**: Time-sensitive transactions
|
||||
- **Bloxroute**: Blockchain network acceleration
|
||||
- **FlashBlock**: High-speed transaction execution with API key authentication
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication
|
||||
- **Node1**: High-speed transaction execution with API key authentication
|
||||
- **Astralane**: Blockchain network acceleration (supports HTTP and QUIC; see [Astralane QUIC](#astralane-quic-low-latency) above)
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC)
|
||||
- **SpeedLanding**: High-speed transaction execution with API key authentication
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
|
||||
+89
-32
@@ -39,6 +39,12 @@
|
||||
<a href="https://discord.gg/vuazbGkqQE">Discord</a>
|
||||
</p>
|
||||
|
||||
> ☕ **支持本项目**
|
||||
>
|
||||
> 本 SDK 完全免费且开源。但维护和持续更新需要消耗大量 AI 算力与 Token。如果这个 SDK 对您的开发有帮助,欢迎每月捐赠任意数量的 SOL,您的支持将帮助这个项目持续运行!
|
||||
>
|
||||
> **捐赠钱包:** `6oW7AXz1yRb57pYSxysuXnMs2aR1ha5rzGzReZ1MjPV8`
|
||||
|
||||
## 📋 目录
|
||||
|
||||
- [✨ 项目特性](#-项目特性)
|
||||
@@ -48,11 +54,12 @@
|
||||
- [⚡ 交易参数](#-交易参数)
|
||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||
- [Astralane QUIC(低延迟)](#astralane-quic低延迟)
|
||||
- [Astralane(Binary / Plain / QUIC)](#astralanebinary--plain--quic)
|
||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||
- [🔍 地址查找表](#-地址查找表)
|
||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||
- [💰 Cashback 支持(PumpFun / PumpSwap)](#-cashback-支持pumpfun--pumpswap)
|
||||
- [Pump.fun 常见链上错误与排错(文档)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)
|
||||
- [🛡️ MEV 保护服务](#️-mev-保护服务)
|
||||
- [📁 项目结构](#-项目结构)
|
||||
- [📄 许可证](#-许可证)
|
||||
@@ -61,15 +68,26 @@
|
||||
|
||||
---
|
||||
|
||||
## 📦 SDK 版本
|
||||
|
||||
本 SDK 提供多种语言版本:
|
||||
|
||||
| 语言 | 仓库 | 描述 |
|
||||
|------|------|------|
|
||||
| **Rust** | [sol-trade-sdk](https://github.com/0xfnzero/sol-trade-sdk) | 超低延迟,零拷贝优化 |
|
||||
| **Node.js** | [sol-trade-sdk-nodejs](https://github.com/0xfnzero/sol-trade-sdk-nodejs) | TypeScript/JavaScript,Node.js 支持 |
|
||||
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
|
||||
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
|
||||
|
||||
## ✨ 项目特性
|
||||
|
||||
1. **PumpFun 交易**: 支持`购买`、`卖出`功能
|
||||
1. **PumpFun 交易**: 支持 `buy`、`sell`、`buy_exact_sol_in` 以及全新的统一化 `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` 指令(SOL + USDC)
|
||||
2. **PumpSwap 交易**: 支持 PumpSwap 池的交易操作
|
||||
3. **Bonk 交易**: 支持 Bonk 的交易操作
|
||||
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
|
||||
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
|
||||
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
|
||||
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane 等服务
|
||||
7. **多种 MEV 保护**: 支持 Jito、Temporal、FlashBlock、BlockRazor、Astralane、SpeedLanding 等服务
|
||||
8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败
|
||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||
@@ -90,14 +108,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.2" }
|
||||
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.8" }
|
||||
```
|
||||
|
||||
### 使用 crates.io
|
||||
|
||||
```toml
|
||||
# 添加到您的 Cargo.toml
|
||||
sol-trade-sdk = "4.0.2"
|
||||
sol-trade-sdk = "4.0.8"
|
||||
```
|
||||
|
||||
## 🛠️ 使用示例
|
||||
@@ -119,22 +137,22 @@ let commitment = CommitmentConfig::processed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane:第4个参数 None 为 HTTP,Some(SwqosTransport::Quic) 为 QUIC;同一 API key
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
), // QUIC
|
||||
SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain(/iris)或 Quic
|
||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
|
||||
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
|
||||
// 可选:自定义 WSOL ATA 与 Seed 优化
|
||||
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
|
||||
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // 默认: true - 初始化时检查并创建 WSOL ATA
|
||||
// .use_seed_optimize(true) // 默认: true - ATA 操作启用 seed 优化
|
||||
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
|
||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
@@ -251,7 +269,7 @@ let jito_config = SwqosConfig::Jito(
|
||||
);
|
||||
|
||||
// 使用默认区域端点(第三个参数为 None)
|
||||
let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
let temporal_config = SwqosConfig::Temporal(
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::NewYork, // 将使用该区域的默认端点
|
||||
None // 没有自定义 URL,使用 SwqosRegion
|
||||
@@ -265,28 +283,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
||||
|
||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||
|
||||
#### Astralane QUIC(低延迟)
|
||||
#### Astralane(Binary / Plain / QUIC)
|
||||
|
||||
Astralane 支持 **HTTP** 与 **QUIC** 两种传输方式。QUIC 可减少连接开销,降低提交延迟。使用 QUIC 时,将 `SwqosConfig::Astralane` 的第四个参数设为 `Some(SwqosTransport::Quic)`。Astralane 的 QUIC 服务使用**单一端点**(无分区域端点),选 QUIC 时 SDK 会忽略 `region` 与可选自定义 URL;为与其他 SWQoS 配置一致,可传入相同 region。
|
||||
Astralane 支持 **Binary** HTTP(`/irisb`)、**Plain** HTTP(`/iris`)与 **QUIC**(`host:7000`,全局 `mev_protection` 为 true 时用 `:9000`)。第四个参数:`Some(AstralaneTransport::Plain)`、`Some(AstralaneTransport::Quic)`,或 `None` 表示 **Binary**(默认)。全局 `mev_protection` 会在 HTTP 上附加 `mev-protect=true`,或为 QUIC 选择 9000 端口。
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, SwqosTransport};
|
||||
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
|
||||
|
||||
// Astralane 使用 QUIC(低延迟);region 会被忽略(QUIC 单一端点)
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
SwqosConfig::Default(rpc_url.clone()),
|
||||
SwqosConfig::Astralane(
|
||||
"your_astralane_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt, // 与其他服务一致即可;QUIC 时会被忽略
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
Some(AstralaneTransport::Quic),
|
||||
),
|
||||
];
|
||||
// 然后照常使用 swqos_configs 创建 TradeConfig / TradingClient
|
||||
```
|
||||
|
||||
- **HTTP**(默认):第四个参数为 `None` 或 `Some(SwqosTransport::Http)`;region 与可选自定义 URL 生效。
|
||||
- **QUIC**:第四个参数为 `Some(SwqosTransport::Quic)`;SDK 使用单一 QUIC 端点并忽略 region。与 HTTP 使用同一 API key。
|
||||
- **Binary**(默认):`None` 或 `Some(AstralaneTransport::Binary)` — `/irisb`,bincode 正文。
|
||||
- **Plain**:`Some(AstralaneTransport::Plain)` — `/iris`。
|
||||
- **QUIC**:`Some(AstralaneTransport::Quic)` — 按区域的 `host:7000` / `:9000`(MEV);同一 API key。
|
||||
|
||||
---
|
||||
|
||||
@@ -320,6 +338,10 @@ PumpFun 与 PumpSwap 支持**返现(Cashback)**:部分手续费可返还
|
||||
- **pumpfun_copy_trading**、**pumpfun_sniper_trading** 示例使用 sol-parser-sdk 订阅 gRPC 事件,并在构造参数时传入 `e.is_cashback_coin`。
|
||||
- **领取返现**:使用 `client.claim_cashback_pumpfun()` 和 `client.claim_cashback_pumpswap(...)` 领取累计的返现。
|
||||
|
||||
#### PumpFun:常见错误与排错思路
|
||||
|
||||
实盘集成时若遇 **Anchor 2006、`NotAuthorized`(6000)、Token program 不匹配、6020/6042** 等,请参阅专门文档:**[Pump.fun 常见链上错误与处理思路(中文)](docs/PUMP_ERRORS_AND_TROUBLESHOOTING_CN.md)**。
|
||||
|
||||
#### PumpFun:Creator Rewards Sharing(creator_vault)
|
||||
|
||||
部分 PumpFun 代币启用了 **Creator Rewards Sharing**,链上 `creator_vault` 可能与默认推导结果不同。若在**卖出**时复用**买入**时缓存的 params,可能触发程序错误 **2006(seeds constraint violated)**。建议:
|
||||
@@ -338,18 +360,53 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
|
||||
- **sol-parser-sdk**:指令解析从账户 17、18 写入;若事件来自日志,账户填充器也会从指令补全。用 `PumpSwapParams::from_trade(..., e.coin_creator_vault_ata, e.coin_creator_vault_authority, ...)` 即可。
|
||||
- **solana-streamer**:指令解析从 `accounts.get(17)`、`accounts.get(18)` 写入。同样用事件的 `coin_creator_vault_ata`、`coin_creator_vault_authority` 调用 `from_trade`。
|
||||
|
||||
### Pump.fun Bonding Curve v2(buy_v2 / sell_v2 / buy_exact_quote_in_v2)
|
||||
|
||||
Pump.fun 已升级 Bonding Curve 合约,推出**统一化 v2 指令**,通过固定账户布局同时支持 SOL 和 USDC 配对币。旧版 `buy`/`sell`/`buy_exact_sol_in` 仍可用于 SOL 配对币,且保持为默认选项。
|
||||
|
||||
**v2 指令关键变化:**
|
||||
- 新增 `quote_mint` 参数 — SOL 配对传包装 SOL(`So11111111111111111111111111111111111111112`),USDC 配对传 USDC mint
|
||||
- 27 个固定账户(buy)/ 26 个固定账户(sell)— **无可选账户**
|
||||
- `buyback_fee_recipient`、`sharing_config` 和 6 个 `associated_quote_*` ATA 变为强制账户
|
||||
- SOL 配对币的报价和成本与旧版一致,无额外开销
|
||||
|
||||
**使用方式:**
|
||||
|
||||
设置 `PumpFunParams` 的 `quote_mint` 即可,SDK 会自动切换到 v2 discriminator 和新账户布局:
|
||||
|
||||
```rust
|
||||
use sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
|
||||
use sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
|
||||
|
||||
// SOL 配对币 — 传包装 SOL mint
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(WSOL_TOKEN_ACCOUNT);
|
||||
|
||||
// USDC 配对币(即将开放 — 必须使用 v2)
|
||||
let params = PumpFunParams::from_trade(/* ... */)
|
||||
.with_quote_mint(USDC_TOKEN_ACCOUNT);
|
||||
|
||||
// 之后正常交易
|
||||
client.buy(buy_params).await?;
|
||||
client.sell(sell_params).await?;
|
||||
```
|
||||
|
||||
| quote_mint | use_v2_ix | 实际使用的指令 | 说明 |
|
||||
|-----------|-------------|---------|------|
|
||||
| 未设置(默认) | `false` | 旧版 `buy`/`sell`/`buy_exact_sol_in` | 向后兼容,仅 SOL |
|
||||
| `WSOL_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | SOL 配对,统一布局 |
|
||||
| `USDC_TOKEN_ACCOUNT` | `true` | `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2` | USDC 配对(必须使用 v2) |
|
||||
|
||||
## 🛡️ MEV 保护服务
|
||||
|
||||
可以通过官网申请密钥:[社区官网](https://fnzero.dev/swqos)
|
||||
|
||||
- **Jito**: 高性能区块空间
|
||||
- **ZeroSlot**: 零延迟交易
|
||||
- **Temporal**: 时间敏感交易
|
||||
- **Bloxroute**: 区块链网络加速
|
||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||
- **Node1**: 高速交易执行,支持 API 密钥认证
|
||||
- **Astralane**: 区块链网络加速(支持 HTTP 与 QUIC,见上方 [Astralane QUIC](#astralane-quic低延迟))
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC)
|
||||
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
# Pump.fun(Bonding Curve)常见链上错误与处理思路
|
||||
|
||||
本文档面向使用 **sol-trade-sdk** 组装 Pump.fun Program(`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`)买卖交易的集成方,汇总**实战中高频**的失败形态、日志特征,以及与 SDK 参数的对应关系和**推荐处理方式**。
|
||||
|
||||
> 自定义错误码以仓库内 `idl/pump.json`、`idl/pump_fees.json` 为准;**2006** 等部分错误来自 **Anchor 框架**,不在 Pump 自定义枚举里。
|
||||
|
||||
---
|
||||
|
||||
## 1. Anchor `2006` / `ConstraintSeeds`(`creator_vault`)
|
||||
|
||||
### 现象
|
||||
|
||||
- Solana Explorer / `simulateTransaction`:**Program Error: custom program error: 2006**,或 Anchor 文案 **A seeds constraint was violated**。
|
||||
- 日志里常见于账户 **`creator_vault`**:打印 **Left**(你传入的 pubkey)与 **Right**(程序按当前 curve 推导的 PDA)。
|
||||
|
||||
### 含义
|
||||
|
||||
Pump 校验 `creator_vault` 必须满足程序的 **PDAs seeds**(与 bonding curve 上记录的 **creator / fee-sharing 布局**一致)。传入地址与程序期望不一致即失败。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. **`bonding_curve.creator` 与链上不符**:事件/缓存里用的是 create 交易的 `creator` 字段、`user`,或陈旧快照,与 curve 账户内真实 creator 不一致;据此推导或缓存的 vault 会错。
|
||||
2. **买单侧「旧 vault」被沿用到卖单**:买入时 ix 里的 `creator_vault` 在后续 trade 语境下已不再与程序约束一致(例如 creator / sharing 语义在链上演进),卖单仍填旧地址 → **Left ≠ Right**。
|
||||
3. **Creator Rewards / pump-fees 分成布局**:部分 mint 使用 `sharing-config` 相关 seeds,单靠 `PDA(["creator-vault"], bonding_curve.creator)` 不足以覆盖全部历史状态;Stale 的 offline hint 也会把 resolve 引向错误 vault。
|
||||
4. **Phantom vault**:历史上若用 `Pubkey::default()` 推导出的占位 vault(SDK 常量 `phantom_default_creator_vault`),链上必定失败。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **买入 / 卖出指令**(同一套解析) | 使用 `resolve_creator_vault_for_ix_with_fee_sharing`(`src/instruction/utils/pumpfun.rs`):有 **非 default、非 phantom** 的 ix / 解析器回填 **`creator_vault` 时原样采用**(**不会**再根据 `creator` 做 `get_creator_vault_pda` 覆盖费分成等非传统布局);**未传 ix vault** 时依次:`fee_sharing_creator_vault_if_active` hint → `PDA(effective_creator)`。`PumpFunInstructionBuilder` 买卖均走此逻辑。 |
|
||||
| **权威对齐** | 低延迟路径外,可 **`PumpFunParams::from_mint_by_rpc`** 或由解析器 **`fill_trade_accounts`**(如 sol-parser-sdk)持续刷新 `creator` / `creator_vault`;必要时对 **fee-sharing** 使用 `fetch_fee_sharing_creator_vault_if_active` / `refresh_fee_sharing_creator_vault_from_rpc`。 |
|
||||
| **bonding_curve 账户地址** | 指令构建时使用 **`get_bonding_curve_pda(mint)`** 作为 canonical bonding curve pubkey,避免缓存中的曲线地址错位导致读到错误 **creator**。 |
|
||||
|
||||
集成方若在 **bot** 侧维护持仓快照,建议在**每笔**解析到的 Pump trade 后刷新缓存/仓位中的 `creator` 与 `creator_vault`,避免「只写一次建仓快照」。
|
||||
|
||||
---
|
||||
|
||||
## 2. Pump `6000` `NotAuthorized`(常见:`feeRecipient`)
|
||||
|
||||
### 现象
|
||||
|
||||
- 日志:`AnchorError thrown in programs/pump/src/fee_recipient.rs` 或 **`Error Code: NotAuthorized`**(与 Global 授权的 fee recipient 池有关)。
|
||||
|
||||
### 含义
|
||||
|
||||
账户 **#2 fee recipient** 不是当前 Pump **Global / 协议**允许使用的收款地址之一,或与 **Mayhem / 非 Mayhem** 池不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. 使用了**过期**或**错误池**的 fee recipient(静态列表落后于主网轮换)。
|
||||
2. **`mayhem_mode` 与 `fee_recipient` 不匹配**:声明 Mayhem 却传普通池地址,或相反(见 `reconcile_mayhem_mode_for_trade`,`src/instruction/utils/pumpfun.rs`)。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
| 方向 | 做法 |
|
||||
|------|------|
|
||||
| **优先事件** | 使用 gRPC / 解析器里的 **`tradeEvent.feeRecipient`** 或同笔 **create_v2 + buy** 观测到的 fee recipient。 |
|
||||
| **纠偏** | `PumpFunParams::from_trade` 会对 `mayhem_mode` 与 `fee_recipient` 做池一致性纠偏;发单前若事件缺省,可走 `pump_fun_fee_recipient_meta`(按 `is_mayhem_mode` 从静态池选)。 |
|
||||
| **提交前保留观测值** | 不要无条件把已观测的 `fee_recipient` 清成 default;否则会退回 SDK 内置静态池,静态池若落后于主网 Global 授权,仍可能触发 6000。只有缺少观测值时才让 builder 兜底。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. SPL:Token **`initializeAccount3`**——`incorrect program id for instruction`
|
||||
|
||||
### 现象
|
||||
|
||||
- 内联指令里 **`Token Program: initializeAccount3`**(或 Token-2022 等价指令)报错 **`incorrect program id for instruction`**。
|
||||
|
||||
### 含义
|
||||
|
||||
为 **Mint** 创建用户 ATA 时,使用的 **token program(Legacy SPL vs Token-2022)** 与 **Mint 的实际 owner(`mint.owner`)** 不一致。
|
||||
|
||||
### 常见成因
|
||||
|
||||
1. Pump 新发多为 **Token-2022**,但代码写死 **`Tokenkeg…`**。
|
||||
2. 少数 Legacy mint(`Tokenkeg…`),却被强制按 Token-2022 建账。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::token_program`** 必须与非 default 的 **mint owner** 一致;从 **gRPC / 解析结果** 带入,**勿**在已明确 program 时再用「仅按 `.pump` 后缀猜 Token-2022」覆盖(业务层若做后缀启发,应仅在 `token_program == default` 时生效)。
|
||||
|
||||
---
|
||||
|
||||
## 4. Pump `6020` `BuyZeroAmount`
|
||||
|
||||
### 现象
|
||||
|
||||
- `buy` / `buy_exact_sol_in` 报 **Buy zero amount**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- `min_tokens_out == 0`(或等价路径算出可买 **0 枚**),协议直接拒绝。
|
||||
- 使用 **Create / Shred** 事件构造曲线时 **virtual / real 储备全 0**,本地定价算出 **0**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 对「首买 / 无储备」场景用 **`PumpFunParams::from_dev_trade`** 或按协议初值回填虚拟储备(与 `global_constants` 一致),再算 **`min_tokens_out`**。
|
||||
- 适当 **放宽买入滑点**(`slippage_basis_points`),避免估算代币量略小于链上。
|
||||
|
||||
---
|
||||
|
||||
## 5. Pump `6042` `BuySlippageBelowMinTokensOut`
|
||||
|
||||
### 现象
|
||||
|
||||
- 文案:**Slippage: Would buy less tokens than expected min_tokens_out**。
|
||||
|
||||
### 含义
|
||||
|
||||
链上实际可成交代币数量 **小于** 指令参数 **`min_tokens_out`**。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 市价波动、SOL 竞价导致曲线状态与本地快照不一致。
|
||||
- 本地 **`get_buy_token_amount_from_sol_amount`** 所用 **creator / 费率假设**与链上 **pfee CPI**(动态费率)不一致,**预估偏多**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **提高滑点容忍**(或降低 **`min_tokens_out`**)。
|
||||
- 尽量用 **较新**的 **virtual/real reserves**(来自最近一次 trade 解析或简短 RPC)。
|
||||
- 若运行在 **狙击手**等极端延迟场景,需接受:**保守的 min_out**(更大滑点)换成功率。
|
||||
|
||||
---
|
||||
|
||||
## 6. Pump `6024` `Overflow` 与其它算术类错误
|
||||
|
||||
### 现象
|
||||
|
||||
- `6024 Overflow`、`6025 Truncation`、`6026 DivisionByZero`(见 IDL)。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- 指令参数 **`amount` / SOL / lamports** 与曲线状态组合不合法(例如极端大卖、或为 0 与后续计算冲突)。
|
||||
- SDK 外传入了 **不合理的储备快照**。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- 校验 **买入/卖出金额 > 0**、与余额一致。
|
||||
- 使用 **`from_mint_by_rpc`** 或与链一致的储备后再算 **`min_sol_output` / `min_tokens_out`**。
|
||||
|
||||
---
|
||||
|
||||
## 7. Pump `6027` `NotEnoughRemainingAccounts`(返现等)
|
||||
|
||||
### 现象
|
||||
|
||||
- 返现代币等路径要求 **remaining accounts**(例如 `UserVolumeAccumulator`),数量不足。
|
||||
|
||||
### 常见成因
|
||||
|
||||
- **`is_cashback_coin`(或等价标志)为 true**,但组装指令时 **未追加**所需账户。
|
||||
|
||||
### SDK 侧处理思路
|
||||
|
||||
- **`PumpFunParams::from_trade` / `from_dev_trade`** 传入正确的 **`is_cashback_coin`**(来自事件)。
|
||||
- README 中与 **Cashback** 章节一致:**事件路径必须带标志**,不能默认 false。
|
||||
|
||||
---
|
||||
|
||||
## 8. Pump `6022` `SellZeroAmount`
|
||||
|
||||
### 含义
|
||||
|
||||
卖出代币数量为 **0**。在业务层过滤即可。
|
||||
|
||||
---
|
||||
|
||||
## 9. 与 pump-fees / Creator 迁移相关的错误(`6049`–`6053` 等)
|
||||
|
||||
IDL 中例如:
|
||||
|
||||
- **`6049` `CreatorMigratedToSharingConfig`**
|
||||
- **`6050` `UnableToDistributeCreatorVaultMigratedToSharingConfig`**
|
||||
- **`6053` `BondingCurveAndSharingConfigCreatorMismatch`**
|
||||
|
||||
### 思路
|
||||
|
||||
这些是 **creator / fee-sharing** 生命周期中的**专用分支**,与一般买卖路径不同。若仿真或清算类指令触发:
|
||||
|
||||
- 以 **Pump / pump-fees 官方文档** 为准使用 **`distribute_creator_fees`**、**reset_fee_sharing_config** 等;
|
||||
- **`creator_vault` resolve** 需结合 **`fetch_fee_sharing_creator_vault_if_active`** 与链上 **`SharingConfig` 状态**,避免离线 deduce 过时。
|
||||
|
||||
---
|
||||
|
||||
## 10. 调试清单(推荐给集成方)
|
||||
|
||||
1. **记下失败指令索引** + **Simulation / explorer 展开的账户列表**,重点核对:**mint、bonding_curve、associated_bonding_curve、creator_vault、fee_recipient、token_program**。
|
||||
2. **对比 Anchor 日志里的 Left / Right**(针对 2006)与本地 `PumpFunParams` 打印是否一致。
|
||||
3. **`mint.owner`** 与 **`PumpFunParams.token_program`** 是否一致。
|
||||
4. **`bonding_curve` 地址**是否与 **`get_bonding_curve_pda(mint)`** 一致。
|
||||
5. **`mayhem_mode` ↔ `fee_recipient`** 是否同池。
|
||||
6. 低延迟不足以覆盖 **creator 演进** 时,是否在卖前引入了 **RPC 或最新 trade** 刷新。
|
||||
|
||||
---
|
||||
|
||||
## 参考代码入口(本仓库)
|
||||
|
||||
| 主题 | 路径 |
|
||||
|------|------|
|
||||
| Buy / Sell vault resolve(显式 ix `creator_vault` → `fee_sharing` hint → `PDA(effective_creator)`) | `src/instruction/utils/pumpfun.rs` — `resolve_creator_vault_for_ix_with_fee_sharing`;`effective_creator_for_trade` 见 `src/trading/core/params/pumpfun.rs` |
|
||||
| Fee recipient / Mayhem | `src/instruction/utils/pumpfun.rs` — `pump_fun_fee_recipient_meta`, `reconcile_mayhem_mode_for_trade` |
|
||||
| 指令构建 | `src/instruction/pumpfun.rs` — `PumpFunInstructionBuilder` |
|
||||
| Params | `src/trading/core/params/pumpfun.rs` — `PumpFunParams::{from_trade, from_dev_trade, from_mint_by_rpc, refresh_fee_sharing_creator_vault_from_rpc}` |
|
||||
|
||||
---
|
||||
|
||||
如需英文版或对 PumpSwap/其它 DEX 的同类文档,可在 `docs/` 下按相同结构扩展。
|
||||
@@ -109,7 +109,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -157,6 +164,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
trade_info.is_cashback_coin,
|
||||
Some(trade_info.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -168,6 +176,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -113,7 +113,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -172,6 +179,7 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()>
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -81,7 +81,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -140,6 +147,7 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult<
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -636,6 +636,7 @@ async fn handle_buy_pumpfun(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -692,6 +693,7 @@ async fn handle_buy_pumpswap(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -748,6 +750,7 @@ async fn handle_buy_bonk(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -808,6 +811,7 @@ async fn handle_buy_raydium_v4(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -869,6 +873,7 @@ async fn handle_buy_raydium_cpmm(
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
match client.buy(buy_params).await {
|
||||
@@ -1392,7 +1397,14 @@ async fn initialize_real_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = RPC_URL.to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(payer, trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -51,6 +51,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -107,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -63,7 +63,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -105,6 +112,7 @@ async fn test_middleware() -> AnyResult<()> {
|
||||
gas_fee_strategy: gas_fee_strategy,
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -108,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -153,6 +160,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
trade_info.fee_recipient,
|
||||
trade_info.token_program,
|
||||
trade_info.is_cashback_coin,
|
||||
Some(trade_info.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -164,6 +172,7 @@ async fn pumpfun_copy_trade_with_grpc(
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -114,7 +114,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||
}
|
||||
|
||||
@@ -150,6 +157,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -161,6 +169,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -199,6 +208,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -104,7 +104,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
|
||||
}
|
||||
|
||||
@@ -140,6 +147,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
@@ -151,6 +159,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -183,6 +192,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
|
||||
e.fee_recipient,
|
||||
e.token_program,
|
||||
e.is_cashback_coin,
|
||||
Some(e.mayhem_mode),
|
||||
)),
|
||||
address_lookup_table_account: None,
|
||||
wait_transaction_confirmed: true,
|
||||
|
||||
@@ -50,6 +50,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -104,7 +105,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -133,7 +133,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -155,7 +162,9 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
||||
trade_info.base_token_program,
|
||||
trade_info.quote_token_program,
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -184,7 +193,9 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
||||
trade_info.base_token_program,
|
||||
trade_info.quote_token_program,
|
||||
trade_info.protocol_fee_recipient,
|
||||
pool_data.coin_creator,
|
||||
pool_data.is_cashback_coin,
|
||||
trade_info.cashback_fee_basis_points,
|
||||
);
|
||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||
@@ -233,6 +244,7 @@ async fn pumpswap_trade_with_grpc(
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -109,7 +109,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -174,6 +181,7 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent)
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -107,7 +107,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
@@ -162,6 +169,7 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) ->
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
|
||||
@@ -50,6 +50,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
gas_fee_strategy: gas_fee_strategy.clone(),
|
||||
simulate: false,
|
||||
use_exact_sol_amount: None,
|
||||
use_pumpfun_v2: false,
|
||||
grpc_recv_us: None,
|
||||
};
|
||||
client.buy(buy_params).await?;
|
||||
@@ -107,7 +108,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
use sol_trade_sdk::{
|
||||
common::{AnyResult, InfrastructureConfig, TradeConfig},
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
SwqosTransport, TradingClient, TradingInfrastructure,
|
||||
AstralaneTransport, TradingClient, TradingInfrastructure,
|
||||
};
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::signature::Keypair;
|
||||
@@ -52,17 +52,16 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
"your_api_token".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Quic),
|
||||
), // QUIC; use None for HTTP
|
||||
Some(AstralaneTransport::Quic),
|
||||
), // QUIC;None / Some(Binary) / Some(Plain) 为 HTTP
|
||||
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
|
||||
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
|
||||
];
|
||||
|
||||
// Optional: Customize WSOL ATA and Seed optimization settings
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment).with_wsol_ata_config(
|
||||
true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup
|
||||
true, // use_seed_optimize: Enable seed optimization for all ATA operations
|
||||
);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
.create_wsol_ata_on_startup(true)
|
||||
.use_seed_optimize(true)
|
||||
.build();
|
||||
|
||||
// Creates new infrastructure internally
|
||||
let client = TradingClient::new(Arc::new(payer), trade_config).await;
|
||||
|
||||
@@ -84,7 +84,14 @@ async fn create_solana_trade_client() -> Result<SolanaTrade, Box<dyn std::error:
|
||||
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
|
||||
let commitment = CommitmentConfig::confirmed();
|
||||
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
|
||||
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .create_wsol_ata_on_startup(true) // default: true
|
||||
// .use_seed_optimize(true) // default: true
|
||||
// .log_enabled(true) // default: true
|
||||
// .check_min_tip(false) // default: false
|
||||
// .swqos_cores_from_end(false) // default: false
|
||||
// .mev_protection(false) // default: false
|
||||
.build();
|
||||
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
|
||||
println!("✅ SolanaTrade client initialized successfully!");
|
||||
Ok(solana_trade)
|
||||
|
||||
+2323
-2896
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -4835,7 +4835,7 @@
|
||||
},
|
||||
{
|
||||
"code": 6052,
|
||||
"name": "TokensInVaultLessThanCashbackEarned"
|
||||
"name": "CashbackEarnedDoesNotMatchTokenInVault"
|
||||
}
|
||||
],
|
||||
"types": [
|
||||
|
||||
+1463
-242
File diff suppressed because it is too large
Load Diff
+1240
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,37 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use anyhow::Result;
|
||||
use solana_address_lookup_table_interface::state::AddressLookupTable;
|
||||
use solana_sdk::{message::AddressLookupTableAccount, pubkey::Pubkey};
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
pub async fn fetch_address_lookup_table_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
lookup_table_address: &Pubkey,
|
||||
) -> Result<AddressLookupTableAccount, anyhow::Error> {
|
||||
let account = rpc.get_account(lookup_table_address).await?;
|
||||
let lookup_table = AddressLookupTable::deserialize(&account.data)?;
|
||||
let address_lookup_table_account = AddressLookupTableAccount {
|
||||
key: *lookup_table_address,
|
||||
addresses: lookup_table.addresses.to_vec(),
|
||||
};
|
||||
|
||||
// Parse address lookup table manually
|
||||
// Layout: 4 bytes (type) + 4 bytes (deactivation_slot) + 4 bytes (last_extended_slot) + 1 byte (last_extended_slot_start_index) + 1 byte (authority) + padding
|
||||
// Then addresses start at offset 56, each address is 32 bytes
|
||||
// First 4 bytes indicate if initialized (should be 1 or 2)
|
||||
|
||||
if account.data.len() < 56 {
|
||||
return Err(anyhow::anyhow!("Address lookup table account data too short"));
|
||||
}
|
||||
|
||||
// Read number of addresses (stored at offset 20 as u32, but we need to scan the bitmap)
|
||||
// Actually simpler: addresses start at offset 56, count from bitmap at offset 8-20
|
||||
let mut addresses = Vec::new();
|
||||
let mut offset = 56;
|
||||
while offset + 32 <= account.data.len() {
|
||||
let addr_bytes: [u8; 32] = account.data[offset..offset + 32].try_into()?;
|
||||
// Skip zero addresses (unused slots)
|
||||
if addr_bytes != [0u8; 32] {
|
||||
addresses.push(Pubkey::from(addr_bytes));
|
||||
}
|
||||
offset += 32;
|
||||
}
|
||||
|
||||
let address_lookup_table_account =
|
||||
AddressLookupTableAccount { key: *lookup_table_address, addresses };
|
||||
Ok(address_lookup_table_account)
|
||||
}
|
||||
|
||||
@@ -154,6 +154,8 @@ pub enum PdaCacheKey {
|
||||
PumpFunUserVolume(Pubkey),
|
||||
PumpFunBondingCurve(Pubkey),
|
||||
PumpFunBondingCurveV2(Pubkey),
|
||||
/// Pump-fees program `sharing-config` PDA for a mint (`feeSharingConfigPda`).
|
||||
PumpFunFeeSharingConfig(Pubkey),
|
||||
PumpFunCreatorVault(Pubkey),
|
||||
BonkPool(Pubkey, Pubkey),
|
||||
BonkVault(Pubkey, Pubkey),
|
||||
|
||||
+14
-14
@@ -1,8 +1,5 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_hash::Hash;
|
||||
use solana_nonce::state::State;
|
||||
use solana_nonce::versions::Versions;
|
||||
use solana_sdk::account_utils::StateMut;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use tracing::error;
|
||||
|
||||
@@ -21,18 +18,21 @@ pub async fn fetch_nonce_info(
|
||||
nonce_account: Pubkey,
|
||||
) -> Option<DurableNonceInfo> {
|
||||
match rpc.get_account(&nonce_account).await {
|
||||
Ok(account) => match account.state() {
|
||||
Ok(Versions::Current(state)) => {
|
||||
if let State::Initialized(data) = *state {
|
||||
let blockhash = data.durable_nonce.as_hash();
|
||||
return Some(DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(*blockhash),
|
||||
});
|
||||
}
|
||||
Ok(account) => {
|
||||
// Parse nonce account manually: first 4 bytes is version, then 4 bytes authority type
|
||||
// For initialized nonce: version=0, authority_type=0, then authority (32 bytes), then blockhash (32 bytes), then fee_calculator
|
||||
if account.data.len() >= 80 {
|
||||
// Skip version (4) + authority_type (4) + authority (32) = 40 bytes
|
||||
// Then blockhash is at offset 40
|
||||
let blockhash_bytes: [u8; 32] = account.data[40..72].try_into().ok()?;
|
||||
return Some(DurableNonceInfo {
|
||||
nonce_account: Some(nonce_account),
|
||||
current_nonce: Some(Hash::from(blockhash_bytes)),
|
||||
});
|
||||
} else {
|
||||
error!("Nonce account data too short");
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to get nonce account information: {:?}", e);
|
||||
}
|
||||
|
||||
@@ -34,10 +34,7 @@ fn extract_swqos_error_message(s: &str) -> String {
|
||||
// Try parse as JSON only when input looks like JSON (avoid parsing long non-JSON strings)
|
||||
if s.starts_with('{') {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(s) {
|
||||
let obj = v
|
||||
.get("error")
|
||||
.and_then(|e| e.as_object())
|
||||
.or_else(|| v.as_object());
|
||||
let obj = v.get("error").and_then(|e| e.as_object()).or_else(|| v.as_object());
|
||||
if let Some(o) = obj {
|
||||
if let Some(m) = o.get("message").and_then(|x| x.as_str()) {
|
||||
return m.to_string();
|
||||
@@ -69,11 +66,7 @@ pub fn set_sdk_log_enabled(enabled: bool) {
|
||||
|
||||
/// Aligned log: ` [Soyas ] Buy submitted: 13.936 µs`. Call only when sdk_log_enabled().
|
||||
#[inline]
|
||||
pub fn log_swqos_submitted(
|
||||
provider: &str,
|
||||
trade_type: impl fmt::Display,
|
||||
elapsed: Duration,
|
||||
) {
|
||||
pub fn log_swqos_submitted(provider: &str, trade_type: impl fmt::Display, elapsed: Duration) {
|
||||
println!(
|
||||
" [{:width$}] {} submitted: {}",
|
||||
provider,
|
||||
|
||||
+12
-4
@@ -3,7 +3,7 @@ use anyhow::anyhow;
|
||||
use fnv::FnvHasher;
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction::create_account_with_seed;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::hash::Hasher;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -92,9 +92,17 @@ pub fn create_associated_token_account_use_seed(
|
||||
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
|
||||
|
||||
let len = 165;
|
||||
// 但账户的 owner 仍然使用正确的 token_program(Token 或 Token-2022)
|
||||
let create_acc =
|
||||
create_account_with_seed(payer, &ata_like, owner, seed, rent, len, token_program);
|
||||
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致)
|
||||
// 否则创建的账户地址与 ata_like 不匹配,导致 initializeAccount3 失败
|
||||
let create_acc = system_instruction::create_account_with_seed(
|
||||
payer,
|
||||
&ata_like,
|
||||
payer,
|
||||
seed,
|
||||
rent,
|
||||
len,
|
||||
token_program,
|
||||
);
|
||||
|
||||
let init_acc = if is_2022_token {
|
||||
crate::common::spl_token_2022::initialize_account3(&token_program, &ata_like, mint, owner)?
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use solana_program::pubkey;
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
@@ -18,14 +18,11 @@ pub fn close_account(
|
||||
let mut data = Vec::with_capacity(1);
|
||||
data.push(9);
|
||||
let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len());
|
||||
accounts.push(solana_sdk::message::AccountMeta::new(*account_pubkey, false));
|
||||
accounts.push(solana_sdk::message::AccountMeta::new(*destination_pubkey, false));
|
||||
accounts.push(solana_sdk::message::AccountMeta::new_readonly(
|
||||
*owner_pubkey,
|
||||
signer_pubkeys.is_empty(),
|
||||
));
|
||||
accounts.push(AccountMeta::new(*account_pubkey, false));
|
||||
accounts.push(AccountMeta::new(*destination_pubkey, false));
|
||||
accounts.push(AccountMeta::new_readonly(*owner_pubkey, signer_pubkeys.is_empty()));
|
||||
for signer_pubkey in signer_pubkeys.iter() {
|
||||
accounts.push(solana_sdk::message::AccountMeta::new_readonly(**signer_pubkey, true));
|
||||
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
|
||||
}
|
||||
Ok(Instruction { program_id: *token_program_id, accounts, data })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use solana_program::pubkey;
|
||||
use solana_sdk::{
|
||||
message::{AccountMeta, Instruction},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
|
||||
+129
-22
@@ -11,6 +11,10 @@ pub struct InfrastructureConfig {
|
||||
pub commitment: CommitmentConfig,
|
||||
/// When true, SWQOS sender threads use the *last* N cores instead of the first N. Reduces contention with main thread / default tokio workers that often use low-numbered cores. Default false.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC `:9000` or HTTP `mev-protect=true`, BlockRazor) use MEV-protected
|
||||
/// endpoints/modes. Default false.
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
|
||||
impl InfrastructureConfig {
|
||||
@@ -24,6 +28,7 @@ impl InfrastructureConfig {
|
||||
swqos_configs,
|
||||
commitment,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +39,7 @@ impl InfrastructureConfig {
|
||||
swqos_configs: config.swqos_configs.clone(),
|
||||
commitment: config.commitment.clone(),
|
||||
swqos_cores_from_end: config.swqos_cores_from_end,
|
||||
mev_protection: config.mev_protection,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +59,7 @@ impl Hash for InfrastructureConfig {
|
||||
self.swqos_configs.hash(state);
|
||||
format!("{:?}", self.commitment).hash(state);
|
||||
self.swqos_cores_from_end.hash(state);
|
||||
self.mev_protection.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +69,7 @@ impl PartialEq for InfrastructureConfig {
|
||||
&& self.swqos_configs == other.swqos_configs
|
||||
&& self.commitment == other.commitment
|
||||
&& self.swqos_cores_from_end == other.swqos_cores_from_end
|
||||
&& self.mev_protection == other.mev_protection
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,52 +91,151 @@ pub struct TradeConfig {
|
||||
pub check_min_tip: bool,
|
||||
/// When true, SWQOS uses the *last* N cores (instead of the first N). Use when main thread / tokio use low-numbered cores to reduce CPU contention. Default false.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
|
||||
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||
pub mev_protection: bool,
|
||||
/// Use PumpFun V2 instructions (buy_v2 / sell_v2, 27/26-account metas, quote_mint support).
|
||||
/// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface.
|
||||
pub use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl TradeConfig {
|
||||
/// Create a new TradeConfig using the builder pattern.
|
||||
///
|
||||
/// # Available builder methods
|
||||
/// - `.create_wsol_ata_on_startup(bool)` — check & create WSOL ATA on init (default: true)
|
||||
/// - `.use_seed_optimize(bool)` — seed optimization for ATA ops (default: true)
|
||||
/// - `.log_enabled(bool)` — SDK timing/SWQOS logs (default: true)
|
||||
/// - `.check_min_tip(bool)` — filter SWQOS below min tip (default: false)
|
||||
/// - `.swqos_cores_from_end(bool)` — bind SWQOS to last N cores (default: false)
|
||||
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// let config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
/// .mev_protection(true)
|
||||
/// .check_min_tip(true)
|
||||
/// .log_enabled(false)
|
||||
/// .build();
|
||||
/// ```
|
||||
pub fn builder(
|
||||
rpc_url: String,
|
||||
swqos_configs: Vec<SwqosConfig>,
|
||||
commitment: CommitmentConfig,
|
||||
) -> TradeConfigBuilder {
|
||||
TradeConfigBuilder::new(rpc_url, swqos_configs, commitment)
|
||||
}
|
||||
|
||||
/// Shortcut: create a TradeConfig with all defaults. Equivalent to `builder(...).build()`.
|
||||
pub fn new(
|
||||
rpc_url: String,
|
||||
swqos_configs: Vec<SwqosConfig>,
|
||||
commitment: CommitmentConfig,
|
||||
) -> Self {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!("🔧 TradeConfig create_wsol_ata_on_startup default: true");
|
||||
println!("🔧 TradeConfig use_seed_optimize default: true");
|
||||
}
|
||||
Self::builder(rpc_url, swqos_configs, commitment).build()
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for [`TradeConfig`]. Created via [`TradeConfig::builder`].
|
||||
///
|
||||
/// All fields are optional and pre-filled with sensible defaults.
|
||||
/// Call `.build()` to produce the final [`TradeConfig`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TradeConfigBuilder {
|
||||
rpc_url: String,
|
||||
swqos_configs: Vec<SwqosConfig>,
|
||||
commitment: CommitmentConfig,
|
||||
create_wsol_ata_on_startup: bool,
|
||||
use_seed_optimize: bool,
|
||||
log_enabled: bool,
|
||||
check_min_tip: bool,
|
||||
swqos_cores_from_end: bool,
|
||||
mev_protection: bool,
|
||||
use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl TradeConfigBuilder {
|
||||
fn new(rpc_url: String, swqos_configs: Vec<SwqosConfig>, commitment: CommitmentConfig) -> Self {
|
||||
Self {
|
||||
rpc_url,
|
||||
swqos_configs,
|
||||
commitment,
|
||||
create_wsol_ata_on_startup: true, // default: check and create on startup
|
||||
use_seed_optimize: true, // default: use seed optimization
|
||||
log_enabled: true, // default: enable all SDK logs
|
||||
check_min_tip: false, // default: skip min tip check to reduce latency
|
||||
create_wsol_ata_on_startup: true,
|
||||
use_seed_optimize: true,
|
||||
log_enabled: true,
|
||||
check_min_tip: false,
|
||||
swqos_cores_from_end: false,
|
||||
mev_protection: false,
|
||||
use_pumpfun_v2: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a TradeConfig with custom WSOL ATA settings
|
||||
pub fn with_wsol_ata_config(
|
||||
mut self,
|
||||
create_wsol_ata_on_startup: bool,
|
||||
use_seed_optimize: bool,
|
||||
) -> Self {
|
||||
self.create_wsol_ata_on_startup = create_wsol_ata_on_startup;
|
||||
self.use_seed_optimize = use_seed_optimize;
|
||||
/// Check and create WSOL ATA on SDK initialization. Default: `true`.
|
||||
pub fn create_wsol_ata_on_startup(mut self, v: bool) -> Self {
|
||||
self.create_wsol_ata_on_startup = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to check minimum tip per SWQOS (filter out configs below min). Default false for lower latency.
|
||||
pub fn with_check_min_tip(mut self, check_min_tip: bool) -> Self {
|
||||
self.check_min_tip = check_min_tip;
|
||||
/// Enable seed optimization for all ATA operations. Default: `true`.
|
||||
pub fn use_seed_optimize(mut self, v: bool) -> Self {
|
||||
self.use_seed_optimize = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use the *last* N cores for SWQOS (instead of the first N). Call this when the main thread or tokio workers use low-numbered cores to avoid binding SWQOS to busy cores. Default false.
|
||||
pub fn with_swqos_cores_from_end(mut self, from_end: bool) -> Self {
|
||||
self.swqos_cores_from_end = from_end;
|
||||
/// Enable SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default: `true`.
|
||||
pub fn log_enabled(mut self, v: bool) -> Self {
|
||||
self.log_enabled = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter out SWQOS providers whose tip is below their minimum requirement.
|
||||
/// Adds a small check on the hot path; disable for lowest latency. Default: `false`.
|
||||
pub fn check_min_tip(mut self, v: bool) -> Self {
|
||||
self.check_min_tip = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Bind SWQOS sender threads to the *last* N CPU cores instead of the first N.
|
||||
/// Useful when main thread / tokio workers occupy low-numbered cores. Default: `false`.
|
||||
pub fn swqos_cores_from_end(mut self, v: bool) -> Self {
|
||||
self.swqos_cores_from_end = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable global MEV protection. When `true`:
|
||||
/// - **Astralane QUIC** uses port `9000`; **Astralane HTTP** adds `mev-protect=true`
|
||||
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
|
||||
///
|
||||
/// May reduce landing speed. Default: `false`.
|
||||
pub fn mev_protection(mut self, v: bool) -> Self {
|
||||
self.mev_protection = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use PumpFun V2 instructions (`buy_v2` / `sell_v2`, 27-account metas, `quote_mint` support).
|
||||
/// Default: `false` (V1 — 18-account metas, legacy SOL-paired, smaller transaction).
|
||||
/// Set to `true` when PumpFun officially deploys V2 on mainnet.
|
||||
pub fn use_pumpfun_v2(mut self, v: bool) -> Self {
|
||||
self.use_pumpfun_v2 = v;
|
||||
self
|
||||
}
|
||||
|
||||
/// Consume the builder and produce a [`TradeConfig`].
|
||||
pub fn build(self) -> TradeConfig {
|
||||
TradeConfig {
|
||||
rpc_url: self.rpc_url,
|
||||
swqos_configs: self.swqos_configs,
|
||||
commitment: self.commitment,
|
||||
create_wsol_ata_on_startup: self.create_wsol_ata_on_startup,
|
||||
use_seed_optimize: self.use_seed_optimize,
|
||||
log_enabled: self.log_enabled,
|
||||
check_min_tip: self.check_min_tip,
|
||||
swqos_cores_from_end: self.swqos_cores_from_end,
|
||||
mev_protection: self.mev_protection,
|
||||
use_pumpfun_v2: self.use_pumpfun_v2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
|
||||
|
||||
@@ -57,3 +57,9 @@ pub const RENT_META: solana_sdk::instruction::AccountMeta =
|
||||
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM_ID: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
+221
-82
@@ -152,6 +152,12 @@ pub const SOYAS_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"),
|
||||
pubkey!("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"),
|
||||
pubkey!("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"),
|
||||
pubkey!("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"),
|
||||
pubkey!("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"),
|
||||
pubkey!("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"),
|
||||
pubkey!("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"),
|
||||
pubkey!("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"),
|
||||
pubkey!("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"),
|
||||
];
|
||||
|
||||
pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
@@ -165,202 +171,267 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
||||
];
|
||||
|
||||
// NewYork,
|
||||
// Frankfurt,
|
||||
// Amsterdam,
|
||||
// SLC,
|
||||
// Tokyo,
|
||||
// London,
|
||||
// LosAngeles,
|
||||
// Default,
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
// **地理就近(用户语义)**:当某枚举区域没有该服务商**独立公布**的 PoP 时,在**该服务商已出现的端点集合内**,按真实地理位置选**大圆距离最近**的一项作为填充;行尾注释说明依据。
|
||||
// **例外**:`SwqosRegion::Default`(下标 9)不表示地球上的点,表中为全局 URL 或文档默认枢纽,**不适用**地理就近,仅表示「未指定区域时的回退」。
|
||||
// 若某区域仅有一种「大区」级入口(例如全美只有一个美东 PoP),则地理上非最优但只能复用,注释会标明「受服务商可用区限制」。
|
||||
|
||||
pub const SWQOS_ENDPOINTS_JITO: [&str; 8] = [
|
||||
/// Jito mainnet block engines (`https://<region>.mainnet.block-engine.jito.wtf`).
|
||||
/// There is no Los Angeles engine → use Salt Lake City for `LosAngeles`; `SwqosRegion::Default` uses the global mainnet URL.
|
||||
pub const SWQOS_ENDPOINTS_JITO: [&str; 10] = [
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
"https://frankfurt.mainnet.block-engine.jito.wtf",
|
||||
"https://amsterdam.mainnet.block-engine.jito.wtf",
|
||||
"https://dublin.mainnet.block-engine.jito.wtf",
|
||||
"https://slc.mainnet.block-engine.jito.wtf",
|
||||
"https://tokyo.mainnet.block-engine.jito.wtf",
|
||||
"https://singapore.mainnet.block-engine.jito.wtf",
|
||||
"https://london.mainnet.block-engine.jito.wtf",
|
||||
"https://ny.mainnet.block-engine.jito.wtf",
|
||||
"https://slc.mainnet.block-engine.jito.wtf", // LosAngeles: no LA PoP; nearest US-West is SLC
|
||||
"https://mainnet.block-engine.jito.wtf",
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 8] = [
|
||||
/// NextBlock regional HTTP hosts (see provider docs). `SwqosRegion` order; no dedicated LA PoP → SLC as US-West fallback.
|
||||
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 10] = [
|
||||
"http://ny.nextblock.io",
|
||||
"http://frankfurt.nextblock.io",
|
||||
"http://amsterdam.nextblock.io",
|
||||
"http://fra.nextblock.io",
|
||||
"http://ams.nextblock.io",
|
||||
"http://dublin.nextblock.io",
|
||||
"http://slc.nextblock.io",
|
||||
"http://tokyo.nextblock.io",
|
||||
"http://sgp.nextblock.io",
|
||||
"http://london.nextblock.io",
|
||||
"http://singapore.nextblock.io",
|
||||
"http://frankfurt.nextblock.io",
|
||||
"http://slc.nextblock.io",
|
||||
"http://fra.nextblock.io", // Default: 非地理区域;服务商无「全局」主机名时用 EU 枢纽作未选区回退
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 10] = [
|
||||
"http://ny.0slot.trade",
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://ams.0slot.trade",
|
||||
"http://ny.0slot.trade",
|
||||
"http://ams.0slot.trade", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams(相对 de2 等)
|
||||
"http://la.0slot.trade", // SLC: no UT PoP; nearest US-West published host
|
||||
"http://jp.0slot.trade",
|
||||
"http://ams.0slot.trade",
|
||||
"http://jp.0slot.trade", // SG: 无本地 PoP;已公布 APAC 仅 jp,为表中离新加坡最近的大圆距离
|
||||
"http://ams.0slot.trade", // London: 无 UK 专用;已公布 EU 点中 ams 距伦敦最近之一
|
||||
"http://la.0slot.trade",
|
||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||
"http://de2.0slot.trade", // Default: 非地理区域;EU 枢纽 de2 作未选区回退
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 8] = [
|
||||
"http://ewr1.nozomi.temporal.xyz",
|
||||
/// Nozomi Direct regions: ewr1, fra2, ams1, lon1, lax1, tyo1, sgp1, …
|
||||
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 10] = [
|
||||
"http://ewr1.nozomi.temporal.xyz", // NewYork → Newark
|
||||
"http://fra2.nozomi.temporal.xyz",
|
||||
"http://ams1.nozomi.temporal.xyz",
|
||||
"http://ewr1.nozomi.temporal.xyz",
|
||||
"http://lon1.nozomi.temporal.xyz", // Dublin: no IE host; UK nearest Direct PoP
|
||||
"http://lax1.nozomi.temporal.xyz", // SLC: US-West
|
||||
"http://tyo1.nozomi.temporal.xyz",
|
||||
"http://sgp1.nozomi.temporal.xyz",
|
||||
"http://pit1.nozomi.temporal.xyz",
|
||||
"http://fra2.nozomi.temporal.xyz",
|
||||
"http://lon1.nozomi.temporal.xyz",
|
||||
"http://lax1.nozomi.temporal.xyz",
|
||||
"http://fra2.nozomi.temporal.xyz", // Default: 非地理区域;EU Direct 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_BLOX: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOX: [&str; 10] = [
|
||||
"https://ny.solana.dex.blxrbdn.com",
|
||||
"https://germany.solana.dex.blxrbdn.com",
|
||||
"https://amsterdam.solana.dex.blxrbdn.com",
|
||||
"https://ny.solana.dex.blxrbdn.com",
|
||||
"https://uk.solana.dex.blxrbdn.com", // Dublin: IE/UK edge
|
||||
"https://la.solana.dex.blxrbdn.com", // SLC: no Mountain PoP; US-West LA
|
||||
"https://tokyo.solana.dex.blxrbdn.com",
|
||||
"https://tokyo.solana.dex.blxrbdn.com", // SG: 文档无 SGP 区域;已公布 APAC 仅 Tokyo,为距 SG 最近选项
|
||||
"https://uk.solana.dex.blxrbdn.com",
|
||||
"https://la.solana.dex.blxrbdn.com",
|
||||
"https://global.solana.dex.blxrbdn.com",
|
||||
"https://global.solana.dex.blxrbdn.com", // Default: 非地理区域;全球任播
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_NODE1: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_NODE1: [&str; 10] = [
|
||||
"http://ny.node1.me",
|
||||
"http://fra.node1.me",
|
||||
"http://ams.node1.me",
|
||||
"http://ny.node1.me",
|
||||
"http://lon.node1.me", // Dublin: 已公布中英爱区域用 lon(地理上近爱尔兰)
|
||||
"http://ny.node1.me", // SLC: 已公布美国仅 ny;美西无 PoP,受可用区限制复用美东
|
||||
"http://tk.node1.me",
|
||||
"http://tk.node1.me", // SG: 已公布 APAC 仅 tk;地理上为表中离 SG 最近
|
||||
"http://lon.node1.me",
|
||||
"http://ny.node1.me",
|
||||
"http://fra.node1.me",
|
||||
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
|
||||
"http://fra.node1.me", // Default: 非地理区域;与 QUIC 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
/// Node1 QUIC: port 16666. Region order: NewYork, Frankfurt, Amsterdam, SLC→ny, Tokyo, London, LosAngeles→ny, Default→ny.
|
||||
/// Node1 QUIC: port 16666. Region order matches [`SwqosRegion`].
|
||||
/// server_name = host part (e.g. ny.node1.me). Auth: first bi stream = 16-byte UUID; each tx = new bi stream, bincode body.
|
||||
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 10] = [
|
||||
"ny.node1.me:16666",
|
||||
"fra.node1.me:16666",
|
||||
"ams.node1.me:16666",
|
||||
"ny.node1.me:16666", // SLC → ny
|
||||
"lon.node1.me:16666",
|
||||
"ny.node1.me:16666",
|
||||
"tk.node1.me:16666",
|
||||
"tk.node1.me:16666",
|
||||
"lon.node1.me:16666",
|
||||
"ny.node1.me:16666", // LA → ny
|
||||
"ny.node1.me:16666", // Default → ny
|
||||
"ny.node1.me:16666",
|
||||
"fra.node1.me:16666", // Default: 非地理区域;与 HTTP 对齐为 EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
|
||||
/// Published: ny, slc, ams, fra, singapore, london, tokyo (no IE/UK split → london for Dublin).
|
||||
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 10] = [
|
||||
"http://ny.flashblock.trade",
|
||||
"http://fra.flashblock.trade",
|
||||
"http://ams.flashblock.trade",
|
||||
"http://london.flashblock.trade", // Dublin: no IE host; UK nearest
|
||||
"http://slc.flashblock.trade",
|
||||
"http://tokyo.flashblock.trade",
|
||||
"http://singapore.flashblock.trade",
|
||||
"http://london.flashblock.trade",
|
||||
"http://ny.flashblock.trade",
|
||||
"http://ny.flashblock.trade",
|
||||
"http://slc.flashblock.trade",
|
||||
"http://fra.flashblock.trade", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
|
||||
/// 若 HTTP 返回 500,可尝试 HTTPS:https://<region>.solana.blockrazor.io/v2/sendTransaction(Frankfurt/NewYork/Tokyo),通过 custom_url 覆盖。
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 10] = [
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction", // Dublin: UK nearest published
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // SLC: 文档无美西;美国仅 NY,受可用区限制
|
||||
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction", // SG: 已公布 APAC 仅 Tokyo,为距 SG 最近
|
||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // LosAngeles: 无美西入口;美国仅 NY
|
||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// BlockRazor gRPC endpoints. Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
|
||||
/// BlockRazor gRPC endpoints. Region order matches [`SwqosRegion`].
|
||||
/// Port 80 for gRPC protocol. Auth: apikey metadata in gRPC headers.
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 10] = [
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
||||
"http://amsterdam.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80", // SLC: 与 HTTP 一致;美国仅 NY
|
||||
"http://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||
"http://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
||||
"http://newyork.solana-grpc.blockrazor.xyz:80", // LosAngeles: 与 HTTP 一致
|
||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80", // Default: 非地理区域
|
||||
];
|
||||
|
||||
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
|
||||
/// Plain HTTP API path (`/iris?api-key=…&method=…`).
|
||||
pub const ASTRALANE_PATH_IRIS: &str = "iris";
|
||||
/// Binary HTTP API path (`/irisb?api-key=…&method=sendTransaction`, raw bincode body).
|
||||
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
|
||||
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
|
||||
/// Astralane **Plain** HTTP gateways (`/iris`). Pair with [`ASTRALANE_PATH_IRIS`].
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_PLAIN: [&str; 10] = [
|
||||
"http://ny.gateway.astralane.io/iris",
|
||||
"http://fr.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams
|
||||
"http://la.gateway.astralane.io/iris",
|
||||
"http://jp.gateway.astralane.io/iris",
|
||||
"http://sg.gateway.astralane.io/iris",
|
||||
"http://ams.gateway.astralane.io/iris", // London: 无 UK 专用;在已公布 EU 点中选距英国最近的 ams
|
||||
"http://la.gateway.astralane.io/iris",
|
||||
"https://edge.astralane.io/iris", // Default: 非地理区域;全局任播边缘
|
||||
];
|
||||
|
||||
/// Astralane **Binary** HTTP gateways (`/irisb`). Pair with [`ASTRALANE_PATH_IRISB`].
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_BINARY: [&str; 10] = [
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://fr.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb",
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb", // Dublin: 同 Plain
|
||||
"http://la.gateway.astralane.io/irisb",
|
||||
"http://jp.gateway.astralane.io/irisb",
|
||||
"http://ny.gateway.astralane.io/irisb",
|
||||
"http://lax.gateway.astralane.io/irisb",
|
||||
"http://lim.gateway.astralane.io/irisb",
|
||||
"http://sg.gateway.astralane.io/irisb",
|
||||
"http://ams.gateway.astralane.io/irisb", // London: 同 Plain
|
||||
"http://la.gateway.astralane.io/irisb",
|
||||
"https://edge.astralane.io/irisb", // Default: 同 Plain
|
||||
];
|
||||
|
||||
/// Astralane QUIC endpoints (port 7000). Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
|
||||
/// See: https://github.com/Astralane/astralane-quic-client. We use fr, ams, la, ny, lim, sg only (avoid ams2/fr2 for lower latency).
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
|
||||
"ny.gateway.astralane.io:7000", // NewYork
|
||||
"fr.gateway.astralane.io:7000", // Frankfurt
|
||||
"ams.gateway.astralane.io:7000", // Amsterdam
|
||||
"lim.gateway.astralane.io:7000", // SLC (no slc, use lim)
|
||||
"sg.gateway.astralane.io:7000", // Tokyo (Asia)
|
||||
"ams.gateway.astralane.io:7000", // London (Europe, avoid ams2)
|
||||
"la.gateway.astralane.io:7000", // LosAngeles
|
||||
"lim.gateway.astralane.io:7000", // Default
|
||||
/// Astralane QUIC endpoints (port 7000). Region order matches [`SwqosRegion`].
|
||||
/// See: https://github.com/Astralane/astralane-quic-client.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 10] = [
|
||||
"ny.gateway.astralane.io:7000",
|
||||
"fr.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // Dublin: 同 HTTP
|
||||
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
|
||||
"jp.gateway.astralane.io:7000",
|
||||
"sg.gateway.astralane.io:7000",
|
||||
"ams.gateway.astralane.io:7000", // London: 同 HTTP
|
||||
"la.gateway.astralane.io:7000",
|
||||
"lim.gateway.astralane.io:7000", // Default: 非地理区域;全局边缘
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
||||
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
|
||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 10] = [
|
||||
"ny.gateway.astralane.io:9000",
|
||||
"fr.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"la.gateway.astralane.io:9000",
|
||||
"jp.gateway.astralane.io:9000",
|
||||
"sg.gateway.astralane.io:9000",
|
||||
"ams.gateway.astralane.io:9000",
|
||||
"la.gateway.astralane.io:9000",
|
||||
"lim.gateway.astralane.io:9000",
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 10] = [
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
"http://ams1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://lhr1.flashrpc.com", // Dublin: 已公布 UK 用 lhr;地理上近爱尔兰
|
||||
"http://ewr1.flashrpc.com", // SLC: 已公布美国仅 ewr;无美西 PoP,受可用区限制
|
||||
"http://tyo1.flashrpc.com",
|
||||
"http://tyo1.flashrpc.com", // SG: 表中无 SGP;APAC 仅 tyo,为距 SG 最近
|
||||
"http://lhr1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com",
|
||||
"http://fra1.flashrpc.com",
|
||||
"http://ewr1.flashrpc.com", // LosAngeles: 同上,美国仅 ewr
|
||||
"http://fra1.flashrpc.com", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 10] = [
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"fra.landing.soyas.xyz:9000",
|
||||
"ams.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"lon.landing.soyas.xyz:9000", // Dublin: 已公布用 lon;地理近爱尔兰
|
||||
"nyc.landing.soyas.xyz:9000", // SLC: 已公布美国仅 nyc;无美西
|
||||
"tyo.landing.soyas.xyz:9000",
|
||||
"tyo.landing.soyas.xyz:9000", // SG: 表中 APAC 仅 tyo
|
||||
"lon.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000",
|
||||
"fra.landing.soyas.xyz:9000",
|
||||
"nyc.landing.soyas.xyz:9000", // LosAngeles: 同上
|
||||
"fra.landing.soyas.xyz:9000", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 10] = [
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778",
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778", // Dublin: 已公布 EU 点中 ams 地理近爱尔兰
|
||||
"nyc.speedlanding.trade:17778", // SLC: 表中美国仅 nyc;无美西 PoP,受可用区限制
|
||||
"tyo.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"nyc.speedlanding.trade:17778",
|
||||
"fra.speedlanding.trade:17778",
|
||||
"sgp.speedlanding.trade:17778",
|
||||
"ams.speedlanding.trade:17778", // London: 已公布 EU 中 ams 距英国最近之一
|
||||
"nyc.speedlanding.trade:17778", // LosAngeles: 同上,美国仅 nyc
|
||||
"fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽
|
||||
];
|
||||
|
||||
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
|
||||
/// Region order: NewYork(EWR), Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles(SG), Default(Global).
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"http://ewr-sender.helius-rpc.com/fast",
|
||||
"http://fra-sender.helius-rpc.com/fast",
|
||||
"http://ams-sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast", // Dublin: IE → UK/EU routing
|
||||
"http://slc-sender.helius-rpc.com/fast",
|
||||
"http://tyo-sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast",
|
||||
"http://sg-sender.helius-rpc.com/fast",
|
||||
"https://sender.helius-rpc.com/fast",
|
||||
"http://lon-sender.helius-rpc.com/fast",
|
||||
"http://slc-sender.helius-rpc.com/fast",
|
||||
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
|
||||
];
|
||||
|
||||
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
|
||||
@@ -381,3 +452,71 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
|
||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const SWQOS_REGION_ENDPOINT_TABLES: &[&[&str]] = &[
|
||||
&SWQOS_ENDPOINTS_JITO,
|
||||
&SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
&SWQOS_ENDPOINTS_ZERO_SLOT,
|
||||
&SWQOS_ENDPOINTS_TEMPORAL,
|
||||
&SWQOS_ENDPOINTS_BLOX,
|
||||
&SWQOS_ENDPOINTS_NODE1,
|
||||
&SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||
&SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
&SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
&SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_BINARY,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_QUIC,
|
||||
&SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
&SWQOS_ENDPOINTS_STELLIUM,
|
||||
&SWQOS_ENDPOINTS_SOYAS,
|
||||
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_swqos_endpoint_tables_align_with_swqos_region() {
|
||||
const EXPECT: usize = 10;
|
||||
for (idx, table) in SWQOS_REGION_ENDPOINT_TABLES.iter().enumerate() {
|
||||
assert_eq!(
|
||||
table.len(),
|
||||
EXPECT,
|
||||
"SWQOS endpoint table index {} length must match SwqosRegion (10 variants)",
|
||||
idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astralane_quic_hosts_match_mev_row_by_row() {
|
||||
for i in 0..10 {
|
||||
let base = SWQOS_ENDPOINTS_ASTRALANE_QUIC[i].trim_end_matches(":7000");
|
||||
let mev = SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[i].trim_end_matches(":9000");
|
||||
assert_eq!(base, mev, "Astralane QUIC vs MEV host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node1_http_host_matches_quic_without_port() {
|
||||
for i in 0..10 {
|
||||
let http_host =
|
||||
SWQOS_ENDPOINTS_NODE1[i].strip_prefix("http://").expect("NODE1 HTTP URL");
|
||||
let quic_host =
|
||||
SWQOS_ENDPOINTS_NODE1_QUIC[i].strip_suffix(":16666").expect("NODE1 QUIC endpoint");
|
||||
assert_eq!(http_host, quic_host, "Node1 HTTP vs QUIC host mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn astralane_plain_and_binary_same_origin_per_region() {
|
||||
for i in 0..10 {
|
||||
let plain = SWQOS_ENDPOINTS_ASTRALANE_PLAIN[i].trim_end_matches("/iris");
|
||||
let binary = SWQOS_ENDPOINTS_ASTRALANE_BINARY[i].trim_end_matches("/irisb");
|
||||
assert_eq!(plain, binary, "Astralane Plain vs Binary base URL mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,11 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.payer.pubkey(),
|
||||
¶ms.output_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
if is_a_in {
|
||||
&protocol_params.token_b_program
|
||||
} else {
|
||||
&protocol_params.token_a_program
|
||||
},
|
||||
params.open_seed_optimize,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub mod bonk;
|
||||
pub mod meteora_damm_v2;
|
||||
pub mod pumpfun;
|
||||
pub(crate) mod pumpfun_ix_data;
|
||||
pub mod pumpswap;
|
||||
pub(crate) mod pumpswap_ix_data;
|
||||
pub mod raydium_amm_v4;
|
||||
pub mod raydium_cpmm;
|
||||
pub mod utils;
|
||||
|
||||
Executable → Regular
+811
-287
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,86 @@
|
||||
//! Pump.fun 曲线 **legacy** `buy` / `buy_exact_sol_in` / `sell` 与 **`buy_v2` / `sell_v2` / `buy_exact_quote_in_v2`**
|
||||
//! 的 instruction data 栈上编码(热路径零堆分配)。
|
||||
//!
|
||||
//! Legacy `buy` / `buy_exact_sol_in` 与 `@pump-fun/pump-sdk` 对齐:`OptionBool` 在 ix 参数中为
|
||||
//! **1 字节 option tag + 1 字节值**(Anchor `Option<bool>` = 2 字节),共 26 字节 ix data。
|
||||
//! `*_v2` 指令无 `track_volume` 字节(见 [pump-public-docs](https://github.com/pump-fun/pump-public-docs))。
|
||||
|
||||
use crate::instruction::utils::pumpfun::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR,
|
||||
BUY_V2_DISCRIMINATOR, SELL_DISCRIMINATOR, SELL_V2_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
/// 与官方 `getBuyInstructionInternal` 对齐:`OptionBool` 在 ix 中为 1 字节 option tag + 1 字节值。
|
||||
/// `track_volume` 在调用侧组合为 `(1u8, cashback as u8)`(Option tag + value),共 26 字节 ix data。
|
||||
pub const TRACK_VOLUME_OPTION_TAG: u8 = 1;
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_ix_data(
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
track_volume_val: u8,
|
||||
) -> [u8; 26] {
|
||||
let mut d = [0u8; 26];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
d[24] = TRACK_VOLUME_OPTION_TAG;
|
||||
d[25] = track_volume_val;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_exact_sol_in_ix_data(
|
||||
spendable_sol_in: u64,
|
||||
min_tokens_out: u64,
|
||||
track_volume_val: u8,
|
||||
) -> [u8; 26] {
|
||||
let mut d = [0u8; 26];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_SOL_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_sol_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
d[24] = TRACK_VOLUME_OPTION_TAG;
|
||||
d[25] = track_volume_val;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_sell_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
// --- v2 instruction data encoders (no track_volume arg — 2 args each, 24 bytes total) ---
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_v2_ix_data(amount: u64, max_sol_cost: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_V2_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_sol_cost.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_buy_exact_quote_in_v2_ix_data(
|
||||
spendable_quote_in: u64,
|
||||
min_tokens_out: u64,
|
||||
) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_tokens_out.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpfun_sell_v2_ix_data(token_amount: u64, min_sol_output: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_V2_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
|
||||
d
|
||||
}
|
||||
+89
-68
@@ -1,10 +1,14 @@
|
||||
use crate::{
|
||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||
instruction::pumpswap_ix_data::{
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
|
||||
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
|
||||
},
|
||||
instruction::utils::pumpswap::{
|
||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
||||
get_protocol_extra_fee_recipient_random, get_protocol_fee_recipient_random,
|
||||
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
|
||||
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR,
|
||||
SELL_DISCRIMINATOR,
|
||||
get_user_volume_accumulator_wsol_ata, warm_pumpswap_global_config,
|
||||
},
|
||||
trading::{
|
||||
common::wsol_manager,
|
||||
@@ -28,6 +32,7 @@ pub struct PumpSwapInstructionBuilder;
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
warm_pumpswap_global_config(params.rpc.as_ref()).await;
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
@@ -76,6 +81,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let result = buy_quote_input_internal(
|
||||
@@ -84,6 +90,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// base_amount_out, max_quote_amount_in
|
||||
@@ -95,6 +102,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// min_quote_amount_out, base_amount_in
|
||||
@@ -125,13 +133,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
(accounts::FEE_RECIPIENT, accounts::FEE_RECIPIENT_META)
|
||||
};
|
||||
let fee_recipient_ata = if is_mayhem_mode {
|
||||
fee_recipient_ata(fee_recipient, crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
fee_recipient_ata(fee_recipient, quote_mint)
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
@@ -139,8 +144,17 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let mut instructions = Vec::with_capacity(6);
|
||||
|
||||
if create_wsol_ata {
|
||||
// Determine wrap amount based on instruction type:
|
||||
// - buy_exact_quote_in: program spends exactly input_amount, wrap input_amount
|
||||
// - buy: program may spend up to max_quote, wrap max_quote
|
||||
let wrap_amount =
|
||||
if quote_is_wsol_or_usdc && params.use_exact_sol_amount.unwrap_or(true) {
|
||||
params.input_amount.unwrap_or(0)
|
||||
} else {
|
||||
sol_amount
|
||||
};
|
||||
instructions
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), sol_amount));
|
||||
.extend(crate::trading::common::handle_wsol(¶ms.payer.pubkey(), wrap_amount));
|
||||
}
|
||||
|
||||
if params.create_output_mint_ata {
|
||||
@@ -156,7 +170,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
// Create buy instruction
|
||||
let mut accounts = Vec::with_capacity(23);
|
||||
let mut accounts = Vec::with_capacity(28);
|
||||
accounts.extend([
|
||||
AccountMeta::new(pool, false), // pool_id
|
||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||
@@ -192,40 +206,51 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||
}
|
||||
}
|
||||
// remainingAccounts: @pump-fun/pump-swap-sdk 要求末尾传 poolV2Pda(baseMint),勿删
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint)
|
||||
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts);
|
||||
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
}
|
||||
// Trailing accounts: GlobalConfig.buyback_fee_recipients 中任 pubkey + quote ATA(与 pump-swap-sdk 静态池对齐;轮换时需查链上)。
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
false,
|
||||
));
|
||||
|
||||
// Create instruction data(buy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数)
|
||||
let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
|
||||
let data: Vec<u8> = if quote_is_wsol_or_usdc {
|
||||
let mut buf = [0u8; 26];
|
||||
if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
|
||||
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
|
||||
if quote_is_wsol_or_usdc {
|
||||
let ix_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
|
||||
token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
buf[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(¶ms.input_amount.unwrap_or(0).to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
buf[24..26].copy_from_slice(&track_volume);
|
||||
encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
min_base_amount_out,
|
||||
track_volume,
|
||||
)
|
||||
} else {
|
||||
buf[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
buf[24..26].copy_from_slice(&track_volume);
|
||||
}
|
||||
buf.to_vec()
|
||||
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
|
||||
};
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
} else {
|
||||
let mut buf = [0u8; 24];
|
||||
buf[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
buf[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
buf[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
buf.to_vec()
|
||||
};
|
||||
|
||||
instructions.push(Instruction { program_id: accounts::AMM_PROGRAM, accounts, data });
|
||||
let ix_data = encode_pumpswap_sell_ix_data(sol_amount, token_amount);
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::AMM_PROGRAM,
|
||||
&ix_data,
|
||||
accounts,
|
||||
));
|
||||
}
|
||||
if close_wsol_ata {
|
||||
// Close wSOL ATA account, reclaim rent
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
@@ -234,6 +259,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
warm_pumpswap_global_config(params.rpc.as_ref()).await;
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
@@ -282,6 +308,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||
creator = params_coin_creator_vault_authority;
|
||||
}
|
||||
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||
|
||||
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal(
|
||||
@@ -290,6 +317,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// base_amount_in, min_quote_amount_out
|
||||
@@ -301,6 +329,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
&creator,
|
||||
cashback_fee_bps,
|
||||
)
|
||||
.unwrap();
|
||||
// max_quote_amount_in, base_amount_out
|
||||
@@ -316,13 +345,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
let (fee_recipient, fee_recipient_meta) = if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_random()
|
||||
} else {
|
||||
(accounts::FEE_RECIPIENT, accounts::FEE_RECIPIENT_META)
|
||||
};
|
||||
let fee_recipient_ata = if is_mayhem_mode {
|
||||
fee_recipient_ata(fee_recipient, crate::constants::WSOL_TOKEN_ACCOUNT)
|
||||
} else {
|
||||
fee_recipient_ata(fee_recipient, quote_mint)
|
||||
let recipient = get_protocol_fee_recipient_random();
|
||||
(recipient, AccountMeta::new_readonly(recipient, false))
|
||||
};
|
||||
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
|
||||
|
||||
let user_base_token_account =
|
||||
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
|
||||
@@ -349,7 +375,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
}
|
||||
|
||||
// Create sell instruction
|
||||
let mut accounts = Vec::with_capacity(23);
|
||||
let mut accounts = Vec::with_capacity(28);
|
||||
accounts.extend([
|
||||
AccountMeta::new(pool, false), // pool_id
|
||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||
@@ -393,32 +419,27 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
accounts.push(AccountMeta::new(accumulator, false));
|
||||
}
|
||||
}
|
||||
// remainingAccounts: @pump-fun/pump-swap-sdk sell 要求末尾传 poolV2Pda(baseMint),勿删
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint)
|
||||
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
|
||||
// Create instruction data
|
||||
let mut data = [0u8; 24];
|
||||
if quote_is_wsol_or_usdc {
|
||||
data[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
// base_amount_in
|
||||
data[8..16].copy_from_slice(&token_amount.to_le_bytes());
|
||||
// min_quote_amount_out
|
||||
data[16..24].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
} else {
|
||||
data[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
// base_amount_out
|
||||
data[8..16].copy_from_slice(&sol_amount.to_le_bytes());
|
||||
// max_quote_amount_in
|
||||
data[16..24].copy_from_slice(&token_amount.to_le_bytes());
|
||||
if protocol_params.coin_creator != Pubkey::default() {
|
||||
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||
}
|
||||
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||
accounts.push(AccountMeta::new(
|
||||
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||
false,
|
||||
));
|
||||
|
||||
instructions.push(Instruction {
|
||||
program_id: accounts::AMM_PROGRAM,
|
||||
accounts,
|
||||
data: data.to_vec(),
|
||||
});
|
||||
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
|
||||
let ix_data = if quote_is_wsol_or_usdc {
|
||||
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
|
||||
} else {
|
||||
encode_pumpswap_buy_two_args(sol_amount, token_amount)
|
||||
};
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
|
||||
|
||||
if close_wsol_ata {
|
||||
instructions.extend(crate::trading::common::close_wsol(¶ms.payer.pubkey()));
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//! PumpSwap AMM `buy` / `buy_exact_quote_in` / `sell` instruction **data**(栈数组、无 `Vec` 分配)。
|
||||
|
||||
use crate::instruction::utils::pumpswap::{
|
||||
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||
};
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_ix_data(
|
||||
base_amount_out: u64,
|
||||
max_quote_amount_in: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_buy_exact_quote_in_ix_data(
|
||||
spendable_quote_in: u64,
|
||||
min_base_amount_out: u64,
|
||||
track_volume: u8,
|
||||
) -> [u8; 25] {
|
||||
let mut d = [0u8; 25];
|
||||
d[..8].copy_from_slice(&BUY_EXACT_QUOTE_IN_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&spendable_quote_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_base_amount_out.to_le_bytes());
|
||||
d[24] = track_volume;
|
||||
d
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn encode_pumpswap_sell_ix_data(base_amount_in: u64, min_quote_amount_out: u64) -> [u8; 24] {
|
||||
let mut d = [0u8; 24];
|
||||
d[..8].copy_from_slice(&SELL_DISCRIMINATOR);
|
||||
d[8..16].copy_from_slice(&base_amount_in.to_le_bytes());
|
||||
d[16..24].copy_from_slice(&min_quote_amount_out.to_le_bytes());
|
||||
d
|
||||
}
|
||||
@@ -1,57 +1,54 @@
|
||||
//! Pump.fun bonding-curve utilities (flat module): PDAs, fee `#2`, pump-fees `SharingConfig`, cold RPC.
|
||||
//!
|
||||
//! Hot swap ix assembly stays sync; async helpers at file bottom. Layout matches `@pump-fun/pump-sdk`.
|
||||
|
||||
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
|
||||
use anyhow::anyhow;
|
||||
use borsh::BorshDeserialize;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use solana_sdk::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
// --- seeds -------------------------------------------------------------
|
||||
|
||||
pub mod seeds {
|
||||
/// Seed for bonding curve PDAs
|
||||
|
||||
/// Seed for bonding curve PDAs (`["bonding-curve", mint]`).
|
||||
pub const BONDING_CURVE_SEED: &[u8] = b"bonding-curve";
|
||||
/// Seed for bonding curve v2 PDA (required by program upgrade, readonly at end of account list)
|
||||
/// Seed for bonding curve v2 PDA (`["bonding-curve-v2", mint]`).
|
||||
pub const BONDING_CURVE_V2_SEED: &[u8] = b"bonding-curve-v2";
|
||||
|
||||
/// Seed for creator vault PDAs
|
||||
/// Creator vault PDA seeds prefix (`["creator-vault", authority]`).
|
||||
pub const CREATOR_VAULT_SEED: &[u8] = b"creator-vault";
|
||||
|
||||
/// Seed for metadata PDAs
|
||||
/// Metadata PDA seeds prefix.
|
||||
pub const METADATA_SEED: &[u8] = b"metadata";
|
||||
|
||||
/// Seed for user volume accumulator PDAs
|
||||
/// User volume accumulator for cashback / bonding-curve UX.
|
||||
pub const USER_VOLUME_ACCUMULATOR_SEED: &[u8] = b"user_volume_accumulator";
|
||||
|
||||
/// Seed for global volume accumulator PDAs
|
||||
/// Global volume accumulator.
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
|
||||
|
||||
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
|
||||
/// `feeSharingConfig` PDA under pump-fees (`feeSharingConfigPda`).
|
||||
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
|
||||
}
|
||||
|
||||
pub mod global_constants {
|
||||
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
pub const INITIAL_VIRTUAL_TOKEN_RESERVES: u64 = 1_073_000_000_000_000;
|
||||
|
||||
pub const INITIAL_VIRTUAL_SOL_RESERVES: u64 = 30_000_000_000;
|
||||
|
||||
pub const INITIAL_REAL_TOKEN_RESERVES: u64 = 793_100_000_000_000;
|
||||
|
||||
pub const TOKEN_TOTAL_SUPPLY: u64 = 1_000_000_000_000_000;
|
||||
|
||||
pub const FEE_BASIS_POINTS: u64 = 95;
|
||||
|
||||
pub const ENABLE_MIGRATE: bool = false;
|
||||
|
||||
pub const POOL_MIGRATION_FEE: u64 = 15_000_001;
|
||||
|
||||
pub const CREATOR_FEE: u64 = 30;
|
||||
pub const SCALE: u64 = 1_000_000;
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL;
|
||||
|
||||
pub const SCALE: u64 = 1_000_000; // 10^6 for token decimals
|
||||
|
||||
pub const LAMPORTS_PER_SOL: u64 = 1_000_000_000; // 10^9 for solana lamports
|
||||
|
||||
pub const COMPLETION_LAMPORTS: u64 = 85 * LAMPORTS_PER_SOL; // ~ 85 SOL
|
||||
|
||||
/// Public key for the fee recipient
|
||||
pub const FEE_RECIPIENT: Pubkey = pubkey!("62qc2CNXwrYqQScmEdiZFFAnJR262PxWEuNQtxfafNgV");
|
||||
pub const FEE_RECIPIENT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
@@ -60,7 +57,6 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Mayhem fee recipients (pump-public-docs: use any one randomly)
|
||||
pub const MAYHEM_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("GesfTA3X2arioaHp8bbKdjG9vJtskViWACZoYvxp4twS"),
|
||||
pubkey!("4budycTjhs9fD6xw62VBducVTNgMgJJ5BgtKq7mAZwn6"),
|
||||
@@ -79,7 +75,6 @@ pub mod global_constants {
|
||||
is_writable: true,
|
||||
};
|
||||
|
||||
/// Public key for the global PDA
|
||||
pub const GLOBAL_ACCOUNT: Pubkey = pubkey!("4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf");
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
@@ -88,49 +83,57 @@ pub mod global_constants {
|
||||
is_writable: false,
|
||||
};
|
||||
|
||||
/// Public key for the authority
|
||||
pub const AUTHORITY: Pubkey = pubkey!("FFWtrEQ4B4PKQoVuHYzZq8FabGkVatYzDpEVHsK5rrhF");
|
||||
|
||||
/// Public key for the withdraw authority
|
||||
pub const WITHDRAW_AUTHORITY: Pubkey = pubkey!("39azUYFWPz3VHgKCf3VChUwbpURdCHRxjWVowf5jUJjg");
|
||||
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ"); // Pump.fun AMM: Protocol Fee 1
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX"); // Pump.fun AMM: Protocol Fee 2
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz"); // Pump.fun AMM: Protocol Fee 3
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY"); // Pump.fun AMM: Protocol Fee 4
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM"); // Pump.fun AMM: Protocol Fee 5
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
|
||||
pub const PUMPFUN_AMM_FEE_1: Pubkey = pubkey!("7VtfL8fvgNfhz17qKRMjzQEXgbdpnHHHQRh54R9jP2RJ");
|
||||
pub const PUMPFUN_AMM_FEE_2: Pubkey = pubkey!("7hTckgnGnLQR6sdH7YkqFTAA7VwTfYFaZ6EhEsU3saCX");
|
||||
pub const PUMPFUN_AMM_FEE_3: Pubkey = pubkey!("9rPYyANsfQZw3DnDmKE3YCQF5E8oD89UXoHn9JFEhJUz");
|
||||
pub const PUMPFUN_AMM_FEE_4: Pubkey = pubkey!("AVmoTthdrX6tKt4nDjco2D775W2YK3sDhxPcMmzUAmTY");
|
||||
pub const PUMPFUN_AMM_FEE_5: Pubkey = pubkey!("CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM");
|
||||
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz");
|
||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
||||
// Pump.fun AMM: Protocol Fee 7
|
||||
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
|
||||
/// Buyback fee recipients (v2 account #9 in buy_v2/sell_v2).
|
||||
/// 对应官方 FEE_RECIPIENTS.md "Buyback (Applies to All)" 池,与主 fee_recipient 池互斥。
|
||||
pub const BUYBACK_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
}
|
||||
|
||||
/// Constants related to program accounts and authorities
|
||||
pub mod accounts {
|
||||
|
||||
use solana_sdk::{pubkey, pubkey::Pubkey};
|
||||
|
||||
/// Public key for the Pump.fun program
|
||||
pub const PUMPFUN: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
|
||||
|
||||
/// Public key for the MPL Token Metadata program
|
||||
pub const MPL_TOKEN_METADATA: Pubkey = pubkey!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");
|
||||
|
||||
/// Authority for program events
|
||||
pub const EVENT_AUTHORITY: Pubkey = pubkey!("Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1");
|
||||
|
||||
/// Associated Token Program ID
|
||||
pub const ASSOCIATED_TOKEN_PROGRAM: Pubkey =
|
||||
pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
||||
|
||||
pub const AMM_PROGRAM: Pubkey = pubkey!("675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8");
|
||||
|
||||
pub const FEE_PROGRAM: Pubkey = pubkey!("pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ");
|
||||
|
||||
pub const GLOBAL_VOLUME_ACCUMULATOR: Pubkey =
|
||||
pubkey!("Hq2wp8uJ9jCPsYgNHex8RtqdvMPfVGoYwjvF1ATiwn2Y");
|
||||
|
||||
pub const FEE_CONFIG: Pubkey = pubkey!("8Wf5TiAheLUqBrKXeYg2JtAFFMWtKdG2BSFgqUcPVwTt");
|
||||
|
||||
// META
|
||||
pub const PUMPFUN_META: solana_sdk::instruction::AccountMeta =
|
||||
solana_sdk::instruction::AccountMeta {
|
||||
pubkey: PUMPFUN,
|
||||
@@ -167,12 +170,86 @@ pub mod accounts {
|
||||
};
|
||||
}
|
||||
|
||||
/// Instruction discriminators for PumpFun program
|
||||
// --- Anchor / layout constants ---------------------------------------
|
||||
|
||||
/// Minimum bonding curve account data length (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
|
||||
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
|
||||
|
||||
pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
/// Returns a random Mayhem fee recipient AccountMeta (pump-public-docs: Bonding Curve 2nd account = Mayhem fee recipient; use any one randomly).
|
||||
/// `buy_v2` — unified SOL/USDC quote interface ([pump-public-docs](https://github.com/pump-fun/pump-public-docs)).
|
||||
pub const BUY_V2_DISCRIMINATOR: [u8; 8] = [184, 23, 238, 97, 103, 197, 211, 61];
|
||||
/// `sell_v2`
|
||||
pub const SELL_V2_DISCRIMINATOR: [u8; 8] = [93, 246, 130, 60, 231, 233, 64, 178];
|
||||
/// `buy_exact_quote_in_v2` (native SOL spend for SOL-paired coins when `quote_mint` is WSOL)
|
||||
pub const BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR: [u8; 8] = [194, 171, 28, 70, 104, 77, 91, 47];
|
||||
|
||||
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
|
||||
|
||||
pub const SHARING_CONFIG_ACCOUNT_DISCRIMINATOR: [u8; 8] = [216, 74, 9, 0, 56, 140, 93, 75];
|
||||
|
||||
pub(crate) const SHARING_CONFIG_STATUS_ACTIVE: u8 = 1;
|
||||
|
||||
// --- Fee recipient pools -----------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(pubkey)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
pubkey == &global_constants::PUMPFUN_AMM_FEE_1
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_2
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_3
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_4
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_5
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_6
|
||||
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_standard_bonding_fee_recipient(pubkey: &Pubkey) -> bool {
|
||||
*pubkey == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(pubkey)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reconcile_mayhem_mode_for_trade(
|
||||
mayhem_from_event: Option<bool>,
|
||||
fee_recipient: &Pubkey,
|
||||
) -> bool {
|
||||
if *fee_recipient == Pubkey::default() {
|
||||
return mayhem_from_event.unwrap_or(false);
|
||||
}
|
||||
let fee_m = is_mayhem_fee_recipient(fee_recipient);
|
||||
let fee_s = is_standard_bonding_fee_recipient(fee_recipient);
|
||||
match mayhem_from_event {
|
||||
Some(log_m) => {
|
||||
if fee_m && !log_m {
|
||||
true
|
||||
} else if fee_s && log_m && !fee_m {
|
||||
false
|
||||
} else {
|
||||
log_m
|
||||
}
|
||||
}
|
||||
None => fee_m,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn fee_recipient_ok_for_bonding_curve_mode(pk: &Pubkey, is_mayhem_mode: bool) -> bool {
|
||||
let is_m = is_mayhem_fee_recipient(pk);
|
||||
let is_s = is_standard_bonding_fee_recipient(pk);
|
||||
if is_mayhem_mode {
|
||||
!(is_s && !is_m)
|
||||
} else {
|
||||
!(is_m && !is_s)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
|
||||
@@ -181,12 +258,75 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
pub struct Symbol;
|
||||
|
||||
impl Symbol {
|
||||
pub const SOLANA: &'static str = "solana";
|
||||
#[inline]
|
||||
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
||||
const POOL: &[Pubkey] = &[
|
||||
global_constants::FEE_RECIPIENT,
|
||||
global_constants::PUMPFUN_AMM_FEE_1,
|
||||
global_constants::PUMPFUN_AMM_FEE_2,
|
||||
global_constants::PUMPFUN_AMM_FEE_3,
|
||||
global_constants::PUMPFUN_AMM_FEE_4,
|
||||
global_constants::PUMPFUN_AMM_FEE_5,
|
||||
global_constants::PUMPFUN_AMM_FEE_6,
|
||||
global_constants::PUMPFUN_AMM_FEE_7,
|
||||
];
|
||||
let recipient = *POOL.choose(&mut rand::rng()).unwrap_or(&global_constants::FEE_RECIPIENT);
|
||||
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
/// Buyback fee recipient (#9 in buy_v2/sell_v2) — dedicated pool, distinct from protocol extra fee recipients.
|
||||
#[inline]
|
||||
pub fn get_buyback_fee_recipient_random() -> Pubkey {
|
||||
*global_constants::BUYBACK_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&global_constants::BUYBACK_FEE_RECIPIENTS[0])
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pump_fun_fee_recipient_meta(
|
||||
observed_fee_recipient: Pubkey,
|
||||
is_mayhem_mode: bool,
|
||||
) -> AccountMeta {
|
||||
let trust_observation = observed_fee_recipient != Pubkey::default()
|
||||
&& fee_recipient_ok_for_bonding_curve_mode(&observed_fee_recipient, is_mayhem_mode);
|
||||
if trust_observation {
|
||||
AccountMeta { pubkey: observed_fee_recipient, is_signer: false, is_writable: true }
|
||||
} else if is_mayhem_mode {
|
||||
get_mayhem_fee_recipient_meta_random()
|
||||
} else {
|
||||
get_standard_fee_recipient_meta_random()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Extend bonding curve (cold path) --------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn extend_bonding_curve_account_instruction(
|
||||
bonding_curve: &Pubkey,
|
||||
user: &Pubkey,
|
||||
) -> Instruction {
|
||||
Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&EXTEND_ACCOUNT_DISCRIMINATOR,
|
||||
vec![
|
||||
AccountMeta::new(*bonding_curve, false),
|
||||
AccountMeta::new(*user, true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
// --- Cached PDAs + creator_vault resolve ------------------------------
|
||||
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -194,13 +334,11 @@ pub fn get_bonding_curve_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Bonding curve v2 PDA (seeds: ["bonding-curve-v2", mint]). Required at end of buy/sell/buy_exact_sol_in accounts.
|
||||
#[inline]
|
||||
pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
@@ -208,8 +346,7 @@ pub fn get_bonding_curve_v2_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::BONDING_CURVE_V2_SEED, mint.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -219,7 +356,6 @@ pub fn get_creator(creator_vault_pda: &Pubkey) -> Pubkey {
|
||||
if creator_vault_pda.eq(&Pubkey::default()) {
|
||||
Pubkey::default()
|
||||
} else {
|
||||
// Fast check against cached default creator vault
|
||||
static DEFAULT_CREATOR_VAULT: std::sync::LazyLock<Option<Pubkey>> =
|
||||
std::sync::LazyLock::new(|| get_creator_vault_pda(&Pubkey::default()));
|
||||
match DEFAULT_CREATOR_VAULT.as_ref() {
|
||||
@@ -236,45 +372,90 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::CREATOR_VAULT_SEED, creator.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seeds, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunFeeSharingConfig(*mint),
|
||||
|| {
|
||||
Pubkey::try_find_program_address(
|
||||
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
|
||||
&accounts::FEE_PROGRAM,
|
||||
)
|
||||
.map(|(p, _)| p)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn phantom_default_creator_vault() -> Pubkey {
|
||||
solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc")
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
|
||||
*pk == phantom_default_creator_vault()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
mint: &Pubkey,
|
||||
) -> Option<Pubkey> {
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(creator, creator_vault_from_event, mint, None)
|
||||
}
|
||||
|
||||
/// Resolves Pump.fun bonding-curve buy/sell **account `#10` (`creator_vault`)** for ix assembly.
|
||||
///
|
||||
/// **Priority (highest first)**
|
||||
/// 1. **Explicit `creator_vault`** from ix / parser / cached observation when non-default and not the phantom
|
||||
/// sentinel — **always used as-is** (no remap to [`get_creator_vault_pda`] from `creator`);
|
||||
/// fee-sharing / multi-party layouts rely on upstream passing the vault the program expects (`pfee…` / `update_fee_shares`).
|
||||
/// 2. If ix vault missing: optional `fee_sharing_creator_vault_if_active` hint (non-default, non-phantom).
|
||||
/// 3. Else: [`get_creator_vault_pda`] from `creator` when `creator` is known.
|
||||
#[inline]
|
||||
pub fn resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
creator: &Pubkey,
|
||||
creator_vault_from_event: Pubkey,
|
||||
_mint: &Pubkey,
|
||||
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
) -> Option<Pubkey> {
|
||||
let phantom = phantom_default_creator_vault();
|
||||
|
||||
if creator_vault_from_event != Pubkey::default() && creator_vault_from_event != phantom {
|
||||
return Some(creator_vault_from_event);
|
||||
}
|
||||
|
||||
if let Some(v) = fee_sharing_creator_vault_if_active {
|
||||
if v != Pubkey::default() && v != phantom {
|
||||
return Some(v);
|
||||
}
|
||||
}
|
||||
|
||||
if *creator == Pubkey::default() {
|
||||
return None;
|
||||
}
|
||||
|
||||
get_creator_vault_pda(creator)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
|
||||
crate::common::fast_fn::get_cached_pda(
|
||||
crate::common::fast_fn::PdaCacheKey::PumpFunUserVolume(*user),
|
||||
|| {
|
||||
let seeds: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let seed: &[&[u8]; 2] = &[seeds::USER_VOLUME_ACCUMULATOR_SEED, user.as_ref()];
|
||||
let program_id: &Pubkey = &accounts::PUMPFUN;
|
||||
let pda: Option<(Pubkey, u8)> = Pubkey::try_find_program_address(seeds, program_id);
|
||||
pda.map(|pubkey| pubkey.0)
|
||||
Pubkey::try_find_program_address(seed, program_id).map(|pubkey| pubkey.0)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or(anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
let bonding_curve =
|
||||
solana_sdk::borsh1::try_from_slice_unchecked::<BondingCurveAccount>(&account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to deserialize bonding curve account: {}", e))?;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_buy_price(
|
||||
amount: u64,
|
||||
@@ -295,9 +476,70 @@ pub fn get_buy_price(
|
||||
s_u64.min(real_token_reserves)
|
||||
}
|
||||
|
||||
// --- RPC -------------------------------------------------------------
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Option<Pubkey>, anyhow::Error> {
|
||||
let Some(config_pda) = get_fee_sharing_config_pda(mint) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let acc = match rpc.get_account(&config_pda).await {
|
||||
Ok(a) => a,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
if acc.owner != accounts::FEE_PROGRAM {
|
||||
return Ok(None);
|
||||
}
|
||||
let d = acc.data.as_slice();
|
||||
if d.len() < 43 || d[..8] != SHARING_CONFIG_ACCOUNT_DISCRIMINATOR {
|
||||
return Ok(None);
|
||||
}
|
||||
if d[10] != SHARING_CONFIG_STATUS_ACTIVE {
|
||||
return Ok(None);
|
||||
}
|
||||
let mint_on_chain = Pubkey::new_from_array(
|
||||
d[11..43].try_into().map_err(|_| anyhow!("SharingConfig mint slice"))?,
|
||||
);
|
||||
if mint_on_chain != *mint {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(get_creator_vault_pda(&config_pda))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn fetch_bonding_curve_account(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<(Arc<BondingCurveAccount>, Pubkey), anyhow::Error> {
|
||||
let bonding_curve_pda: Pubkey =
|
||||
get_bonding_curve_pda(mint).ok_or_else(|| anyhow!("Bonding curve not found"))?;
|
||||
|
||||
let account = rpc.get_account(&bonding_curve_pda).await?;
|
||||
if account.data.is_empty() {
|
||||
return Err(anyhow!("Bonding curve not found"));
|
||||
}
|
||||
|
||||
// Use `deserialize` instead of `try_from_slice` so that extra trailing bytes
|
||||
// (from on-chain schema additions like new fields) are silently ignored.
|
||||
// `try_from_slice` requires the entire slice to be consumed, causing
|
||||
// "Not all bytes read" when the account has been extended.
|
||||
let mut bonding_curve = BondingCurveAccount::deserialize(&mut &account.data[8..])
|
||||
.map_err(|e| anyhow::anyhow!("Failed to decode bonding curve account: {}", e))?;
|
||||
bonding_curve.account = bonding_curve_pda;
|
||||
|
||||
Ok((Arc::new(bonding_curve), bonding_curve_pda))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::{
|
||||
global_constants, phantom_default_creator_vault, pump_fun_fee_recipient_meta,
|
||||
reconcile_mayhem_mode_for_trade, resolve_creator_vault_for_ix,
|
||||
resolve_creator_vault_for_ix_with_fee_sharing, *,
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[test]
|
||||
@@ -305,6 +547,9 @@ mod tests {
|
||||
assert_eq!(BUY_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_EXACT_SOL_IN_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(SELL_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_V2_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(SELL_V2_DISCRIMINATOR.len(), 8);
|
||||
assert_eq!(BUY_EXACT_QUOTE_IN_V2_DISCRIMINATOR.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -322,4 +567,167 @@ mod tests {
|
||||
let b = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fee_sharing_config_pda_deterministic() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let a = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let b = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_creator_yields_fixed_creator_vault() {
|
||||
let v = get_creator_vault_pda(&Pubkey::default()).unwrap();
|
||||
assert_eq!(
|
||||
v,
|
||||
phantom_default_creator_vault(),
|
||||
"phantom vault constant must match PDA(default creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_uses_ix_vault_when_creator_borsh_is_default() {
|
||||
let mint = Pubkey::new_unique();
|
||||
let ix_vault = Pubkey::new_unique();
|
||||
let resolved = resolve_creator_vault_for_ix(&Pubkey::default(), ix_vault, &mint);
|
||||
assert_eq!(resolved, Some(ix_vault));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_returns_none_when_creator_and_vault_missing() {
|
||||
let mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(&Pubkey::default(), Pubkey::default(), &mint),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() {
|
||||
let mint = Pubkey::new_unique();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(
|
||||
&Pubkey::default(),
|
||||
phantom_default_creator_vault(),
|
||||
&mint,
|
||||
),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_prefers_creator_pda_ix_vault_even_when_fee_sharing_hint_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, v_derived, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(v_derived),
|
||||
"observed ix uses PDA(creator); stale hint must not override → wrong vault / Anchor 2006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_trusts_ix_fee_sharing_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let creator_vault = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_ne!(vs, creator_vault);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, Some(vs));
|
||||
assert_eq!(
|
||||
resolved,
|
||||
Some(vs),
|
||||
"explicit ix creator_vault (e.g. fee-sharing #8) wins; no remap to PDA(creator)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_remaps_phantom_vault_when_creator_known() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let expected = get_creator_vault_pda(&creator).unwrap();
|
||||
assert_eq!(
|
||||
resolve_creator_vault_for_ix(&creator, phantom_default_creator_vault(), &mint),
|
||||
Some(expected)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_ix_vault_always_wins_when_non_default_even_if_creator_pda_differs() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let ix_other = Pubkey::new_unique();
|
||||
assert_ne!(ix_other, v_derived);
|
||||
let resolved =
|
||||
resolve_creator_vault_for_ix_with_fee_sharing(&creator, ix_other, &mint, None);
|
||||
assert_eq!(resolved, Some(ix_other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_fee_sharing_ix_vault_used_as_is_even_without_hint() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let v_derived = get_creator_vault_pda(&creator).unwrap();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
assert_ne!(v_derived, vs);
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(&creator, vs, &mint, None);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_falls_back_to_fee_sharing_hint_when_ix_vault_placeholder() {
|
||||
let creator = Pubkey::new_unique();
|
||||
let mint = Pubkey::new_unique();
|
||||
let sharing_pk = get_fee_sharing_config_pda(&mint).unwrap();
|
||||
let vs = get_creator_vault_pda(&sharing_pk).unwrap();
|
||||
let resolved = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
Pubkey::default(),
|
||||
&mint,
|
||||
Some(vs),
|
||||
);
|
||||
assert_eq!(resolved, Some(vs));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_false_but_fee_is_mayhem_pool() {
|
||||
let fee = global_constants::MAYHEM_FEE_RECIPIENTS[0];
|
||||
assert!(reconcile_mayhem_mode_for_trade(Some(false), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconcile_mayhem_prefers_fee_when_log_says_true_but_fee_is_standard_pool() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
assert!(!reconcile_mayhem_mode_for_trade(Some(true), &fee));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_rejects_standard_fee_when_building_mayhem_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_4;
|
||||
let m = pump_fun_fee_recipient_meta(fee, true);
|
||||
assert!(
|
||||
global_constants::MAYHEM_FEE_RECIPIENTS.contains(&m.pubkey),
|
||||
"expected fallback to mayhem pool, got {}",
|
||||
m.pubkey
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pump_fee_meta_uses_observed_standard_fee_for_standard_ix() {
|
||||
let fee = global_constants::PUMPFUN_AMM_FEE_7;
|
||||
let m = pump_fun_fee_recipient_meta(fee, false);
|
||||
assert_eq!(m.pubkey, fee);
|
||||
assert!(m.is_writable);
|
||||
assert!(!m.is_signer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,16 @@ use crate::{
|
||||
instruction::utils::pumpswap_types::{pool_decode, Pool},
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::RwLock;
|
||||
use rand::seq::IndexedRandom;
|
||||
use solana_account_decoder::UiAccountEncoding;
|
||||
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::warn;
|
||||
|
||||
/// PumpSwap 池账户总长度(见 pump-public-docs Breaking Change):8 字节 discriminator + 244 字节 Pool。
|
||||
/// 官方文档:pool structure needs to be 244 bytes (was 243),含 is_mayhem_mode。DataSize 必须与此一致,否则 getProgramAccounts 会返回 0。
|
||||
const POOL_ACCOUNT_DATA_LEN: u64 = 8 + 244;
|
||||
// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22)
|
||||
|
||||
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
|
||||
pub mod seeds {
|
||||
@@ -93,6 +96,20 @@ pub mod accounts {
|
||||
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
|
||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||
|
||||
/// Buyback trailing fee recipients (`GlobalConfig.buyback_fee_recipients` on Pump AMM).
|
||||
/// Must match one of these for the pubkey passed after optional `pool-v2` (`@pump-fun/pump-swap-sdk` `getBuybackFeeRecipient`).
|
||||
/// Static mirror of pump-public-docs; if protocol rotates configs, decode global_config from RPC.
|
||||
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||
];
|
||||
|
||||
// META
|
||||
|
||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||
@@ -163,16 +180,182 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
|
||||
pub const BUY_EXACT_QUOTE_IN_DISCRIMINATOR: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112];
|
||||
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
|
||||
|
||||
const PUMPSWAP_GLOBAL_CONFIG_TTL: Duration = Duration::from_secs(90);
|
||||
const PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT: Duration = Duration::from_millis(180);
|
||||
|
||||
const PUBKEY_LEN: usize = 32;
|
||||
const U64_LEN: usize = 8;
|
||||
const U8_LEN: usize = 1;
|
||||
const BOOL_LEN: usize = 1;
|
||||
const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GlobalConfig {
|
||||
pub protocol_fee_recipients: [Pubkey; 8],
|
||||
pub reserved_fee_recipient: Pubkey,
|
||||
pub reserved_fee_recipients: [Pubkey; 7],
|
||||
pub buyback_fee_recipients: [Pubkey; 8],
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CachedGlobalConfig {
|
||||
fetched_at: Instant,
|
||||
config: GlobalConfig,
|
||||
}
|
||||
|
||||
static GLOBAL_CONFIG_CACHE: Lazy<RwLock<Option<CachedGlobalConfig>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
|
||||
fn read_pubkey(data: &[u8], offset: usize) -> Option<Pubkey> {
|
||||
let bytes = data.get(offset..offset + PUBKEY_LEN)?;
|
||||
Some(Pubkey::new_from_array(bytes.try_into().ok()?))
|
||||
}
|
||||
|
||||
fn read_pubkey_array<const N: usize>(data: &[u8], offset: usize) -> Option<[Pubkey; N]> {
|
||||
let mut keys = [Pubkey::default(); N];
|
||||
for (i, key) in keys.iter_mut().enumerate() {
|
||||
*key = read_pubkey(data, offset + i * PUBKEY_LEN)?;
|
||||
}
|
||||
Some(keys)
|
||||
}
|
||||
|
||||
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
|
||||
let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN;
|
||||
offset += PUBKEY_LEN; // admin
|
||||
offset += U64_LEN * 2; // lp_fee_basis_points + protocol_fee_basis_points
|
||||
offset += U8_LEN; // disable_flags
|
||||
|
||||
let protocol_fee_recipients = read_pubkey_array::<8>(data, offset)?;
|
||||
offset += PUBKEY_LEN * 8;
|
||||
offset += U64_LEN; // coin_creator_fee_basis_points
|
||||
offset += PUBKEY_LEN; // admin_set_coin_creator_authority
|
||||
offset += PUBKEY_LEN; // whitelist_pda
|
||||
|
||||
let reserved_fee_recipient = read_pubkey(data, offset)?;
|
||||
offset += PUBKEY_LEN;
|
||||
offset += BOOL_LEN; // mayhem_mode_enabled
|
||||
|
||||
let reserved_fee_recipients = read_pubkey_array::<7>(data, offset)?;
|
||||
offset += PUBKEY_LEN * 7;
|
||||
offset += BOOL_LEN; // is_cashback_enabled
|
||||
|
||||
let buyback_fee_recipients = read_pubkey_array::<8>(data, offset)?;
|
||||
|
||||
Some(GlobalConfig {
|
||||
protocol_fee_recipients,
|
||||
reserved_fee_recipient,
|
||||
reserved_fee_recipients,
|
||||
buyback_fee_recipients,
|
||||
})
|
||||
}
|
||||
|
||||
async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfig> {
|
||||
let account = match tokio::time::timeout(
|
||||
PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT,
|
||||
rpc.get_account(&accounts::GLOBAL_ACCOUNT),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(account)) => account,
|
||||
Ok(Err(e)) => {
|
||||
warn!(target: "pumpswap_global_config", "PumpSwap GlobalConfig 读取失败: {}", e);
|
||||
return None;
|
||||
}
|
||||
Err(_) => {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
timeout_ms = PUMPSWAP_GLOBAL_CONFIG_RPC_TIMEOUT.as_millis(),
|
||||
"PumpSwap GlobalConfig 读取超时"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(config) = decode_global_config(&account.data) else {
|
||||
warn!(
|
||||
target: "pumpswap_global_config",
|
||||
data_len = account.data.len(),
|
||||
"PumpSwap GlobalConfig 解析失败"
|
||||
);
|
||||
return None;
|
||||
};
|
||||
|
||||
*GLOBAL_CONFIG_CACHE.write() =
|
||||
Some(CachedGlobalConfig { fetched_at: Instant::now(), config: config.clone() });
|
||||
Some(config)
|
||||
}
|
||||
|
||||
pub async fn warm_pumpswap_global_config(rpc: Option<&Arc<SolanaRpcClient>>) {
|
||||
let Some(rpc) = rpc else {
|
||||
return;
|
||||
};
|
||||
let stale = GLOBAL_CONFIG_CACHE
|
||||
.read()
|
||||
.as_ref()
|
||||
.map(|c| c.fetched_at.elapsed() > PUMPSWAP_GLOBAL_CONFIG_TTL)
|
||||
.unwrap_or(true);
|
||||
if stale {
|
||||
let _ = refresh_global_config_once(rpc.as_ref()).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn cached_global_config() -> Option<GlobalConfig> {
|
||||
let guard = GLOBAL_CONFIG_CACHE.read();
|
||||
let cached = guard.as_ref()?;
|
||||
(cached.fetched_at.elapsed() <= PUMPSWAP_GLOBAL_CONFIG_TTL).then(|| cached.config.clone())
|
||||
}
|
||||
|
||||
fn choose_nonzero(keys: &[Pubkey]) -> Option<Pubkey> {
|
||||
let mut valid = [Pubkey::default(); 8];
|
||||
let mut len = 0;
|
||||
for key in keys.iter().copied() {
|
||||
if key == Pubkey::default() || len == valid.len() {
|
||||
continue;
|
||||
}
|
||||
valid[len] = key;
|
||||
len += 1;
|
||||
}
|
||||
valid[..len].choose(&mut rand::rng()).copied()
|
||||
}
|
||||
|
||||
/// Returns a random Mayhem fee recipient and its AccountMeta (pump-public-docs: use any one randomly).
|
||||
#[inline]
|
||||
pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
|
||||
let recipient = *accounts::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0]);
|
||||
let recipient = cached_global_config()
|
||||
.and_then(|config| {
|
||||
let mut pool = [Pubkey::default(); 8];
|
||||
pool[0] = config.reserved_fee_recipient;
|
||||
pool[1..].copy_from_slice(&config.reserved_fee_recipients);
|
||||
choose_nonzero(&pool)
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
*accounts::MAYHEM_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::MAYHEM_FEE_RECIPIENTS[0])
|
||||
});
|
||||
let meta = AccountMeta { pubkey: recipient, is_signer: false, is_writable: false };
|
||||
(recipient, meta)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_protocol_fee_recipient_random() -> Pubkey {
|
||||
cached_global_config()
|
||||
.and_then(|config| choose_nonzero(&config.protocol_fee_recipients))
|
||||
.unwrap_or(accounts::FEE_RECIPIENT)
|
||||
}
|
||||
|
||||
/// Random entry from [`accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (readonly; paired with [`fee_recipient_ata`] as last account).
|
||||
#[inline]
|
||||
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
|
||||
cached_global_config()
|
||||
.and_then(|config| choose_nonzero(&config.buyback_fee_recipients))
|
||||
.unwrap_or_else(|| {
|
||||
*accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS
|
||||
.choose(&mut rand::rng())
|
||||
.unwrap_or(&accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
|
||||
})
|
||||
}
|
||||
|
||||
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
|
||||
#[inline]
|
||||
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||
@@ -301,20 +484,23 @@ pub async fn fetch_pool(
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub async fn find_by_base_mint(
|
||||
/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022)
|
||||
const POOL_DATA_LEN_SPL: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_T22: u64 = 643;
|
||||
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel.
|
||||
async fn get_program_accounts_both_sizes(
|
||||
rpc: &SolanaRpcClient,
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// Use getProgramAccounts to find pools for the given mint.
|
||||
// base_mint 在账户布局中的偏移:8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let filters = vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, base_mint.as_ref()),
|
||||
),
|
||||
];
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(filters),
|
||||
memcmp_offset: usize,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Vec<(Pubkey, solana_sdk::account::Account)>, anyhow::Error> {
|
||||
let make_config = |data_size: u64| solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
|
||||
),
|
||||
]),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
@@ -326,91 +512,62 @@ pub async fn find_by_base_mint(
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||
}
|
||||
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
|
||||
let mut pools: Vec<_> = accounts
|
||||
let (spl_result, t22_result) = tokio::join!(
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)),
|
||||
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)),
|
||||
);
|
||||
let mut all = spl_result.unwrap_or_default();
|
||||
all.extend(t22_result.unwrap_or_default());
|
||||
Ok(all)
|
||||
}
|
||||
|
||||
fn decode_pool_accounts(
|
||||
accounts: Vec<(Pubkey, solana_sdk::account::Account)>,
|
||||
) -> Vec<(Pubkey, Pool)> {
|
||||
accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
// 🔧 修复:跳过8字节的discriminator
|
||||
if acc.data.len() > 8 {
|
||||
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
|
||||
if pools.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No valid pool decoded for mint {} (found {} accounts but all decode failed)",
|
||||
base_mint,
|
||||
accounts_count
|
||||
));
|
||||
pub async fn find_by_base_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
base_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", base_mint));
|
||||
}
|
||||
let mut pools = decode_pool_accounts(accounts);
|
||||
if pools.is_empty() {
|
||||
return Err(anyhow!("No valid pool decoded for mint {}", base_mint));
|
||||
}
|
||||
|
||||
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
|
||||
let first = &pools[0];
|
||||
Ok((first.0, first.1.clone()))
|
||||
Ok((pools[0].0, pools[0].1.clone()))
|
||||
}
|
||||
|
||||
pub async fn find_by_quote_mint(
|
||||
rpc: &SolanaRpcClient,
|
||||
quote_mint: &Pubkey,
|
||||
) -> Result<(Pubkey, Pool), anyhow::Error> {
|
||||
// Use getProgramAccounts to find pools for the given mint.
|
||||
// quote_mint 在账户布局中的偏移:8 + 1 + 2 + 32 + 32 = 75
|
||||
let filters = vec![
|
||||
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
|
||||
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
|
||||
solana_client::rpc_filter::Memcmp::new_base58_encoded(75, quote_mint.as_ref()),
|
||||
),
|
||||
];
|
||||
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
|
||||
filters: Some(filters),
|
||||
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
|
||||
encoding: Some(UiAccountEncoding::Base64),
|
||||
data_slice: None,
|
||||
commitment: None,
|
||||
min_context_slot: None,
|
||||
},
|
||||
with_context: None,
|
||||
sort_results: None,
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
|
||||
// quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75
|
||||
let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?;
|
||||
if accounts.is_empty() {
|
||||
return Err(anyhow!("No pool found for mint {}", quote_mint));
|
||||
}
|
||||
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
|
||||
let mut pools: Vec<_> = accounts
|
||||
.into_iter()
|
||||
.filter_map(|(addr, acc)| {
|
||||
// 🔧 修复:跳过8字节的discriminator
|
||||
if acc.data.len() > 8 {
|
||||
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
|
||||
let mut pools = decode_pool_accounts(accounts);
|
||||
if pools.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"No valid pool decoded for quote_mint {} (found {} accounts but all decode failed)",
|
||||
quote_mint,
|
||||
accounts_count
|
||||
));
|
||||
return Err(anyhow!("No valid pool decoded for quote_mint {}", quote_mint));
|
||||
}
|
||||
|
||||
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
|
||||
let first = &pools[0];
|
||||
Ok((first.0, first.1.clone()))
|
||||
Ok((pools[0].0, pools[0].1.clone()))
|
||||
}
|
||||
|
||||
/// 按 mint 查找 PumpSwap 池(本函数仅用于 PumpSwap,其他 DEX 勿用)。
|
||||
@@ -444,21 +601,25 @@ pub async fn find_by_mint(
|
||||
Err(e) => diag.push(format!("canonical get_account/decode 失败: {}", e)),
|
||||
}
|
||||
|
||||
// 3. 回退:getProgramAccounts 按 base_mint / quote_mint
|
||||
match find_by_base_mint(rpc, mint).await {
|
||||
Ok((address, pool)) => return Ok((address, pool)),
|
||||
Err(e) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
|
||||
// 3. Fallback: getProgramAccounts by base_mint / quote_mint (with 3s timeout to avoid blocking)
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_base_mint(rpc, mint))
|
||||
.await
|
||||
{
|
||||
Ok(Ok((address, pool))) => return Ok((address, pool)),
|
||||
Ok(Err(e)) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
|
||||
Err(_) => diag.push("getProgramAccounts(base_mint): timed out (3s)".into()),
|
||||
}
|
||||
match find_by_quote_mint(rpc, mint).await {
|
||||
Ok((address, pool)) => return Ok((address, pool)),
|
||||
Err(e) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_quote_mint(rpc, mint))
|
||||
.await
|
||||
{
|
||||
Ok(Ok((address, pool))) => return Ok((address, pool)),
|
||||
Ok(Err(e)) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
|
||||
Err(_) => diag.push("getProgramAccounts(quote_mint): timed out (3s)".into()),
|
||||
}
|
||||
|
||||
Err(anyhow!(
|
||||
"No pool found for mint {}. 诊断: {}。若使用自建 RPC 请确认已开启 getProgramAccounts 或换用公共 RPC 重试;若代币未在 PumpSwap 建池请先在 pump.fun/DEX 上确认",
|
||||
mint,
|
||||
diag.join("; ")
|
||||
))
|
||||
let diag_str = diag.join("; ");
|
||||
eprintln!("[find_by_mint] {} failed: {}", mint, diag_str);
|
||||
Err(anyhow!("No pool found for mint {}. diag: {}", mint, diag_str))
|
||||
}
|
||||
|
||||
pub async fn get_token_balances(
|
||||
|
||||
+7
-1164
File diff suppressed because it is too large
Load Diff
@@ -447,23 +447,23 @@ impl BranchOptimizer {
|
||||
|
||||
/// Prefetch: load cache line at ptr into L1. Caller must ensure ptr is valid, read-only, no concurrent write. 预取:将 ptr 所在缓存行加载到 L1;调用方需保证有效、只读、无并发写。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_read_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_read_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T0;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T0);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prefetch for write (T1 hint). 写预取(T1 提示)。
|
||||
#[inline(always)]
|
||||
pub unsafe fn prefetch_write_data<T>(ptr: *const T) {
|
||||
pub unsafe fn prefetch_write_data<T>(_ptr: *const T) {
|
||||
#[cfg(target_arch = "x86_64")]
|
||||
{
|
||||
use std::arch::x86_64::_mm_prefetch;
|
||||
use std::arch::x86_64::_MM_HINT_T1;
|
||||
_mm_prefetch(ptr as *const i8, _MM_HINT_T1);
|
||||
_mm_prefetch(_ptr as *const i8, _MM_HINT_T1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+49
-11
@@ -27,6 +27,8 @@ pub enum AstralaneBackend {
|
||||
Http {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
/// Mirrors global `mev_protection`: adds `mev-protect=true` on HTTP sends (QUIC uses :9000 instead).
|
||||
mev_http: bool,
|
||||
http_client: Client,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
@@ -77,8 +79,8 @@ impl SwqosClientTrait for AstralaneClient {
|
||||
}
|
||||
|
||||
impl AstralaneClient {
|
||||
/// 使用 HTTP(irisb)提交。
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
/// HTTP 提交:`/iris`(Plain)或 `/irisb`(Binary),由 `endpoint` URL 路径区分;`mev_http` 为 true 时附加 `mev-protect=true`。
|
||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String, mev_http: bool) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().build().unwrap();
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
@@ -89,6 +91,7 @@ impl AstralaneClient {
|
||||
backend: AstralaneBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_http,
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
@@ -119,6 +122,7 @@ impl AstralaneClient {
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
..
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
@@ -182,10 +186,14 @@ impl AstralaneClient {
|
||||
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||
|
||||
match &self.backend {
|
||||
AstralaneBackend::Http { endpoint, auth_token, http_client, .. } => {
|
||||
let response = http_client
|
||||
AstralaneBackend::Http { endpoint, auth_token, mev_http, http_client, .. } => {
|
||||
let mut req = http_client
|
||||
.post(endpoint)
|
||||
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")])
|
||||
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")]);
|
||||
if *mev_http {
|
||||
req = req.query(&[("mev-protect", "true")]);
|
||||
}
|
||||
let response = req
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(body_bytes)
|
||||
.send()
|
||||
@@ -194,11 +202,20 @@ impl AstralaneClient {
|
||||
let _ = response.bytes().await;
|
||||
if status.is_success() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Astralane", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Astralane", trade_type, start_time.elapsed(), format!("status {}", status));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {}", status),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("Astralane sendTransaction failed: {}", status));
|
||||
}
|
||||
@@ -206,12 +223,21 @@ impl AstralaneClient {
|
||||
AstralaneBackend::Quic(quic) => {
|
||||
if let Err(e) = quic.send_transaction(&body_bytes).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Astralane", trade_type, start_time.elapsed(), &e);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Astralane", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,14 +248,26 @@ impl AstralaneClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "Astralane", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Astralane", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Astralane",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+17
-17
@@ -52,34 +52,32 @@ impl AstralaneQuicClient {
|
||||
fn astralane_quic_ip_candidates(host: &str, port: u16) -> Vec<SocketAddr> {
|
||||
// Official recommended direct-IP list (faster/more stable than DNS-only for QUIC).
|
||||
// We intentionally avoid fr2/ams2 per prior guidance.
|
||||
// Both port 7000 (standard) and port 9000 (MEV-protected) use the same IPs.
|
||||
match host {
|
||||
"fr.gateway.astralane.io" => vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), 7000),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), 7000),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(185, 191, 117, 97)), port),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(45, 139, 132, 160)), port),
|
||||
],
|
||||
"ny.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), 7000)]
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 45, 19)), port)]
|
||||
}
|
||||
"ams.gateway.astralane.io" => vec![
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), 7000),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), 7000),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(64, 130, 43, 43)), port),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 186, 73)), port),
|
||||
],
|
||||
"la.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), 7000)]
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(74, 118, 142, 151)), port)]
|
||||
}
|
||||
"lim.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), 7000)]
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(162, 19, 222, 232)), port)]
|
||||
}
|
||||
"sg.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), 7000)]
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(67, 209, 54, 176)), port)]
|
||||
}
|
||||
"lit.gateway.astralane.io" => {
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), 7000)]
|
||||
}
|
||||
_ => {
|
||||
let _ = port; // keep signature consistent; fall back to DNS below.
|
||||
Vec::new()
|
||||
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,8 +130,8 @@ impl AstralaneQuicClient {
|
||||
/// Generates a self-signed TLS certificate with the API key as the Common Name (CN).
|
||||
pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let candidates = Self::resolve_server_candidates(server_addr)
|
||||
.context("Invalid server address")?;
|
||||
let candidates =
|
||||
Self::resolve_server_candidates(server_addr).context("Invalid server address")?;
|
||||
let addr = candidates[0];
|
||||
|
||||
info!("[astralane-quic] Building TLS config (CN = api_key)");
|
||||
@@ -167,7 +165,8 @@ impl AstralaneQuicClient {
|
||||
}
|
||||
}
|
||||
let connection = connection_opt.ok_or_else(|| {
|
||||
last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to connect to Astralane QUIC server"))
|
||||
last_err
|
||||
.unwrap_or_else(|| anyhow::anyhow!("Failed to connect to Astralane QUIC server"))
|
||||
})?;
|
||||
|
||||
info!("[astralane-quic] Connected at {}", selected_addr);
|
||||
@@ -203,7 +202,8 @@ impl AstralaneQuicClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to reconnect to Astralane QUIC server")))
|
||||
Err(last_err
|
||||
.unwrap_or_else(|| anyhow::anyhow!("Failed to reconnect to Astralane QUIC server")))
|
||||
}
|
||||
|
||||
/// Send a single bincode-serialized `VersionedTransaction`.
|
||||
|
||||
+169
-45
@@ -5,6 +5,7 @@ use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use solana_transaction_status::UiTransactionEncoding;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -17,8 +18,8 @@ use crate::{common::SolanaRpcClient, constants::swqos::BLOCKRAZOR_TIP_ACCOUNTS};
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tokio::task::JoinHandle;
|
||||
use tonic::transport::Channel;
|
||||
use tonic::metadata::AsciiMetadataValue;
|
||||
use tonic::transport::Channel;
|
||||
|
||||
// Include pre-generated gRPC code
|
||||
pub mod serverpb {
|
||||
@@ -45,7 +46,9 @@ impl BlockRazorGrpcClient {
|
||||
let mut request = tonic::Request::new(serverpb::HealthRequest {});
|
||||
request.metadata_mut().insert("apikey", apikey);
|
||||
|
||||
let response = client.get_health(request).await
|
||||
let response = client
|
||||
.get_health(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("gRPC health check failed: {}", e))?;
|
||||
Ok(response.into_inner().status)
|
||||
}
|
||||
@@ -74,7 +77,9 @@ impl BlockRazorGrpcClient {
|
||||
});
|
||||
request.metadata_mut().insert("apikey", apikey);
|
||||
|
||||
let response = client.send_transaction(request).await
|
||||
let response = client
|
||||
.send_transaction(request)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("gRPC send transaction failed: {}", e))?;
|
||||
Ok(response.into_inner().signature)
|
||||
}
|
||||
@@ -85,9 +90,11 @@ pub enum BlockRazorBackend {
|
||||
Grpc {
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
grpc_client: Arc<BlockRazorGrpcClient>,
|
||||
grpc_client: Arc<ArcSwap<BlockRazorGrpcClient>>,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
/// When true, gRPC send_transaction sets revert_protection=true for MEV protection.
|
||||
mev_protection: bool,
|
||||
},
|
||||
Http {
|
||||
endpoint: String,
|
||||
@@ -95,6 +102,8 @@ pub enum BlockRazorBackend {
|
||||
http_client: Client,
|
||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
/// When true, HTTP request adds revertProtection=true query param for MEV protection.
|
||||
mev_protection: bool,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -143,10 +152,15 @@ impl SwqosClientTrait for BlockRazorClient {
|
||||
impl BlockRazorClient {
|
||||
pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
|
||||
// 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR
|
||||
Ok(Self::new_http(rpc_url, endpoint, auth_token))
|
||||
Ok(Self::new_http(rpc_url, endpoint, auth_token, false))
|
||||
}
|
||||
|
||||
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
|
||||
pub async fn new_grpc(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
|
||||
// 配置 Channel,增加连接超时
|
||||
@@ -157,7 +171,8 @@ impl BlockRazorClient {
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
|
||||
|
||||
let grpc_client = Arc::new(BlockRazorGrpcClient::new(channel, auth_token.clone()));
|
||||
let grpc_client =
|
||||
Arc::new(ArcSwap::from_pointee(BlockRazorGrpcClient::new(channel, auth_token.clone())));
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
@@ -169,6 +184,7 @@ impl BlockRazorClient {
|
||||
grpc_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
mev_protection,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -180,7 +196,12 @@ impl BlockRazorClient {
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
||||
pub fn new_http(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
auth_token: String,
|
||||
mev_protection: bool,
|
||||
) -> Self {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let http_client = default_http_client_builder().user_agent("").build().unwrap();
|
||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||
@@ -194,6 +215,7 @@ impl BlockRazorClient {
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
mev_protection,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -211,27 +233,71 @@ impl BlockRazorClient {
|
||||
grpc_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
endpoint,
|
||||
auth_token,
|
||||
..
|
||||
} => {
|
||||
let grpc_client = grpc_client.clone();
|
||||
let ping_handle = ping_handle.clone();
|
||||
let stop_ping = stop_ping.clone();
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = grpc_client.get_health().await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC ping request failed: {}", e);
|
||||
let mut delay = 1u64;
|
||||
|
||||
// 初始健康检查
|
||||
{
|
||||
let client = grpc_client.load();
|
||||
if let Err(e) = client.get_health().await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC initial health check failed: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut interval = tokio::time::interval(Duration::from_secs(30));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = grpc_client.get_health().await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC ping request failed: {}", e);
|
||||
|
||||
// 健康检查(使用 load() 无锁读取)
|
||||
let client = grpc_client.load();
|
||||
match client.get_health().await {
|
||||
Ok(_) => {
|
||||
delay = 1; // 成功,重置延迟
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC health check failed: {} - reconnecting in {}s", e, delay);
|
||||
}
|
||||
|
||||
// 等待指数退避时间
|
||||
tokio::time::sleep(Duration::from_secs(delay)).await;
|
||||
delay = (delay * 2).min(60);
|
||||
|
||||
// 尝试重连
|
||||
match Self::reconnect_grpc(&endpoint, &auth_token).await {
|
||||
Ok(new_client) => {
|
||||
// 使用 swap() 无锁替换客户端
|
||||
grpc_client.swap(Arc::new(new_client));
|
||||
delay = 1; // 重置延迟
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor gRPC reconnected successfully");
|
||||
}
|
||||
}
|
||||
Err(reconnect_err) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
"BlockRazor gRPC reconnect failed: {}",
|
||||
reconnect_err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +315,7 @@ impl BlockRazorClient {
|
||||
http_client,
|
||||
ping_handle,
|
||||
stop_ping,
|
||||
..
|
||||
} => {
|
||||
let endpoint = endpoint.clone();
|
||||
let auth_token = auth_token.clone();
|
||||
@@ -257,7 +324,8 @@ impl BlockRazorClient {
|
||||
let stop_ping = stop_ping.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor HTTP ping request failed: {}", e);
|
||||
}
|
||||
@@ -268,7 +336,9 @@ impl BlockRazorClient {
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_http_ping(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) =
|
||||
Self::send_http_ping(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("BlockRazor HTTP ping request failed: {}", e);
|
||||
}
|
||||
@@ -285,11 +355,7 @@ impl BlockRazorClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_http_ping(
|
||||
http_client: &Client,
|
||||
endpoint: &str,
|
||||
auth_token: &str,
|
||||
) -> Result<()> {
|
||||
async fn send_http_ping(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
|
||||
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
|
||||
let response = http_client
|
||||
.post(&ping_url)
|
||||
@@ -307,6 +373,18 @@ impl BlockRazorClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重新建立 gRPC 连接
|
||||
async fn reconnect_grpc(endpoint: &str, auth_token: &str) -> Result<BlockRazorGrpcClient> {
|
||||
let channel = tonic::transport::Channel::from_shared(endpoint.to_string())
|
||||
.map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))?
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("Failed to reconnect to gRPC endpoint: {}", e))?;
|
||||
|
||||
Ok(BlockRazorGrpcClient::new(channel, auth_token.to_string()))
|
||||
}
|
||||
|
||||
async fn send_transaction_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
@@ -316,52 +394,82 @@ impl BlockRazorClient {
|
||||
let start_time = Instant::now();
|
||||
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc {
|
||||
grpc_client,
|
||||
..
|
||||
} => {
|
||||
BlockRazorBackend::Grpc { grpc_client, mev_protection, .. } => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let signature = grpc_client.send_transaction(
|
||||
content,
|
||||
"fast".to_string(),
|
||||
None,
|
||||
false,
|
||||
).await;
|
||||
// 使用 load() 无锁获取客户端引用
|
||||
let client = grpc_client.load();
|
||||
let signature = client
|
||||
.send_transaction(
|
||||
content,
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revert_protection is unrelated to MEV; keep false.
|
||||
if *mev_protection {
|
||||
"sandwichMitigation".to_string()
|
||||
} else {
|
||||
"fast".to_string()
|
||||
},
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
match signature {
|
||||
Ok(sig) => {
|
||||
if !sig.is_empty() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("BlockRazor", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("BlockRazor", trade_type, start_time.elapsed(), "empty signature".to_string());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"empty signature".to_string(),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("BlockRazor gRPC returned empty signature"));
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor gRPC returned empty signature"
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("BlockRazor", trade_type, start_time.elapsed(), format!("gRPC error: {}", e));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"BlockRazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("gRPC error: {}", e),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("BlockRazor gRPC sendTransaction failed: {}", e));
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor gRPC sendTransaction failed: {}",
|
||||
e
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
BlockRazorBackend::Http {
|
||||
endpoint,
|
||||
auth_token,
|
||||
http_client,
|
||||
..
|
||||
endpoint, auth_token, http_client, mev_protection, ..
|
||||
} => {
|
||||
let (content, _signature) =
|
||||
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
|
||||
|
||||
let query_params: Vec<(&str, &str)> = vec![
|
||||
("auth", auth_token.as_str()),
|
||||
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
|
||||
// revertProtection is unrelated to MEV; not set.
|
||||
("mode", if *mev_protection { "sandwichMitigation" } else { "fast" }),
|
||||
];
|
||||
|
||||
let response = http_client
|
||||
.post(endpoint)
|
||||
.query(&[("auth", auth_token.as_str())])
|
||||
.query(&query_params)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(content)
|
||||
.send()
|
||||
@@ -371,12 +479,21 @@ impl BlockRazorClient {
|
||||
if status.is_success() {
|
||||
let _ = response.bytes().await;
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("blockrazor", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("blockrazor", trade_type, start_time.elapsed(), format!("status {} body: {}", status, body));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {} body: {}", status, body),
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"BlockRazor HTTP sendTransaction failed: status {} body: {}",
|
||||
@@ -408,7 +525,13 @@ impl BlockRazorClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "blockrazor", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"blockrazor",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -418,7 +541,8 @@ impl BlockRazorClient {
|
||||
impl Drop for BlockRazorClient {
|
||||
fn drop(&mut self) {
|
||||
match &self.backend {
|
||||
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. } | BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. }
|
||||
| BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
|
||||
stop_ping.store(true, Ordering::Relaxed);
|
||||
|
||||
let ping_handle = ping_handle.clone();
|
||||
|
||||
+30
-5
@@ -100,13 +100,27 @@ impl BloxrouteClient {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("bloxroute", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [bloxroute] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [bloxroute] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("bloxroute", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -128,7 +142,13 @@ impl BloxrouteClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "bloxroute", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"bloxroute",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -170,7 +190,12 @@ impl BloxrouteClient {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" bloxroute {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" bloxroute {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-4
@@ -97,12 +97,26 @@ impl FlashBlockClient {
|
||||
// Parse response
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("success").is_some() || response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("FlashBlock", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [FlashBlock] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [FlashBlock] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("FlashBlock", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -122,7 +136,13 @@ impl FlashBlockClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "FlashBlock", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"FlashBlock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+28
-5
@@ -107,7 +107,10 @@ impl HeliusClient {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(
|
||||
" [helius] {} submission failed after {:?} status={} body={}",
|
||||
trade_type, start_time.elapsed(), status, response_text
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
status,
|
||||
response_text
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
@@ -124,15 +127,29 @@ impl HeliusClient {
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("unknown");
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("helius", trade_type, start_time.elapsed(), err_msg);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
err_msg,
|
||||
);
|
||||
}
|
||||
return Err(anyhow::anyhow!("Helius Sender error: {}", err_msg));
|
||||
}
|
||||
if response_json.get("result").is_some() && crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("helius", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("helius", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
@@ -152,7 +169,13 @@ impl HeliusClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "helius", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"helius",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+37
-6
@@ -105,12 +105,26 @@ impl JitoClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("jito", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [jito] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [jito] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("jito", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -118,13 +132,25 @@ impl JitoClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "jito", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "jito", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"jito",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -171,7 +197,12 @@ impl JitoClient {
|
||||
if response_json.get("result").is_some() {
|
||||
println!(" jito {} submitted: {:?}", trade_type, start_time.elapsed());
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" jito {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" jito {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-4
@@ -103,12 +103,26 @@ impl LightspeedClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("lightspeed", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("lightspeed", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("lightspeed", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -128,7 +142,13 @@ impl LightspeedClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "lightspeed", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"lightspeed",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+102
-50
@@ -22,24 +22,24 @@ use std::sync::Arc;
|
||||
|
||||
use solana_commitment_config::CommitmentConfig;
|
||||
use solana_sdk::transaction::VersionedTransaction;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
constants::swqos::{
|
||||
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
|
||||
SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL,
|
||||
SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR,
|
||||
SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK,
|
||||
SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED,
|
||||
SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_SOYAS,
|
||||
SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL,
|
||||
SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX,
|
||||
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO,
|
||||
SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||
SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
|
||||
SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
|
||||
SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
|
||||
SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1,
|
||||
SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM,
|
||||
SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
},
|
||||
swqos::{
|
||||
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
|
||||
@@ -51,11 +51,8 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Reserved for future per-SWQOS tip account caching (currently unused).
|
||||
#[allow(dead_code)]
|
||||
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
|
||||
}
|
||||
// Tip 账户:`SwqosClient::get_tip_account()` 在各实现里多为静态常量;同一批多路提交时,
|
||||
// 在 `trading::core::async_executor::execute_parallel` 内用局部 `tip_cache`(按 client 指针)去重解析。
|
||||
|
||||
/// SWQOS provider blacklist configuration
|
||||
/// Providers added here will be disabled even if configured by user
|
||||
@@ -66,7 +63,7 @@ pub const SWQOS_BLACKLIST: &[SwqosType] = &[
|
||||
|
||||
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
|
||||
/// BlockRazor 支持 gRPC 和 HTTP。
|
||||
/// Astralane 和 Node1 支持 QUIC。
|
||||
/// Node1 支持 QUIC。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum SwqosTransport {
|
||||
#[default]
|
||||
@@ -75,6 +72,19 @@ pub enum SwqosTransport {
|
||||
Quic,
|
||||
}
|
||||
|
||||
/// Astralane 三种提交方式:QUIC TPU、Plain HTTP(`/iris`)、Binary HTTP(`/irisb` + bincode)。
|
||||
/// 与全局 [`crate::common::TradeConfig::mev_protection`] 配合:HTTP 加 `mev-protect=true`;QUIC 选 `:9000` / `:7000`。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum AstralaneTransport {
|
||||
/// Binary over HTTP:`…/irisb?api-key=…&method=sendTransaction`(与 `AstralaneClient` 当前序列化一致)。
|
||||
#[default]
|
||||
Binary,
|
||||
/// Plain HTTP:`…/iris?…`(非 irisb 路径)。
|
||||
Plain,
|
||||
/// QUIC(`host:7000`;MEV 时 `host:9000`)。
|
||||
Quic,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum TradeType {
|
||||
Create,
|
||||
@@ -199,15 +209,23 @@ pub trait SwqosClientTrait {
|
||||
}
|
||||
}
|
||||
|
||||
/// 地理区域,用于默认 SWQOS 端点下标(见 `constants::swqos`)。
|
||||
///
|
||||
/// 各服务商常量表在**缺独立 PoP**时,于**已公布的端点集合内**按地理距离选最近项;[`SwqosRegion::Default`] 不表示地球上的位置,表中为全局/枢纽回退,不适用地理就近。
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum SwqosRegion {
|
||||
NewYork,
|
||||
Frankfurt,
|
||||
Amsterdam,
|
||||
/// Ireland (EU); Jito publishes `dublin.mainnet.block-engine.jito.wtf`.
|
||||
Dublin,
|
||||
SLC,
|
||||
Tokyo,
|
||||
/// Southeast Asia (Singapore); not interchangeable with [`SwqosRegion::Tokyo`].
|
||||
Singapore,
|
||||
London,
|
||||
LosAngeles,
|
||||
/// 非地理区域:未指定区域时的回退,对应表中全局 URL 或枢纽,**不按地理距离选取**。
|
||||
Default,
|
||||
}
|
||||
|
||||
@@ -230,8 +248,8 @@ pub enum SwqosConfig {
|
||||
FlashBlock(String, SwqosRegion, Option<String>),
|
||||
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
|
||||
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// Astralane(api_token, region, custom_url, transport). transport=None 表示 Http。
|
||||
Astralane(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`](`/irisb`)。
|
||||
Astralane(String, SwqosRegion, Option<String>, Option<AstralaneTransport>),
|
||||
/// Stellium(api_token, region, custom_url)
|
||||
Stellium(String, SwqosRegion, Option<String>),
|
||||
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
|
||||
@@ -288,7 +306,7 @@ impl SwqosConfig {
|
||||
SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(),
|
||||
SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(),
|
||||
SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(),
|
||||
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string(),
|
||||
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string(),
|
||||
SwqosType::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(),
|
||||
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
|
||||
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
||||
@@ -303,6 +321,7 @@ impl SwqosConfig {
|
||||
region: SwqosRegion,
|
||||
url: Option<String>,
|
||||
transport: Option<SwqosTransport>,
|
||||
_mev_protection: bool,
|
||||
) -> String {
|
||||
if let Some(custom_url) = url {
|
||||
return custom_url;
|
||||
@@ -326,15 +345,7 @@ impl SwqosConfig {
|
||||
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
SwqosType::Astralane => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
_ => Self::get_endpoint(swqos_type, region, url),
|
||||
_ => Self::get_endpoint(swqos_type, region, None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,6 +353,7 @@ impl SwqosConfig {
|
||||
rpc_url: String,
|
||||
commitment: CommitmentConfig,
|
||||
swqos_config: SwqosConfig,
|
||||
mev_protection: bool,
|
||||
) -> Result<Arc<SwqosClient>> {
|
||||
match swqos_config {
|
||||
SwqosConfig::Jito(auth_token, region, url) => {
|
||||
@@ -398,33 +410,73 @@ impl SwqosConfig {
|
||||
SwqosConfig::BlockRazor(auth_token, region, url, transport) => {
|
||||
// BlockRazor: transport=None 或 transport=Grpc 时使用 gRPC,transport=Http 时使用 HTTP
|
||||
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(SwqosType::BlockRazor, region, url, transport);
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::BlockRazor,
|
||||
region,
|
||||
url,
|
||||
transport,
|
||||
mev_protection,
|
||||
);
|
||||
if use_http {
|
||||
let blockrazor_client =
|
||||
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
let blockrazor_client = BlockRazorClient::new_http(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
} else {
|
||||
// 使用 gRPC 模式(默认或用户明确指定了 gRPC)
|
||||
let blockrazor_client =
|
||||
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
|
||||
let blockrazor_client = BlockRazorClient::new_grpc(
|
||||
rpc_url.clone(),
|
||||
endpoint.to_string(),
|
||||
auth_token,
|
||||
mev_protection,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(blockrazor_client))
|
||||
}
|
||||
}
|
||||
SwqosConfig::Astralane(auth_token, region, url, transport) => {
|
||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
||||
if use_quic {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
});
|
||||
let astralane_client =
|
||||
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
||||
.await?;
|
||||
Ok(Arc::new(astralane_client))
|
||||
} else {
|
||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
|
||||
let astralane_client =
|
||||
AstralaneClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
||||
Ok(Arc::new(astralane_client))
|
||||
SwqosConfig::Astralane(auth_token, region, url, mode) => {
|
||||
let mode = mode.unwrap_or_default();
|
||||
match mode {
|
||||
AstralaneTransport::Quic => {
|
||||
let quic_endpoint = url.unwrap_or_else(|| {
|
||||
if mev_protection {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||
}
|
||||
});
|
||||
let astralane_client =
|
||||
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
||||
.await?;
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
AstralaneTransport::Plain => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_PLAIN[region as usize].to_string()
|
||||
});
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
AstralaneTransport::Binary => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string()
|
||||
});
|
||||
let astralane_client = AstralaneClient::new(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
auth_token,
|
||||
mev_protection,
|
||||
);
|
||||
Ok(Arc::new(astralane_client))
|
||||
}
|
||||
}
|
||||
}
|
||||
SwqosConfig::Stellium(auth_token, region, url) => {
|
||||
|
||||
+24
-4
@@ -99,12 +99,26 @@ impl NextBlockClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("nextblock", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nextblock", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nextblock", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -124,7 +138,13 @@ impl NextBlockClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "nextblock", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"nextblock",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+24
-4
@@ -184,13 +184,27 @@ impl Node1Client {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("node1", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
eprintln!(" [node1] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
|
||||
eprintln!(
|
||||
" [node1] {} submission failed after {:?}: {:?}",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("node1", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -212,7 +226,13 @@ impl Node1Client {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "node1", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"node1",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+10
-10
@@ -51,7 +51,7 @@ pub mod server_client {
|
||||
}
|
||||
impl<T> ServerClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||
T: tonic::client::GrpcService<tonic::body::BoxBody>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
@@ -72,13 +72,13 @@ pub mod server_client {
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
||||
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
http::Request<tonic::body::BoxBody>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
ServerClient::new(InterceptedService::new(inner, interceptor))
|
||||
@@ -126,7 +126,7 @@ pub mod server_client {
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/serverpb.Server/SendTransaction",
|
||||
);
|
||||
@@ -147,7 +147,7 @@ pub mod server_client {
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/serverpb.Server/GetHealth",
|
||||
);
|
||||
@@ -244,7 +244,7 @@ pub mod server_server {
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Response = http::Response<tonic::body::BoxBody>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
@@ -283,7 +283,7 @@ pub mod server_server {
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = SendTransactionSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
@@ -326,7 +326,7 @@ pub mod server_server {
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetHealthSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let codec = tonic::codec::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
@@ -344,7 +344,7 @@ pub mod server_server {
|
||||
_ => {
|
||||
Box::pin(async move {
|
||||
let mut response = http::Response::new(
|
||||
tonic::body::Body::default(),
|
||||
tonic::body::BoxBody::default(),
|
||||
);
|
||||
let headers = response.headers_mut();
|
||||
headers
|
||||
|
||||
@@ -7,7 +7,7 @@ use solana_transaction_status::UiTransactionEncoding;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
common::{sdk_log, SolanaRpcClient},
|
||||
swqos::{common::poll_transaction_confirmation, SwqosType, TradeType},
|
||||
};
|
||||
use anyhow::Result;
|
||||
@@ -25,6 +25,7 @@ impl SwqosClientTrait for SolRpcClient {
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let submit_start = Instant::now();
|
||||
let signature = self
|
||||
.rpc_client
|
||||
.send_transaction_with_config(
|
||||
@@ -39,6 +40,8 @@ impl SwqosClientTrait for SolRpcClient {
|
||||
)
|
||||
.await?;
|
||||
|
||||
sdk_log::log_swqos_submitted("Default", trade_type, submit_start.elapsed());
|
||||
|
||||
let start_time = Instant::now();
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
|
||||
+97
-8
@@ -6,9 +6,11 @@ use quinn::{
|
||||
TransportConfig,
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use rcgen::{CertificateParams, KeyPair as RcgenKeyPair};
|
||||
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use solana_client::rpc_client::SerializableTransaction;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
net::{SocketAddr, ToSocketAddrs as _},
|
||||
@@ -25,6 +27,66 @@ use crate::{
|
||||
swqos::{SwqosType, TradeType},
|
||||
};
|
||||
|
||||
// Skip server verification implementation
|
||||
#[derive(Debug)]
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl SkipServerVerification {
|
||||
fn new() -> Arc<Self> {
|
||||
Arc::new(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: rustls::pki_types::UnixTime,
|
||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
||||
_dss: &rustls::DigitallySignedStruct,
|
||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS 客户端证书:ECDSA P-256 + CN=钱包公钥(与 Speedlanding / Astralane QUIC 策略一致)。
|
||||
fn generate_client_tls_credentials(
|
||||
keypair: &Keypair,
|
||||
) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
|
||||
let tls_key = RcgenKeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||
let mut cert_params = CertificateParams::new(vec![])?;
|
||||
cert_params
|
||||
.distinguished_name
|
||||
.push(rcgen::DnType::CommonName, rcgen::DnValue::Utf8String(keypair.pubkey().to_string()));
|
||||
let cert = cert_params.self_signed(&tls_key)?;
|
||||
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(tls_key.serialize_der()));
|
||||
Ok((cert_der, key_der))
|
||||
}
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
const SOYAS_SERVER: &str = "soyas-landing";
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||
@@ -42,8 +104,13 @@ pub struct SoyasClient {
|
||||
impl SoyasClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let keypair_bytes = bs58::decode(api_key.trim()).into_vec().map_err(|e| {
|
||||
anyhow::anyhow!("Soyas api_token base58 解码失败(QUIC mTLS 用): {}", e)
|
||||
})?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Soyas api_token 无法解析为 Solana keypair(QUIC mTLS 用): {}", e)
|
||||
})?;
|
||||
let (cert, key) = generate_client_tls_credentials(&keypair)?;
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
@@ -110,13 +177,23 @@ impl SwqosClientTrait for SoyasClient {
|
||||
let connection = self.connection.load_full();
|
||||
if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Soyas", trade_type, start_time.elapsed(), "reconnecting");
|
||||
}
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"reconnecting",
|
||||
);
|
||||
}
|
||||
self.reconnect().await?;
|
||||
let connection = self.connection.load_full();
|
||||
if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Soyas", trade_type, start_time.elapsed(), &e);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
@@ -130,14 +207,26 @@ impl SwqosClientTrait for SoyasClient {
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "Soyas", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Soyas", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Soyas",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+64
-55
@@ -6,6 +6,7 @@ use quinn::{
|
||||
TransportConfig,
|
||||
};
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use solana_sdk::signer::Signer;
|
||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||
use std::time::Instant;
|
||||
@@ -27,8 +28,8 @@ use crate::{
|
||||
};
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
/// Fallback SNI when endpoint is IP or cannot extract host (keeps legacy behavior).
|
||||
const SPEED_SERVER_FALLBACK: &str = "speed-landing";
|
||||
/// QUIC TLS SNI:与 Speedlanding 官方客户端一致,固定为 `speed-landing`(勿用 PoP 主机名,否则易握手失败)。
|
||||
const SPEED_SERVER: &str = "speed-landing";
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
@@ -39,33 +40,20 @@ pub struct SpeedlandingClient {
|
||||
endpoint: Endpoint,
|
||||
client_config: ClientConfig,
|
||||
addr: SocketAddr,
|
||||
/// TLS SNI: host from endpoint URL so server presents the right cert (e.g. nyc.speedlanding.trade).
|
||||
server_name: String,
|
||||
connection: ArcSwap<Connection>,
|
||||
reconnect: Mutex<()>,
|
||||
}
|
||||
|
||||
impl SpeedlandingClient {
|
||||
/// Extract TLS SNI (host) from endpoint URL. Uses fallback "speed-landing" for IP or when host cannot be determined.
|
||||
fn server_name_from_endpoint(endpoint: &str) -> String {
|
||||
let without_scheme = endpoint
|
||||
.strip_prefix("https://")
|
||||
.or_else(|| endpoint.strip_prefix("http://"))
|
||||
.unwrap_or(endpoint);
|
||||
let host = without_scheme.split(':').next().unwrap_or("").trim();
|
||||
if host.is_empty() {
|
||||
return SPEED_SERVER_FALLBACK.to_string();
|
||||
}
|
||||
if !host.chars().any(|c| c.is_ascii_alphabetic()) {
|
||||
return SPEED_SERVER_FALLBACK.to_string();
|
||||
}
|
||||
host.to_string()
|
||||
}
|
||||
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||
let server_name = Self::server_name_from_endpoint(&endpoint_string);
|
||||
let keypair = Keypair::from_base58_string(&api_key);
|
||||
// Speedlanding QUIC:与官方一致使用 `solana_tls_utils::new_dummy_x509_certificate`(Ed25519 dummy cert)+ SNI `speed-landing`。
|
||||
let keypair_bytes = bs58::decode(api_key.trim()).into_vec().map_err(|e| {
|
||||
anyhow::anyhow!("Speedlanding api_token base58 解码失败(mTLS 用): {}", e)
|
||||
})?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Speedlanding api_token 无法解析为 Solana keypair(mTLS 用): {}", e)
|
||||
})?;
|
||||
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
@@ -89,18 +77,23 @@ impl SpeedlandingClient {
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
|
||||
let connecting = endpoint.connect(addr, &server_name)?;
|
||||
let connecting = endpoint.connect(addr, SPEED_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC connect timeout")?
|
||||
.context("Speedlanding QUIC handshake failed")?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Speedlanding QUIC handshake failed(请确认:1) 机器人登记的身份与钱包公钥 {} 一致 2) 本机 UDP 可访问 {} 3) region 与 PoP 匹配)",
|
||||
keypair.pubkey(),
|
||||
endpoint_string
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
client_config,
|
||||
addr,
|
||||
server_name,
|
||||
connection: ArcSwap::from_pointee(connection),
|
||||
reconnect: Mutex::new(()),
|
||||
})
|
||||
@@ -119,15 +112,17 @@ impl SpeedlandingClient {
|
||||
let _guard = self.reconnect.lock().await;
|
||||
let current = self.connection.load_full();
|
||||
if current.close_reason().is_some() {
|
||||
let connecting = self.endpoint.connect_with(
|
||||
self.client_config.clone(),
|
||||
self.addr,
|
||||
self.server_name.as_str(),
|
||||
)?;
|
||||
let connecting =
|
||||
self.endpoint.connect_with(self.client_config.clone(), self.addr, SPEED_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Speedlanding QUIC reconnect timeout")?
|
||||
.context("Speedlanding QUIC re-handshake failed")?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Speedlanding QUIC re-handshake failed(对端 {} SNI {})",
|
||||
self.addr, SPEED_SERVER
|
||||
)
|
||||
})?;
|
||||
self.connection.store(Arc::new(connection));
|
||||
return Ok(self.connection.load_full());
|
||||
}
|
||||
@@ -160,51 +155,65 @@ impl SwqosClientTrait for SpeedlandingClient {
|
||||
Ok(Err(_)) | Err(_) => true,
|
||||
};
|
||||
if need_retry {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [Speedlanding] {} submission failed after {:?}, reconnecting", trade_type, start_time.elapsed());
|
||||
}
|
||||
eprintln!(
|
||||
" [Speedlanding] {} QUIC 首次发送失败 {:?},正在重试",
|
||||
trade_type,
|
||||
start_time.elapsed()
|
||||
);
|
||||
let connection = self.ensure_connected().await?;
|
||||
send_result =
|
||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
|
||||
}
|
||||
match send_result.context("Speedlanding QUIC send timeout") {
|
||||
Ok(Ok(())) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
|
||||
}
|
||||
// 提交结果与「详细耗时/SDK 开关」无关,便于确认当前通道确实在执行
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), &e);
|
||||
}
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), "timeout");
|
||||
}
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"timeout",
|
||||
);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
println!(" signature: {:?}", signature);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Speedlanding", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Speedlanding",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+24
-4
@@ -168,13 +168,27 @@ impl StelliumClient {
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Stellium", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Stellium", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("Stellium", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -196,7 +210,13 @@ impl StelliumClient {
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "Stellium", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Stellium",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+24
-4
@@ -209,12 +209,26 @@ impl TemporalClient {
|
||||
|
||||
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if response_json.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("nozomi", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(_error) = response_json.get("error") {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nozomi", trade_type, start_time.elapsed(), _error);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
_error,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("nozomi", trade_type, start_time.elapsed(), response_text);
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text,
|
||||
);
|
||||
}
|
||||
|
||||
let start_time: Instant = Instant::now();
|
||||
@@ -232,7 +246,13 @@ impl TemporalClient {
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "nozomi", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"nozomi",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+79
-20
@@ -1,12 +1,10 @@
|
||||
use crate::swqos::common::{
|
||||
default_http_client_builder, poll_transaction_confirmation,
|
||||
};
|
||||
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
|
||||
use bincode;
|
||||
use rand::seq::IndexedRandom;
|
||||
use reqwest::Client;
|
||||
use std::{sync::Arc, time::Instant, time::Duration};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::{sync::Arc, time::Duration, time::Instant};
|
||||
use tokio::task::JoinHandle;
|
||||
use bincode;
|
||||
|
||||
use crate::swqos::SwqosClientTrait;
|
||||
use crate::swqos::{SwqosType, TradeType};
|
||||
@@ -101,7 +99,8 @@ impl ZeroSlotClient {
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
|
||||
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!("0slot ping request failed: {}", e);
|
||||
}
|
||||
@@ -176,41 +175,89 @@ impl ZeroSlotClient {
|
||||
200 => {
|
||||
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&response_text) {
|
||||
if json_value.get("result").is_some() {
|
||||
crate::common::sdk_log::log_swqos_submitted("0slot", trade_type, start_time.elapsed());
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
);
|
||||
} else if let Some(error) = json_value.get("error") {
|
||||
let code = error.get("code")
|
||||
let code = error
|
||||
.get("code")
|
||||
.and_then(|c| c.as_i64())
|
||||
.map(|c| c.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let message = error.get("message")
|
||||
let message = error
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.unwrap_or("unknown error");
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("code {}: {}", code, message));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("code {}: {}", code, message),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx error: {}", message));
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("unexpected JSON: {}", response_text));
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx unexpected JSON: {}", response_text));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("unexpected JSON: {}", response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"0slot Binary-Tx unexpected JSON: {}",
|
||||
response_text
|
||||
));
|
||||
}
|
||||
} else {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("invalid JSON: {}", response_text));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("invalid JSON: {}", response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx invalid JSON: {}", response_text));
|
||||
}
|
||||
}
|
||||
403 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text.clone());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text.clone(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot API key error: {}", response_text));
|
||||
}
|
||||
419 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text.clone());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
response_text.clone(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot rate limit exceeded"));
|
||||
}
|
||||
500 => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), "submission failed".to_string());
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"submission failed".to_string(),
|
||||
);
|
||||
return Err(anyhow::anyhow!("0slot transaction submission failed"));
|
||||
}
|
||||
_ => {
|
||||
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), format!("status {} body: {}", status, response_text));
|
||||
return Err(anyhow::anyhow!("0slot Binary-Tx failed with status {}: {}", status, response_text));
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
format!("status {} body: {}", status, response_text),
|
||||
);
|
||||
return Err(anyhow::anyhow!(
|
||||
"0slot Binary-Tx failed with status {}: {}",
|
||||
status,
|
||||
response_text
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,13 +269,25 @@ impl ZeroSlotClient {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmation failed: {:?}", "0slot", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(" [{:width$}] {} confirmed: {:?}", "0slot", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"0slot",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
||||
signature::Keypair, signer::Signer, transaction::VersionedTransaction,
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signer::Signer,
|
||||
transaction::VersionedTransaction,
|
||||
};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
|
||||
@@ -51,7 +52,7 @@ pub async fn build_transaction(
|
||||
|
||||
if with_tip && tip_amount > 0.0 {
|
||||
let tip_lamports = sol_f64_to_lamports(tip_amount);
|
||||
instructions.push(transfer(&payer.pubkey(), tip_account, tip_lamports));
|
||||
instructions.push(system_instruction::transfer(&payer.pubkey(), tip_account, tip_lamports));
|
||||
}
|
||||
|
||||
super::compute_budget_manager::extend_compute_budget_instructions(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
|
||||
use crate::common::{
|
||||
fast_fn::{
|
||||
@@ -88,7 +88,8 @@ pub async fn transfer_sol(
|
||||
return Err(anyhow!("Insufficient balance"));
|
||||
}
|
||||
|
||||
let transfer_instruction = transfer(&payer.pubkey(), receive_wallet, amount);
|
||||
let transfer_instruction =
|
||||
system_instruction::transfer(&payer.pubkey(), receive_wallet, amount);
|
||||
|
||||
let recent_blockhash = rpc.get_latest_blockhash().await?;
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::common::{
|
||||
spl_token::close_account,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use solana_sdk::{instruction::Instruction, message::AccountMeta, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction::transfer;
|
||||
use solana_sdk::{instruction::AccountMeta, instruction::Instruction, pubkey::Pubkey};
|
||||
use solana_system_interface::instruction as system_instruction;
|
||||
|
||||
#[inline]
|
||||
pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]> {
|
||||
@@ -27,7 +27,7 @@ pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]>
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
));
|
||||
insts.extend([
|
||||
transfer(&payer, &wsol_token_account, amount_in),
|
||||
system_instruction::transfer(&payer, &wsol_token_account, amount_in),
|
||||
// sync_native
|
||||
Instruction {
|
||||
program_id: crate::constants::TOKEN_PROGRAM,
|
||||
@@ -91,7 +91,7 @@ pub fn wrap_sol_only(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 2
|
||||
|
||||
let mut insts = SmallVec::<[Instruction; 2]>::new();
|
||||
insts.extend([
|
||||
transfer(&payer, &wsol_token_account, amount_in),
|
||||
system_instruction::transfer(&payer, &wsol_token_account, amount_in),
|
||||
// sync_native
|
||||
Instruction {
|
||||
program_id: crate::constants::TOKEN_PROGRAM,
|
||||
|
||||
@@ -14,16 +14,20 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::message::AddressLookupTableAccount;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::BuildHasherDefault;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::{str::FromStr, sync::Arc, time::Instant};
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use fnv::FnvHasher;
|
||||
@@ -183,11 +187,7 @@ fn ensure_dedicated_pool(
|
||||
.map(|all_ids| {
|
||||
sender_thread_cores
|
||||
.map(|indices| {
|
||||
indices
|
||||
.iter()
|
||||
.take(n)
|
||||
.filter_map(|&i| all_ids.get(i).cloned())
|
||||
.collect()
|
||||
indices.iter().take(n).filter_map(|&i| all_ids.get(i).cloned()).collect()
|
||||
})
|
||||
.unwrap_or_else(|| all_ids.into_iter().take(n).collect())
|
||||
})
|
||||
@@ -402,14 +402,30 @@ impl ResultCollector {
|
||||
timeout_secs: u64,
|
||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
let start = Instant::now();
|
||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
||||
let poll_interval = std::time::Duration::from_millis(2);
|
||||
let primary = Duration::from_secs(timeout_secs);
|
||||
let poll_interval = Duration::from_millis(2);
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
if start.elapsed() > timeout {
|
||||
if start.elapsed() > primary {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(poll_interval).await;
|
||||
}
|
||||
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
|
||||
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
|
||||
let all_submitted = self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
|
||||
if all_submitted {
|
||||
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
|
||||
tokio::time::sleep(Duration::from_millis(35)).await;
|
||||
} else {
|
||||
tokio::time::sleep(Duration::from_millis(600)).await;
|
||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||
if start.elapsed() > primary + Duration::from_secs(6) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(120)).await;
|
||||
}
|
||||
self.get_first()
|
||||
}
|
||||
}
|
||||
@@ -451,11 +467,8 @@ pub async fn execute_parallel(
|
||||
let instructions = Arc::new(instructions);
|
||||
|
||||
// One get_strategies call per batch (avoid N calls in loop).
|
||||
let gas_fee_configs = gas_fee_strategy.get_strategies(if is_buy {
|
||||
TradeType::Buy
|
||||
} else {
|
||||
TradeType::Sell
|
||||
});
|
||||
let gas_fee_configs =
|
||||
gas_fee_strategy.get_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
|
||||
let mut task_configs = Vec::with_capacity(swqos_clients.len() * 3);
|
||||
for (i, swqos_client) in swqos_clients.iter().enumerate() {
|
||||
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
|
||||
@@ -492,7 +505,11 @@ pub async fn execute_parallel(
|
||||
}
|
||||
|
||||
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
|
||||
let collector = Arc::new(ResultCollector::new(task_configs.len()));
|
||||
let channel_count = task_configs.len().max(1);
|
||||
let collector = Arc::new(ResultCollector::new(channel_count));
|
||||
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
|
||||
let submit_timeout_secs: u64 =
|
||||
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
|
||||
let shared = Arc::new(SwqosSharedContext {
|
||||
payer,
|
||||
instructions,
|
||||
@@ -562,11 +579,12 @@ pub async fn execute_parallel(
|
||||
// All jobs enqueued (no spawn on hot path)
|
||||
|
||||
if !wait_transaction_confirmed {
|
||||
const SUBMIT_TIMEOUT_SECS: u64 = 30;
|
||||
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
|
||||
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
|
||||
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
|
||||
let ret = collector.wait_for_all_submitted(submit_timeout_secs).await.unwrap_or((
|
||||
false,
|
||||
vec![],
|
||||
Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)),
|
||||
Some(anyhow!("No SWQOS result within grace window (primary {}s)", submit_timeout_secs)),
|
||||
vec![],
|
||||
));
|
||||
let (success, signatures, last_error, submit_timings) = ret;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use anyhow::Result;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
|
||||
signature::Keypair, signature::Signature,
|
||||
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
@@ -12,6 +12,7 @@ use std::{
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
use super::{params::SwapParams, traits::InstructionBuilder};
|
||||
use crate::swqos::SwqosType;
|
||||
use crate::swqos::TradeType;
|
||||
use crate::{
|
||||
common::{nonce_cache::DurableNonceInfo, GasFeeStrategy, SolanaRpcClient},
|
||||
@@ -55,7 +56,7 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
|
||||
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
|
||||
let timing_start_us: Option<i64> = if params.log_enabled {
|
||||
@@ -215,7 +216,9 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
} else {
|
||||
(ok, signatures, err)
|
||||
};
|
||||
Ok(confirm_result)
|
||||
|
||||
//就是把confirm_result 拆开 再加上 submit_timings
|
||||
Ok((confirm_result.0, confirm_result.1, confirm_result.2, submit_timings))
|
||||
} else {
|
||||
// Not waiting for confirmation: confirmed is not measured (-); total is per-channel submit time only.
|
||||
if log_enabled {
|
||||
@@ -229,7 +232,8 @@ impl TradeExecutor for GenericTradeExecutor {
|
||||
None,
|
||||
);
|
||||
}
|
||||
Ok((ok, signatures, err))
|
||||
|
||||
Ok((ok, signatures, err, submit_timings))
|
||||
};
|
||||
|
||||
result
|
||||
@@ -254,7 +258,7 @@ async fn simulate_transaction(
|
||||
is_buy: bool,
|
||||
with_tip: bool,
|
||||
gas_fee_strategy: GasFeeStrategy,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||
use crate::trading::common::build_transaction;
|
||||
use solana_client::rpc_config::RpcSimulateTransactionConfig;
|
||||
use solana_commitment_config::CommitmentLevel;
|
||||
@@ -330,7 +334,7 @@ async fn simulate_transaction(
|
||||
trace!(target: "sol_trade_sdk", "Compute Units Consumed: {}", units_consumed);
|
||||
}
|
||||
}
|
||||
return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err))));
|
||||
return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err)), Vec::new()));
|
||||
}
|
||||
|
||||
// Simulation succeeded
|
||||
@@ -345,7 +349,7 @@ async fn simulate_transaction(
|
||||
}
|
||||
}
|
||||
|
||||
Ok((true, vec![signature], None))
|
||||
Ok((true, vec![signature], None, Vec::new()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -360,8 +364,20 @@ mod tests {
|
||||
let before_submit_ms = 15.67;
|
||||
let w = 12usize; // same as crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
println!("\n--- 1. 构建指令耗时 / 提交前耗时(各打印一次,统一 ms,保留 4 位小数)---\n");
|
||||
println!(" [SDK][{:width$}] {} build_instructions: {:.4} ms", "-", dir, build_ms, width = w);
|
||||
println!(" [SDK][{:width$}] {} before_submit: {:.4} ms", "-", dir, before_submit_ms, width = w);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
build_ms,
|
||||
width = w
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
before_submit_ms,
|
||||
width = w
|
||||
);
|
||||
|
||||
println!("\n--- 2. 每个 SWQOS 独立耗时:submit_done=起点→该通道提交完成, confirmed=该通道提交→链上确认, total=起点→链上确认 ---\n");
|
||||
for (swqos_type, submit_ms, confirmed_ms, total_ms) in [
|
||||
@@ -380,7 +396,9 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n");
|
||||
println!(
|
||||
"\n--- 3. 不等待链上确认时:每行 total = 该通道 submit_done(提交完成总耗时)---\n"
|
||||
);
|
||||
for (swqos_type, submit_ms, total_ms) in
|
||||
[(SwqosType::Jito, 44.20, 44.20), (SwqosType::Helius, 51.80, 51.80)]
|
||||
{
|
||||
@@ -395,8 +413,20 @@ mod tests {
|
||||
}
|
||||
|
||||
println!("\n--- 4. Simulate 模式(build/before_submit 仍从 grpc_recv_us 起算)---\n");
|
||||
println!(" [SDK][{:width$}] {} build_instructions: {:.4} ms", "-", dir, build_ms, width = w);
|
||||
println!(" [SDK][{:width$}] {} before_submit: {:.4} ms", "-", dir, before_submit_ms, width = w);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} build_instructions: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
build_ms,
|
||||
width = w
|
||||
);
|
||||
println!(
|
||||
" [SDK][{:width$}] {} before_submit: {:.4} ms",
|
||||
"-",
|
||||
dir,
|
||||
before_submit_ms,
|
||||
width = w
|
||||
);
|
||||
println!(" [SDK][{:width$}] {} simulate (dry-run): {:.4} ms", "-", dir, 8.50, width = w);
|
||||
println!(" [SDK][{:width$}] {} total: {:.4} ms", "-", dir, 36.51, width = w);
|
||||
println!();
|
||||
|
||||
@@ -1,818 +0,0 @@
|
||||
use crate::common::bonding_curve::BondingCurveAccount;
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||
use crate::constants::TOKEN_PROGRAM;
|
||||
use core_affinity::CoreId;
|
||||
use crate::instruction::utils::pumpfun::global_constants::MAYHEM_FEE_RECIPIENT;
|
||||
|
||||
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
|
||||
#[derive(Clone)]
|
||||
pub struct SenderConcurrencyConfig {
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
pub max_sender_concurrency: usize,
|
||||
}
|
||||
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
use crate::swqos::{SwqosClient, TradeType};
|
||||
use crate::trading::common::get_multi_token_balances;
|
||||
use crate::trading::MiddlewareManager;
|
||||
use solana_hash::Hash;
|
||||
use solana_sdk::message::AddressLookupTableAccount;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
||||
#[derive(Clone)]
|
||||
pub enum DexParamEnum {
|
||||
PumpFun(PumpFunParams),
|
||||
PumpSwap(PumpSwapParams),
|
||||
Bonk(BonkParams),
|
||||
RaydiumCpmm(RaydiumCpmmParams),
|
||||
RaydiumAmmV4(RaydiumAmmV4Params),
|
||||
MeteoraDammV2(MeteoraDammV2Params),
|
||||
}
|
||||
|
||||
impl DexParamEnum {
|
||||
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
||||
#[inline]
|
||||
pub fn as_any(&self) -> &dyn std::any::Any {
|
||||
match self {
|
||||
DexParamEnum::PumpFun(p) => p,
|
||||
DexParamEnum::PumpSwap(p) => p,
|
||||
DexParamEnum::Bonk(p) => p,
|
||||
DexParamEnum::RaydiumCpmm(p) => p,
|
||||
DexParamEnum::RaydiumAmmV4(p) => p,
|
||||
DexParamEnum::MeteoraDammV2(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap parameters
|
||||
#[derive(Clone)]
|
||||
pub struct SwapParams {
|
||||
pub rpc: Option<Arc<SolanaRpcClient>>,
|
||||
pub payer: Arc<Keypair>,
|
||||
pub trade_type: TradeType,
|
||||
pub input_mint: Pubkey,
|
||||
pub input_token_program: Option<Pubkey>,
|
||||
pub output_mint: Pubkey,
|
||||
pub output_token_program: Option<Pubkey>,
|
||||
pub input_amount: Option<u64>,
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
pub wait_tx_confirmed: bool,
|
||||
pub protocol_params: DexParamEnum,
|
||||
pub open_seed_optimize: bool,
|
||||
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
|
||||
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
pub with_tip: bool,
|
||||
pub create_input_mint_ata: bool,
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
pub simulate: bool,
|
||||
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
||||
pub log_enabled: bool,
|
||||
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
|
||||
pub use_dedicated_sender_threads: bool,
|
||||
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
|
||||
pub max_sender_concurrency: usize,
|
||||
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||
pub use_exact_sol_amount: Option<bool>,
|
||||
}
|
||||
|
||||
impl SwapParams {
|
||||
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
|
||||
#[inline]
|
||||
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
|
||||
SenderConcurrencyConfig {
|
||||
sender_thread_cores: self.sender_thread_cores.clone(),
|
||||
effective_core_ids: self.effective_core_ids.clone(),
|
||||
max_sender_concurrency: self.max_sender_concurrency,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SwapParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SwapParams: ...")
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator Rewards Sharing**: Some coins use a dynamic `creator_vault` (fee-sharing config).
|
||||
/// Always use the latest on-chain creator/vault when building params for **sell**; do not reuse
|
||||
/// cached params from buy. Either fetch fresh data via RPC, or pass `creator_vault` from gRPC
|
||||
/// using [`from_trade`](PumpFunParams::from_trade) / [`from_dev_trade`](PumpFunParams::from_dev_trade),
|
||||
/// or override with [`with_creator_vault`](PumpFunParams::with_creator_vault).
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// Creator vault PDA. For Creator Rewards Sharing coins this can change; pass latest from gRPC when selling.
|
||||
pub creator_vault: Pubkey,
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
pub fn immediate_sell(
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
close_token_account_when_sell: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
creator_vault: creator_vault,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
pub fn from_dev_trade(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
creator: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
bonding_curve,
|
||||
&mint,
|
||||
token_amount,
|
||||
max_sol_cost,
|
||||
creator,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
mint: Pubkey,
|
||||
creator: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
virtual_token_reserves: u64,
|
||||
virtual_sol_reserves: u64,
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
bonding_curve,
|
||||
mint,
|
||||
creator,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account =
|
||||
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
||||
let mint_account = rpc.get_account(&mint).await?;
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: account.1,
|
||||
virtual_token_reserves: account.0.virtual_token_reserves,
|
||||
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
||||
real_token_reserves: account.0.real_token_reserves,
|
||||
real_sol_reserves: account.0.real_sol_reserves,
|
||||
token_total_supply: account.0.token_total_supply,
|
||||
complete: account.0.complete,
|
||||
creator: account.0.creator,
|
||||
is_mayhem_mode: account.0.is_mayhem_mode,
|
||||
is_cashback_coin: account.0.is_cashback_coin,
|
||||
};
|
||||
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
||||
&bonding_curve.account,
|
||||
mint,
|
||||
&mint_account.owner,
|
||||
);
|
||||
let creator_vault =
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator);
|
||||
Ok(Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
creator_vault: creator_vault.unwrap(),
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
})
|
||||
}
|
||||
|
||||
/// Override `creator_vault` with a value from gRPC/event (e.g. for Creator Rewards Sharing).
|
||||
/// Use when selling so the instruction uses the latest on-chain vault and avoids "seeds constraint violated" (2006).
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
/// token configuration, and transaction amounts.
|
||||
///
|
||||
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
||||
/// retrieve the relevant information from RPC, which will increase transaction time.
|
||||
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpSwapParams {
|
||||
/// Liquidity pool address
|
||||
pub pool: Pubkey,
|
||||
/// Base token mint address
|
||||
/// The mint account address of the base token in the trading pair
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
||||
pub quote_mint: Pubkey,
|
||||
/// Pool base token account
|
||||
pub pool_base_token_account: Pubkey,
|
||||
/// Pool quote token account
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
/// Token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Whether the pool is in mayhem mode
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Whether the pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
}
|
||||
|
||||
impl PumpSwapParams {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
Self {
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
||||
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
||||
/// lookups and associated latency. Token program IDs should be sourced from
|
||||
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
||||
/// differences.
|
||||
///
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
||||
/// from the event so that buy/sell instructions include the correct remaining
|
||||
/// accounts for cashback.
|
||||
pub fn from_trade(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
fee_recipient,
|
||||
is_cashback_coin,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(&pool_data, rpc).await?;
|
||||
let creator = pool_data.coin_creator;
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
&pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
&pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
base_mint: pool_data.base_mint,
|
||||
quote_mint: pool_data.quote_mint,
|
||||
pool_base_token_account: pool_data.pool_base_token_account,
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves: pool_base_token_reserves,
|
||||
pool_quote_token_reserves: pool_quote_token_reserves,
|
||||
coin_creator_vault_ata: coin_creator_vault_ata,
|
||||
coin_creator_vault_authority: coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_cashback_coin: pool_data.is_cashback_coin,
|
||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Bonk protocol specific parameters
|
||||
/// Configuration parameters specific to Bonk trading protocol
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BonkParams {
|
||||
pub virtual_base: u128,
|
||||
pub virtual_quote: u128,
|
||||
pub real_base: u128,
|
||||
pub real_quote: u128,
|
||||
pub pool_state: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
/// Token program ID
|
||||
pub mint_token_program: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub platform_associated_account: Pubkey,
|
||||
pub creator_associated_account: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
}
|
||||
|
||||
impl BonkParams {
|
||||
pub fn immediate_sell(
|
||||
mint_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
mint_token_program,
|
||||
platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_trade(
|
||||
virtual_base: u64,
|
||||
virtual_quote: u64,
|
||||
real_base_after: u64,
|
||||
real_quote_after: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
virtual_base: virtual_base as u128,
|
||||
virtual_quote: virtual_quote as u128,
|
||||
real_base: real_base_after as u128,
|
||||
real_quote: real_quote_after as u128,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dev_trade(
|
||||
is_exact_in: bool,
|
||||
amount_in: u64,
|
||||
amount_out: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||
let _amount_in = if is_exact_in {
|
||||
amount_in
|
||||
} else {
|
||||
crate::instruction::utils::bonk::get_amount_in(
|
||||
amount_out,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
) as u128;
|
||||
let _amount_out = if is_exact_in {
|
||||
crate::instruction::utils::bonk::get_amount_out(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
) as u128
|
||||
} else {
|
||||
amount_out as u128
|
||||
};
|
||||
let real_base = _amount_out;
|
||||
Self {
|
||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||
real_base: real_base,
|
||||
real_quote: real_quote,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
usd1_pool: bool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||
mint,
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pool_data =
|
||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||
let platform_associated_account =
|
||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||
&pool_data.platform_config,
|
||||
);
|
||||
let creator_associated_account =
|
||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||
let platform_associated_account = platform_associated_account.unwrap();
|
||||
let creator_associated_account = creator_associated_account.unwrap();
|
||||
Ok(Self {
|
||||
virtual_base: pool_data.virtual_base as u128,
|
||||
virtual_quote: pool_data.virtual_quote as u128,
|
||||
real_base: pool_data.real_base as u128,
|
||||
real_quote: pool_data.real_quote as u128,
|
||||
pool_state: pool_address,
|
||||
base_vault: pool_data.base_vault,
|
||||
quote_vault: pool_data.quote_vault,
|
||||
mint_token_program: token_account.owner,
|
||||
platform_config: pool_data.platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config: pool_data.global_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumCpmmParams {
|
||||
/// Pool address
|
||||
pub pool_state: Pubkey,
|
||||
/// Amm config address
|
||||
pub amm_config: Pubkey,
|
||||
/// Base token mint address
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
pub quote_mint: Pubkey,
|
||||
/// Base token reserve amount in the pool
|
||||
pub base_reserve: u64,
|
||||
/// Quote token reserve amount in the pool
|
||||
pub quote_reserve: u64,
|
||||
/// Base token vault address
|
||||
pub base_vault: Pubkey,
|
||||
/// Quote token vault address
|
||||
pub quote_vault: Pubkey,
|
||||
/// Base token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Observation state account
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
impl RaydiumCpmmParams {
|
||||
pub fn from_trade(
|
||||
pool_state: Pubkey,
|
||||
amm_config: Pubkey,
|
||||
input_token_mint: Pubkey,
|
||||
output_token_mint: Pubkey,
|
||||
input_vault: Pubkey,
|
||||
output_vault: Pubkey,
|
||||
input_token_program: Pubkey,
|
||||
output_token_program: Pubkey,
|
||||
observation_state: Pubkey,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool_state: pool_state,
|
||||
amm_config: amm_config,
|
||||
base_mint: input_token_mint,
|
||||
quote_mint: output_token_mint,
|
||||
base_reserve: base_reserve,
|
||||
quote_reserve: quote_reserve,
|
||||
base_vault: input_vault,
|
||||
quote_vault: output_vault,
|
||||
base_token_program: input_token_program,
|
||||
quote_token_program: output_token_program,
|
||||
observation_state: observation_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool =
|
||||
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
||||
let (token0_balance, token1_balance) =
|
||||
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
||||
rpc,
|
||||
pool_address,
|
||||
&pool.token0_mint,
|
||||
&pool.token1_mint,
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
pool_state: *pool_address,
|
||||
amm_config: pool.amm_config,
|
||||
base_mint: pool.token0_mint,
|
||||
quote_mint: pool.token1_mint,
|
||||
base_reserve: token0_balance,
|
||||
quote_reserve: token1_balance,
|
||||
base_vault: pool.token0_vault,
|
||||
quote_vault: pool.token1_vault,
|
||||
base_token_program: pool.token0_program,
|
||||
quote_token_program: pool.token1_program,
|
||||
observation_state: pool.observation_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumAmmV4Params {
|
||||
/// AMM pool address
|
||||
pub amm: Pubkey,
|
||||
/// Base token (coin) mint address
|
||||
pub coin_mint: Pubkey,
|
||||
/// Quote token (pc) mint address
|
||||
pub pc_mint: Pubkey,
|
||||
/// Pool's coin token account address
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
pub pc_reserve: u64,
|
||||
}
|
||||
|
||||
impl RaydiumAmmV4Params {
|
||||
pub fn new(
|
||||
amm: Pubkey,
|
||||
coin_mint: Pubkey,
|
||||
pc_mint: Pubkey,
|
||||
token_coin: Pubkey,
|
||||
token_pc: Pubkey,
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
}
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
amm,
|
||||
coin_mint: amm_info.coin_mint,
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// MeteoraDammV2 protocol specific parameters
|
||||
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct MeteoraDammV2Params {
|
||||
pub pool: Pubkey,
|
||||
pub token_a_vault: Pubkey,
|
||||
pub token_b_vault: Pubkey,
|
||||
pub token_a_mint: Pubkey,
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
token_a_vault: Pubkey,
|
||||
token_b_vault: Pubkey,
|
||||
token_a_mint: Pubkey,
|
||||
token_b_mint: Pubkey,
|
||||
token_a_program: Pubkey,
|
||||
token_b_program: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
token_a_vault,
|
||||
token_b_vault,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data =
|
||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
token_a_vault: pool_data.token_a_vault,
|
||||
token_b_vault: pool_data.token_b_vault,
|
||||
token_a_mint: pool_data.token_a_mint,
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program: TOKEN_PROGRAM,
|
||||
token_b_program: TOKEN_PROGRAM,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Bonk protocol specific parameters
|
||||
/// Configuration parameters specific to Bonk trading protocol
|
||||
#[derive(Clone, Default)]
|
||||
pub struct BonkParams {
|
||||
pub virtual_base: u128,
|
||||
pub virtual_quote: u128,
|
||||
pub real_base: u128,
|
||||
pub real_quote: u128,
|
||||
pub pool_state: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
/// Token program ID
|
||||
pub mint_token_program: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub platform_associated_account: Pubkey,
|
||||
pub creator_associated_account: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
}
|
||||
|
||||
impl BonkParams {
|
||||
pub fn immediate_sell(
|
||||
mint_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
mint_token_program,
|
||||
platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
pub fn from_trade(
|
||||
virtual_base: u64,
|
||||
virtual_quote: u64,
|
||||
real_base_after: u64,
|
||||
real_quote_after: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
virtual_base: virtual_base as u128,
|
||||
virtual_quote: virtual_quote as u128,
|
||||
real_base: real_base_after as u128,
|
||||
real_quote: real_quote_after as u128,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_dev_trade(
|
||||
is_exact_in: bool,
|
||||
amount_in: u64,
|
||||
amount_out: u64,
|
||||
pool_state: Pubkey,
|
||||
base_vault: Pubkey,
|
||||
quote_vault: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
platform_config: Pubkey,
|
||||
platform_associated_account: Pubkey,
|
||||
creator_associated_account: Pubkey,
|
||||
global_config: Pubkey,
|
||||
) -> Self {
|
||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||
let _amount_in = if is_exact_in {
|
||||
amount_in
|
||||
} else {
|
||||
crate::instruction::utils::bonk::get_amount_in(
|
||||
amount_out,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
};
|
||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
) as u128;
|
||||
let _amount_out = if is_exact_in {
|
||||
crate::instruction::utils::bonk::get_amount_out(
|
||||
amount_in,
|
||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||
DEFAULT_VIRTUAL_BASE,
|
||||
DEFAULT_VIRTUAL_QUOTE,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
) as u128
|
||||
} else {
|
||||
amount_out as u128
|
||||
};
|
||||
let real_base = _amount_out;
|
||||
Self {
|
||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||
real_base: real_base,
|
||||
real_quote: real_quote,
|
||||
pool_state: pool_state,
|
||||
base_vault: base_vault,
|
||||
quote_vault: quote_vault,
|
||||
mint_token_program: base_token_program,
|
||||
platform_config: platform_config,
|
||||
platform_associated_account: platform_associated_account,
|
||||
creator_associated_account: creator_associated_account,
|
||||
global_config: global_config,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
usd1_pool: bool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||
mint,
|
||||
if usd1_pool {
|
||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||
} else {
|
||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let pool_data =
|
||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||
let platform_associated_account =
|
||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||
&pool_data.platform_config,
|
||||
);
|
||||
let creator_associated_account =
|
||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||
let platform_associated_account = platform_associated_account.unwrap();
|
||||
let creator_associated_account = creator_associated_account.unwrap();
|
||||
Ok(Self {
|
||||
virtual_base: pool_data.virtual_base as u128,
|
||||
virtual_quote: pool_data.virtual_quote as u128,
|
||||
real_base: pool_data.real_base as u128,
|
||||
real_quote: pool_data.real_quote as u128,
|
||||
pool_state: pool_address,
|
||||
base_vault: pool_data.base_vault,
|
||||
quote_vault: pool_data.quote_vault,
|
||||
mint_token_program: token_account.owner,
|
||||
platform_config: pool_data.platform_config,
|
||||
platform_associated_account,
|
||||
creator_associated_account,
|
||||
global_config: pool_data.global_config,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
use crate::common::nonce_cache::DurableNonceInfo;
|
||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||
use crate::swqos::{SwqosClient, TradeType};
|
||||
use crate::trading::MiddlewareManager;
|
||||
use core_affinity::CoreId;
|
||||
use solana_hash::Hash;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::bonk::BonkParams;
|
||||
use super::meteora_damm_v2::MeteoraDammV2Params;
|
||||
use super::pumpfun::PumpFunParams;
|
||||
use super::pumpswap::PumpSwapParams;
|
||||
use super::raydium_amm_v4::RaydiumAmmV4Params;
|
||||
use super::raydium_cpmm::RaydiumCpmmParams;
|
||||
|
||||
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
|
||||
#[derive(Clone)]
|
||||
pub struct SenderConcurrencyConfig {
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
pub max_sender_concurrency: usize,
|
||||
}
|
||||
|
||||
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
||||
#[derive(Clone)]
|
||||
pub enum DexParamEnum {
|
||||
PumpFun(PumpFunParams),
|
||||
PumpSwap(PumpSwapParams),
|
||||
Bonk(BonkParams),
|
||||
RaydiumCpmm(RaydiumCpmmParams),
|
||||
RaydiumAmmV4(RaydiumAmmV4Params),
|
||||
MeteoraDammV2(MeteoraDammV2Params),
|
||||
}
|
||||
|
||||
impl DexParamEnum {
|
||||
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
||||
#[inline]
|
||||
pub fn as_any(&self) -> &dyn std::any::Any {
|
||||
match self {
|
||||
DexParamEnum::PumpFun(p) => p,
|
||||
DexParamEnum::PumpSwap(p) => p,
|
||||
DexParamEnum::Bonk(p) => p,
|
||||
DexParamEnum::RaydiumCpmm(p) => p,
|
||||
DexParamEnum::RaydiumAmmV4(p) => p,
|
||||
DexParamEnum::MeteoraDammV2(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap parameters
|
||||
#[derive(Clone)]
|
||||
pub struct SwapParams {
|
||||
pub rpc: Option<Arc<SolanaRpcClient>>,
|
||||
pub payer: Arc<Keypair>,
|
||||
pub trade_type: TradeType,
|
||||
pub input_mint: Pubkey,
|
||||
pub input_token_program: Option<Pubkey>,
|
||||
pub output_mint: Pubkey,
|
||||
pub output_token_program: Option<Pubkey>,
|
||||
pub input_amount: Option<u64>,
|
||||
pub slippage_basis_points: Option<u64>,
|
||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||
pub recent_blockhash: Option<Hash>,
|
||||
pub wait_tx_confirmed: bool,
|
||||
pub protocol_params: DexParamEnum,
|
||||
pub open_seed_optimize: bool,
|
||||
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
|
||||
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||
pub durable_nonce: Option<DurableNonceInfo>,
|
||||
pub with_tip: bool,
|
||||
pub create_input_mint_ata: bool,
|
||||
pub close_input_mint_ata: bool,
|
||||
pub create_output_mint_ata: bool,
|
||||
pub close_output_mint_ata: bool,
|
||||
pub fixed_output_amount: Option<u64>,
|
||||
pub gas_fee_strategy: GasFeeStrategy,
|
||||
pub simulate: bool,
|
||||
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
||||
pub log_enabled: bool,
|
||||
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
|
||||
pub use_dedicated_sender_threads: bool,
|
||||
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
|
||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
|
||||
pub max_sender_concurrency: usize,
|
||||
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
|
||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
||||
pub check_min_tip: bool,
|
||||
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||
pub grpc_recv_us: Option<i64>,
|
||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||
pub use_exact_sol_amount: Option<bool>,
|
||||
/// Use PumpFun V2 instructions (buy_v2 / sell_v2 / buy_exact_quote_in_v2, 27/26-account metas, quote_mint support).
|
||||
/// Default: `false` keeps legacy SOL-paired instructions for smaller transactions; V2 is the official future-proof interface.
|
||||
pub use_pumpfun_v2: bool,
|
||||
}
|
||||
|
||||
impl SwapParams {
|
||||
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
|
||||
#[inline]
|
||||
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
|
||||
SenderConcurrencyConfig {
|
||||
sender_thread_cores: self.sender_thread_cores.clone(),
|
||||
effective_core_ids: self.effective_core_ids.clone(),
|
||||
max_sender_concurrency: self.max_sender_concurrency,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SwapParams {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "SwapParams: ...")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// MeteoraDammV2 protocol specific parameters
|
||||
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct MeteoraDammV2Params {
|
||||
pub pool: Pubkey,
|
||||
pub token_a_vault: Pubkey,
|
||||
pub token_b_vault: Pubkey,
|
||||
pub token_a_mint: Pubkey,
|
||||
pub token_b_mint: Pubkey,
|
||||
pub token_a_program: Pubkey,
|
||||
pub token_b_program: Pubkey,
|
||||
}
|
||||
|
||||
impl MeteoraDammV2Params {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
token_a_vault: Pubkey,
|
||||
token_b_vault: Pubkey,
|
||||
token_a_mint: Pubkey,
|
||||
token_b_mint: Pubkey,
|
||||
token_a_program: Pubkey,
|
||||
token_b_program: Pubkey,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
token_a_vault,
|
||||
token_b_vault,
|
||||
token_a_mint,
|
||||
token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data =
|
||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||
let mint_accounts =
|
||||
rpc.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint]).await?;
|
||||
let token_a_program = mint_accounts
|
||||
.get(0)
|
||||
.and_then(|a| a.as_ref())
|
||||
.map(|a| a.owner)
|
||||
.ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?;
|
||||
let token_b_program = mint_accounts
|
||||
.get(1)
|
||||
.and_then(|a| a.as_ref())
|
||||
.map(|a| a.owner)
|
||||
.ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?;
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
token_a_vault: pool_data.token_a_vault,
|
||||
token_b_vault: pool_data.token_b_vault,
|
||||
token_a_mint: pool_data.token_a_mint,
|
||||
token_b_mint: pool_data.token_b_mint,
|
||||
token_a_program,
|
||||
token_b_program,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//! DEX protocol parameter types and [`SwapParams`].
|
||||
|
||||
mod bonk;
|
||||
mod dex_swap;
|
||||
mod meteora_damm_v2;
|
||||
mod pumpfun;
|
||||
mod pumpswap;
|
||||
mod raydium_amm_v4;
|
||||
mod raydium_cpmm;
|
||||
|
||||
pub use bonk::BonkParams;
|
||||
pub use dex_swap::{DexParamEnum, SenderConcurrencyConfig, SwapParams};
|
||||
pub use meteora_damm_v2::MeteoraDammV2Params;
|
||||
pub use pumpfun::PumpFunParams;
|
||||
pub use pumpswap::PumpSwapParams;
|
||||
pub use raydium_amm_v4::RaydiumAmmV4Params;
|
||||
pub use raydium_cpmm::RaydiumCpmmParams;
|
||||
@@ -0,0 +1,307 @@
|
||||
use crate::common::bonding_curve::BondingCurveAccount;
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::instruction::utils::pumpfun::reconcile_mayhem_mode_for_trade;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// PumpFun protocol specific parameters
|
||||
/// Configuration parameters specific to PumpFun trading protocol.
|
||||
///
|
||||
/// **Creator vault**: Pump buy/sell pass `creator_vault` = `PDA(["creator-vault", authority])`.
|
||||
/// Usually `authority` is [`BondingCurveAccount::creator`]; with **Creator Rewards Sharing** it is
|
||||
/// `fee_sharing_config_pda(mint)` (see [`fetch_fee_sharing_creator_vault_if_active`](crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active)).
|
||||
/// **Buy/sell**:`creator_vault` 及(若可得)**`tradeEvent` / CPI 日志中的 `creator`** 优先于陈旧的曲线快照;
|
||||
/// ix 组装与链下询价见 [`Self::effective_creator_for_trade`]、[`crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing`]。
|
||||
///
|
||||
/// **V2 instructions**: Set `use_v2_ix = true` to use `buy_v2`/`sell_v2`/`buy_exact_quote_in_v2`
|
||||
/// with unified 27/26-account layout. Required for USDC-paired coins (`quote_mint != WSOL`).
|
||||
/// For SOL-paired coins, legacy instructions still work and are the default.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpFunParams {
|
||||
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||
pub associated_bonding_curve: Pubkey,
|
||||
/// 最新一笔可观测 trade 的 **`tradeEvent.creator`(日志)**。当 `Some` 且非 default 时,
|
||||
/// **优先于** `bonding_curve.creator` 用于链下 creator-fee 询价与 `creator_vault` 在缺省 ix 时的推导。
|
||||
/// Pump 上 creator 可能随交易推进,调用方应在每次解析到带 `creator` 的 trade 后更新(如 `.with_observed_trade_creator`)。
|
||||
pub observed_trade_creator: Option<Pubkey>,
|
||||
/// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing):
|
||||
/// **显式 `creator_vault`(非 default、非 phantom)永远优先并按原值使用**,不会再用 `creator` 重算覆盖;
|
||||
/// 未传时再按 `fee_sharing_creator_vault_if_active`、`PDA(effective_creator)`(见 [`Self::effective_creator_for_trade`])。
|
||||
pub creator_vault: Pubkey,
|
||||
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
|
||||
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
/// SPL Token or Token-2022 program id owning the **mint** (from gRPC / parser / cache).
|
||||
/// **`Pubkey::default()`**:ix 构建时使用 SDK 默认 **Token-2022**(与多数 Pump.fun 新发一致);显式传入 Legacy 或 Token-2022 id 可覆盖该默认值。
|
||||
pub token_program: Pubkey,
|
||||
/// Whether to close token account when selling, only effective during sell operations
|
||||
pub close_token_account_when_sell: Option<bool>,
|
||||
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
|
||||
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
|
||||
pub fee_recipient: Pubkey,
|
||||
/// Quote mint for v2 instructions (default: `So11111111111111111111111111111111111111112` for SOL-paired).
|
||||
/// For USDC-paired coins, set to `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v`.
|
||||
pub quote_mint: Pubkey,
|
||||
/// Whether to use v2 instructions (`buy_v2`/`sell_v2`/`buy_exact_quote_in_v2`).
|
||||
/// Default `false` for backward compatibility. Must be `true` for USDC-paired coins.
|
||||
pub use_v2_ix: bool,
|
||||
}
|
||||
|
||||
impl PumpFunParams {
|
||||
pub fn immediate_sell(
|
||||
creator_vault: Pubkey,
|
||||
token_program: Pubkey,
|
||||
close_token_account_when_sell: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||
associated_bonding_curve: Pubkey::default(),
|
||||
observed_trade_creator: None,
|
||||
creator_vault: creator_vault,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
token_program: token_program,
|
||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||
fee_recipient: Pubkey::default(),
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
|
||||
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
|
||||
pub fn from_dev_trade(
|
||||
mint: Pubkey,
|
||||
token_amount: u64,
|
||||
max_sol_cost: u64,
|
||||
creator: Pubkey,
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
|
||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||
bonding_curve,
|
||||
&mint,
|
||||
token_amount,
|
||||
max_sol_cost,
|
||||
creator,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve_account.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(
|
||||
&bonding_curve_account.creator,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve_account),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: (creator != Pubkey::default()).then_some(creator),
|
||||
creator_vault: creator_vault_resolved,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||
///
|
||||
/// `mayhem_mode`:
|
||||
/// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**(Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。
|
||||
/// - **`None`**:用 `fee_recipient` 是否落在 Mayhem 静态列表推断。
|
||||
pub fn from_trade(
|
||||
bonding_curve: Pubkey,
|
||||
associated_bonding_curve: Pubkey,
|
||||
mint: Pubkey,
|
||||
creator: Pubkey,
|
||||
creator_vault: Pubkey,
|
||||
virtual_token_reserves: u64,
|
||||
virtual_sol_reserves: u64,
|
||||
real_token_reserves: u64,
|
||||
real_sol_reserves: u64,
|
||||
close_token_account_when_sell: Option<bool>,
|
||||
fee_recipient: Pubkey,
|
||||
token_program: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
mayhem_mode: Option<bool>,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
|
||||
let bonding_curve = BondingCurveAccount::from_trade(
|
||||
bonding_curve,
|
||||
mint,
|
||||
creator,
|
||||
virtual_token_reserves,
|
||||
virtual_sol_reserves,
|
||||
real_token_reserves,
|
||||
real_sol_reserves,
|
||||
is_mayhem_mode,
|
||||
is_cashback_coin,
|
||||
);
|
||||
let creator_vault_resolved =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
creator_vault,
|
||||
&mint,
|
||||
None,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: (creator != Pubkey::default()).then_some(creator),
|
||||
creator_vault: creator_vault_resolved,
|
||||
fee_sharing_creator_vault_if_active: None,
|
||||
close_token_account_when_sell: close_token_account_when_sell,
|
||||
token_program: token_program,
|
||||
fee_recipient,
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 仅 RPC 读取曲线快照;[`Self::observed_trade_creator`] 为 `None`,便于 bot 缓存合并时用粘性的 trade 日志 creator 覆盖陈旧曲线推导。
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let account =
|
||||
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
||||
let mint_account = rpc.get_account(&mint).await?;
|
||||
let bonding_curve = BondingCurveAccount {
|
||||
discriminator: 0,
|
||||
account: account.1,
|
||||
virtual_token_reserves: account.0.virtual_token_reserves,
|
||||
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
||||
real_token_reserves: account.0.real_token_reserves,
|
||||
real_sol_reserves: account.0.real_sol_reserves,
|
||||
token_total_supply: account.0.token_total_supply,
|
||||
complete: account.0.complete,
|
||||
creator: account.0.creator,
|
||||
is_mayhem_mode: account.0.is_mayhem_mode,
|
||||
is_cashback_coin: account.0.is_cashback_coin,
|
||||
};
|
||||
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
||||
&bonding_curve.account,
|
||||
mint,
|
||||
&mint_account.owner,
|
||||
);
|
||||
let fee_sharing_creator_vault_if_active =
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc, mint,
|
||||
)
|
||||
.await?;
|
||||
let creator_vault =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&bonding_curve.creator,
|
||||
Pubkey::default(),
|
||||
mint,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
.or_else(|| {
|
||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
Ok(Self {
|
||||
bonding_curve: Arc::new(bonding_curve),
|
||||
associated_bonding_curve: associated_bonding_curve,
|
||||
observed_trade_creator: None,
|
||||
creator_vault,
|
||||
fee_sharing_creator_vault_if_active,
|
||||
close_token_account_when_sell: None,
|
||||
token_program: mint_account.owner,
|
||||
fee_recipient: Pubkey::default(),
|
||||
quote_mint: Pubkey::default(),
|
||||
use_v2_ix: false,
|
||||
})
|
||||
}
|
||||
|
||||
/// 链下公式与 **`creator_vault` 推导回退**:**日志/事件 `creator`**(若已写入 `observed_trade_creator`)
|
||||
/// 优先,否则使用 `bonding_curve.creator`。
|
||||
#[inline]
|
||||
pub fn effective_creator_for_trade(&self) -> Pubkey {
|
||||
self.observed_trade_creator
|
||||
.filter(|c| *c != Pubkey::default())
|
||||
.unwrap_or(self.bonding_curve.creator)
|
||||
}
|
||||
|
||||
/// One `getAccount` on pump-fees `SharingConfig` + re-resolves [`Self::creator_vault`]. Call before sell
|
||||
/// when params come from gRPC/cache so migrated fee-sharing mints do not hit Anchor 2006.
|
||||
pub async fn refresh_fee_sharing_creator_vault_from_rpc(
|
||||
mut self,
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
self.fee_sharing_creator_vault_if_active =
|
||||
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(
|
||||
rpc, mint,
|
||||
)
|
||||
.await?;
|
||||
let c = self.effective_creator_for_trade();
|
||||
if let Some(v) =
|
||||
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&c,
|
||||
self.creator_vault,
|
||||
mint,
|
||||
self.fee_sharing_creator_vault_if_active,
|
||||
)
|
||||
{
|
||||
self.creator_vault = v;
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Sets `quote_mint` and enables v2 instructions. Required for USDC-paired coins.
|
||||
/// For SOL-paired coins, pass `WSOL_TOKEN_ACCOUNT` or leave default.
|
||||
#[inline]
|
||||
pub fn with_quote_mint(mut self, quote_mint: Pubkey) -> Self {
|
||||
self.quote_mint = quote_mint;
|
||||
self.use_v2_ix = quote_mint != Pubkey::default();
|
||||
self
|
||||
}
|
||||
|
||||
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`Self::effective_creator_for_trade`] + resolve.
|
||||
#[inline]
|
||||
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
|
||||
self.creator_vault = creator_vault;
|
||||
self
|
||||
}
|
||||
|
||||
/// 覆盖 **最新一笔 trade 日志中的 `creator`**(`tradeEvent.creator`)。`None` 或 default 会清除覆盖。
|
||||
#[inline]
|
||||
pub fn with_observed_trade_creator(mut self, c: Option<Pubkey>) -> Self {
|
||||
self.observed_trade_creator = c.filter(|x| *x != Pubkey::default());
|
||||
self
|
||||
}
|
||||
|
||||
/// Override fee-sharing vault hint (e.g. from an off-chain indexer). `None` clears the hint.
|
||||
#[inline]
|
||||
pub fn with_fee_sharing_creator_vault_if_active(
|
||||
mut self,
|
||||
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||
) -> Self {
|
||||
self.fee_sharing_creator_vault_if_active = fee_sharing_creator_vault_if_active;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// PumpSwap Protocol Specific Parameters
|
||||
///
|
||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||
/// token configuration, and transaction amounts.
|
||||
///
|
||||
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
||||
/// retrieve the relevant information from RPC, which will increase transaction time.
|
||||
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
||||
#[derive(Clone)]
|
||||
pub struct PumpSwapParams {
|
||||
/// Liquidity pool address
|
||||
pub pool: Pubkey,
|
||||
/// Base token mint address
|
||||
/// The mint account address of the base token in the trading pair
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
||||
pub quote_mint: Pubkey,
|
||||
/// Pool base token account
|
||||
pub pool_base_token_account: Pubkey,
|
||||
/// Pool quote token account
|
||||
pub pool_quote_token_account: Pubkey,
|
||||
/// Base token reserves in the pool
|
||||
pub pool_base_token_reserves: u64,
|
||||
/// Quote token reserves in the pool
|
||||
pub pool_quote_token_reserves: u64,
|
||||
/// Coin creator vault ATA
|
||||
pub coin_creator_vault_ata: Pubkey,
|
||||
/// Coin creator vault authority
|
||||
pub coin_creator_vault_authority: Pubkey,
|
||||
/// Token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Whether the pool is in mayhem mode
|
||||
pub is_mayhem_mode: bool,
|
||||
/// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap
|
||||
/// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()`
|
||||
/// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053).
|
||||
pub coin_creator: Pubkey,
|
||||
/// Whether the pool's coin has cashback enabled
|
||||
pub is_cashback_coin: bool,
|
||||
/// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell
|
||||
/// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS)
|
||||
/// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket.
|
||||
/// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps).
|
||||
pub cashback_fee_basis_points: u64,
|
||||
}
|
||||
|
||||
impl PumpSwapParams {
|
||||
pub fn new(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
coin_creator: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Self {
|
||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||
Self {
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
is_mayhem_mode,
|
||||
coin_creator,
|
||||
is_cashback_coin,
|
||||
cashback_fee_basis_points,
|
||||
}
|
||||
}
|
||||
|
||||
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
||||
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
||||
/// lookups and associated latency. Token program IDs should be sourced from
|
||||
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
||||
/// differences.
|
||||
///
|
||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
||||
/// from the event so that buy/sell instructions include the correct remaining
|
||||
/// accounts for cashback.
|
||||
pub fn from_trade(
|
||||
pool: Pubkey,
|
||||
base_mint: Pubkey,
|
||||
quote_mint: Pubkey,
|
||||
pool_base_token_account: Pubkey,
|
||||
pool_quote_token_account: Pubkey,
|
||||
pool_base_token_reserves: u64,
|
||||
pool_quote_token_reserves: u64,
|
||||
coin_creator_vault_ata: Pubkey,
|
||||
coin_creator_vault_authority: Pubkey,
|
||||
base_token_program: Pubkey,
|
||||
quote_token_program: Pubkey,
|
||||
fee_recipient: Pubkey,
|
||||
coin_creator: Pubkey,
|
||||
is_cashback_coin: bool,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Self {
|
||||
Self::new(
|
||||
pool,
|
||||
base_mint,
|
||||
quote_mint,
|
||||
pool_base_token_account,
|
||||
pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program,
|
||||
quote_token_program,
|
||||
fee_recipient,
|
||||
coin_creator,
|
||||
is_cashback_coin,
|
||||
cashback_fee_basis_points,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn from_mint_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
mint: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else if let Ok((pool_address, _)) =
|
||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||
{
|
||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||
Self::from_pool_data(rpc, pool_address, &pool_data).await
|
||||
}
|
||||
|
||||
/// Build params from an already-decoded Pool, only fetching token balances.
|
||||
///
|
||||
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
|
||||
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
|
||||
/// decoded Pool).
|
||||
pub async fn from_pool_data(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||
let creator = pool_data.coin_creator;
|
||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||
creator,
|
||||
pool_data.quote_mint,
|
||||
);
|
||||
let coin_creator_vault_authority =
|
||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||
|
||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.base_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||
pool_address,
|
||||
&pool_data.quote_mint,
|
||||
&crate::constants::TOKEN_PROGRAM,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
pool: *pool_address,
|
||||
base_mint: pool_data.base_mint,
|
||||
quote_mint: pool_data.quote_mint,
|
||||
pool_base_token_account: pool_data.pool_base_token_account,
|
||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
coin_creator_vault_ata,
|
||||
coin_creator_vault_authority,
|
||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_cashback_coin: pool_data.is_cashback_coin,
|
||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||
crate::constants::TOKEN_PROGRAM
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_2022
|
||||
},
|
||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||
coin_creator: pool_data.coin_creator,
|
||||
cashback_fee_basis_points: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use crate::trading::common::get_multi_token_balances;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumAmmV4Params {
|
||||
/// AMM pool address
|
||||
pub amm: Pubkey,
|
||||
/// Base token (coin) mint address
|
||||
pub coin_mint: Pubkey,
|
||||
/// Quote token (pc) mint address
|
||||
pub pc_mint: Pubkey,
|
||||
/// Pool's coin token account address
|
||||
pub token_coin: Pubkey,
|
||||
/// Pool's pc token account address
|
||||
pub token_pc: Pubkey,
|
||||
/// Current coin reserve amount in the pool
|
||||
pub coin_reserve: u64,
|
||||
/// Current pc reserve amount in the pool
|
||||
pub pc_reserve: u64,
|
||||
}
|
||||
|
||||
impl RaydiumAmmV4Params {
|
||||
pub fn new(
|
||||
amm: Pubkey,
|
||||
coin_mint: Pubkey,
|
||||
pc_mint: Pubkey,
|
||||
token_coin: Pubkey,
|
||||
token_pc: Pubkey,
|
||||
coin_reserve: u64,
|
||||
pc_reserve: u64,
|
||||
) -> Self {
|
||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||
}
|
||||
pub async fn from_amm_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
amm: Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||
let (coin_reserve, pc_reserve) =
|
||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||
Ok(Self {
|
||||
amm,
|
||||
coin_mint: amm_info.coin_mint,
|
||||
pc_mint: amm_info.pc_mint,
|
||||
token_coin: amm_info.token_coin,
|
||||
token_pc: amm_info.token_pc,
|
||||
coin_reserve,
|
||||
pc_reserve,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::common::SolanaRpcClient;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// RaydiumCpmm protocol specific parameters
|
||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||
#[derive(Clone)]
|
||||
pub struct RaydiumCpmmParams {
|
||||
/// Pool address
|
||||
pub pool_state: Pubkey,
|
||||
/// Amm config address
|
||||
pub amm_config: Pubkey,
|
||||
/// Base token mint address
|
||||
pub base_mint: Pubkey,
|
||||
/// Quote token mint address
|
||||
pub quote_mint: Pubkey,
|
||||
/// Base token reserve amount in the pool
|
||||
pub base_reserve: u64,
|
||||
/// Quote token reserve amount in the pool
|
||||
pub quote_reserve: u64,
|
||||
/// Base token vault address
|
||||
pub base_vault: Pubkey,
|
||||
/// Quote token vault address
|
||||
pub quote_vault: Pubkey,
|
||||
/// Base token program ID
|
||||
pub base_token_program: Pubkey,
|
||||
/// Quote token program ID
|
||||
pub quote_token_program: Pubkey,
|
||||
/// Observation state account
|
||||
pub observation_state: Pubkey,
|
||||
}
|
||||
|
||||
impl RaydiumCpmmParams {
|
||||
pub fn from_trade(
|
||||
pool_state: Pubkey,
|
||||
amm_config: Pubkey,
|
||||
input_token_mint: Pubkey,
|
||||
output_token_mint: Pubkey,
|
||||
input_vault: Pubkey,
|
||||
output_vault: Pubkey,
|
||||
input_token_program: Pubkey,
|
||||
output_token_program: Pubkey,
|
||||
observation_state: Pubkey,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool_state: pool_state,
|
||||
amm_config: amm_config,
|
||||
base_mint: input_token_mint,
|
||||
quote_mint: output_token_mint,
|
||||
base_reserve: base_reserve,
|
||||
quote_reserve: quote_reserve,
|
||||
base_vault: input_vault,
|
||||
quote_vault: output_vault,
|
||||
base_token_program: input_token_program,
|
||||
quote_token_program: output_token_program,
|
||||
observation_state: observation_state,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pool_address_by_rpc(
|
||||
rpc: &SolanaRpcClient,
|
||||
pool_address: &Pubkey,
|
||||
) -> Result<Self, anyhow::Error> {
|
||||
let pool =
|
||||
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
||||
let (token0_balance, token1_balance) =
|
||||
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
||||
rpc,
|
||||
pool_address,
|
||||
&pool.token0_mint,
|
||||
&pool.token1_mint,
|
||||
)
|
||||
.await?;
|
||||
Ok(Self {
|
||||
pool_state: *pool_address,
|
||||
amm_config: pool.amm_config,
|
||||
base_mint: pool.token0_mint,
|
||||
quote_mint: pool.token1_mint,
|
||||
base_reserve: token0_balance,
|
||||
quote_reserve: token1_balance,
|
||||
base_vault: pool.token0_vault,
|
||||
quote_vault: pool.token1_vault,
|
||||
base_token_program: pool.token0_program,
|
||||
quote_token_program: pool.token1_program,
|
||||
observation_state: pool.observation_key,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::swqos::SwqosType;
|
||||
use crate::trading::SwapParams;
|
||||
use anyhow::Result;
|
||||
use solana_sdk::{instruction::Instruction, signature::Signature};
|
||||
|
||||
/// 交易执行器trait - 定义了所有交易协议都需要实现的核心方法
|
||||
#[async_trait::async_trait]
|
||||
pub trait TradeExecutor: Send + Sync {
|
||||
@@ -12,7 +12,7 @@ pub trait TradeExecutor: Send + Sync {
|
||||
async fn swap(
|
||||
&self,
|
||||
params: SwapParams,
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)>;
|
||||
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)>;
|
||||
/// 获取协议名称
|
||||
fn protocol_name(&self) -> &'static str;
|
||||
}
|
||||
|
||||
@@ -19,10 +19,11 @@ const TX_BUILDER_POOL_PREFILL: usize = 64;
|
||||
|
||||
use crossbeam_queue::ArrayQueue;
|
||||
use once_cell::sync::Lazy;
|
||||
use solana_message::AddressLookupTableAccount;
|
||||
use solana_sdk::{
|
||||
hash::Hash,
|
||||
instruction::Instruction,
|
||||
message::{v0, AddressLookupTableAccount, Message, VersionedMessage},
|
||||
message::{v0, Message, VersionedMessage},
|
||||
pubkey::Pubkey,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -28,6 +28,10 @@ pub const fn ceil_div(a: u128, b: u128) -> u128 {
|
||||
(a + b - 1) / b
|
||||
}
|
||||
|
||||
/// Maximum slippage in basis points (99.99% = 9999 bps)
|
||||
/// This prevents the wrap amount from doubling when slippage is 100%
|
||||
pub const MAX_SLIPPAGE_BASIS_POINTS: u64 = 9999;
|
||||
|
||||
/// Calculate buy amount with slippage protection
|
||||
/// Add slippage percentage to the amount to ensure successful purchase
|
||||
///
|
||||
@@ -40,9 +44,18 @@ pub const fn ceil_div(a: u128, b: u128) -> u128 {
|
||||
/// * basis_points = 10 -> 0.1% slippage
|
||||
/// * basis_points = 100 -> 1% slippage
|
||||
/// * basis_points = 500 -> 5% slippage
|
||||
///
|
||||
/// # Note
|
||||
/// Basis points are clamped to MAX_SLIPPAGE_BASIS_POINTS (9999 = 99.99%)
|
||||
/// to prevent the amount from doubling when basis_points = 10000.
|
||||
#[inline(always)]
|
||||
pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64 {
|
||||
amount + (amount * basis_points / 10000)
|
||||
let bps = if basis_points > MAX_SLIPPAGE_BASIS_POINTS {
|
||||
MAX_SLIPPAGE_BASIS_POINTS
|
||||
} else {
|
||||
basis_points
|
||||
};
|
||||
amount + (amount * bps / 10000)
|
||||
}
|
||||
|
||||
/// Calculate sell amount with slippage protection
|
||||
@@ -59,6 +72,9 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
|
||||
/// * basis_points = 500 -> 5% slippage
|
||||
#[inline(always)]
|
||||
pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
||||
if amount == 0 {
|
||||
return 0;
|
||||
}
|
||||
if amount <= basis_points / 10000 {
|
||||
1
|
||||
} else {
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
use solana_sdk::{native_token::sol_str_to_lamports, pubkey::Pubkey};
|
||||
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
|
||||
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
fn sol_to_lamports(sol: f64) -> u64 {
|
||||
(sol * 1_000_000_000.0) as u64
|
||||
}
|
||||
|
||||
use crate::{
|
||||
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
|
||||
utils::calc::common::compute_fee,
|
||||
};
|
||||
|
||||
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
|
||||
#[inline]
|
||||
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
|
||||
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
|
||||
}
|
||||
|
||||
/// Calculates the amount of tokens that can be purchased with a given SOL amount
|
||||
/// using the bonding curve formula.
|
||||
///
|
||||
|
||||
+28
-12
@@ -6,6 +6,18 @@ use crate::instruction::utils::pumpswap::accounts::{
|
||||
};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
|
||||
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
|
||||
#[inline]
|
||||
pub(crate) fn creator_side_fee_basis_points(
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
let creator_bps =
|
||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
||||
creator_bps.saturating_add(cashback_fee_basis_points)
|
||||
}
|
||||
|
||||
/// Result for buying base tokens with base amount input
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BuyBaseInputResult {
|
||||
@@ -58,6 +70,7 @@ pub struct SellQuoteInputResult {
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `BuyBaseInputResult` containing quote amounts and slippage calculations
|
||||
@@ -67,6 +80,7 @@ pub fn buy_base_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -89,11 +103,9 @@ pub fn buy_base_input_internal(
|
||||
let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
||||
let coin_creator_fee = if *coin_creator == Pubkey::default() {
|
||||
0
|
||||
} else {
|
||||
compute_fee(quote_amount_in as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
|
||||
};
|
||||
let creator_bps =
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
|
||||
let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64;
|
||||
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
|
||||
|
||||
// Calculate max quote with slippage
|
||||
@@ -114,6 +126,7 @@ pub fn buy_base_input_internal(
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `BuyQuoteInputResult` containing base amount and slippage calculations
|
||||
@@ -123,6 +136,7 @@ pub fn buy_quote_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -131,7 +145,7 @@ pub fn buy_quote_input_internal(
|
||||
// Calculate total fee basis points
|
||||
let total_fee_bps = LP_FEE_BASIS_POINTS
|
||||
+ PROTOCOL_FEE_BASIS_POINTS
|
||||
+ if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
||||
+ creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points);
|
||||
let denominator = 10_000 + total_fee_bps;
|
||||
|
||||
// Calculate effective quote amount after fees
|
||||
@@ -165,6 +179,7 @@ pub fn buy_quote_input_internal(
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `SellBaseInputResult` containing quote amounts and slippage calculations
|
||||
@@ -174,6 +189,7 @@ pub fn sell_base_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -187,11 +203,9 @@ pub fn sell_base_input_internal(
|
||||
let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
||||
let protocol_fee =
|
||||
compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
||||
let coin_creator_fee = if *coin_creator == Pubkey::default() {
|
||||
0
|
||||
} else {
|
||||
compute_fee(quote_amount_out as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
|
||||
};
|
||||
let creator_bps =
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
|
||||
let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64;
|
||||
|
||||
// Calculate final quote after fees
|
||||
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
|
||||
@@ -234,6 +248,7 @@ fn calculate_quote_amount_out(
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
/// # Returns
|
||||
/// * `SellQuoteInputResult` containing base amount and slippage calculations
|
||||
@@ -243,6 +258,7 @@ pub fn sell_quote_input_internal(
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
@@ -256,7 +272,7 @@ pub fn sell_quote_input_internal(
|
||||
quote,
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS },
|
||||
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||
);
|
||||
|
||||
// Calculate base amount needed using inverse constant product formula
|
||||
|
||||
+6
-1
@@ -81,6 +81,7 @@ mkdir -p examples/pumpfun_buy_test/src
|
||||
cat > examples/pumpfun_buy_test/src/main.rs << 'EOF'
|
||||
use sol_trade_sdk::{
|
||||
common::{TradeConfig, AnyResult},
|
||||
constants::TOKEN_PROGRAM,
|
||||
swqos::{SwqosConfig, SwqosRegion},
|
||||
trading::{core::params::PumpFunParams, factory::DexType},
|
||||
SolanaTrade, TradeTokenType, TradeBuyParams,
|
||||
@@ -166,7 +167,7 @@ async fn main() -> AnyResult<()> {
|
||||
println!("📊 滑点: {} basis points", slippage);
|
||||
println!("================================\n");
|
||||
|
||||
// PumpFun买入参数 (买入不需要特殊参数,使用零值)
|
||||
// PumpFun买入参数 (占位;实际路径应使用 RPC 或事件填充)
|
||||
let params = PumpFunParams::from_trade(
|
||||
Pubkey::default(), // bonding_curve
|
||||
Pubkey::default(), // associated_bonding_curve
|
||||
@@ -178,6 +179,10 @@ async fn main() -> AnyResult<()> {
|
||||
0, // real_token_reserves
|
||||
0, // real_sol_reserves
|
||||
None, // close_token_account_when_sell
|
||||
Pubkey::default(), // fee_recipient
|
||||
TOKEN_PROGRAM,
|
||||
false, // is_cashback_coin
|
||||
None, // mayhem_mode(None=按 fee_recipient 推断)
|
||||
);
|
||||
|
||||
let buy_params = TradeBuyParams {
|
||||
|
||||
Reference in New Issue
Block a user