Compare commits

...

29 Commits

Author SHA1 Message Date
0xfnzero a391539000 chore: release 4.0.3
- Bump version and README crates.io examples to 4.0.3.

Made-with: Cursor
2026-04-12 00:55:52 +08:00
0xfnzero c75ca4b034 fix(pumpfun): canonical BC PDAs, trust stream fee recipient, harden creator vault
- Always derive bonding curve and associated bonding vault from mint (avoids stale cached PDAs / wrong pool).
- Use non-default fee_recipient from parser/stream as account #2 without extra authorization filtering (addresses Pump 6000 NotAuthorized when event fee was rejected).
- Rework resolve_creator_vault_for_ix: default-creator cases, phantom default PDA, fee-sharing vault; add unit tests.
- PumpFunParams: respect explicit mayhem_mode from events; fallback creator_vault via get_creator_vault_pda when resolve returns None.
- Remove unused lazy_static tip-cache stub and lazy_static dependency.

Made-with: Cursor
2026-04-12 00:34:44 +08:00
0xfnzero b13b4fda0a fix(pumpfun): resolve creator_vault with fee-sharing PDA (mint-aware)
- Add get_fee_sharing_config_pda per @pump-fun/pump-sdk (sharing-config + mint under pump-fees)
- resolve_creator_vault_for_ix now takes mint: accept gRPC creator_vault when it matches either
  PDA(creator) or PDA(fee_sharing_config(mint)) for migrated / fee-sharing bonding curves
- Mayhem uses the same Pump buy/sell account layout (IDL); Token-2022 + fee pool differ, not creator_vault seeds

Made-with: Cursor
2026-04-11 19:44:54 +08:00
0xfnzero 4dec087ea6 fix(pumpfun): resolve creator_vault vs PDA(creator); validate fee recipient pool
- Add resolve_creator_vault_for_ix: use gRPC creator_vault only when it matches
  PDA(['creator-vault', bonding_curve.creator]); on mismatch use derived PDA to
  avoid ConstraintSeeds (2006) from stale/wrong stream vaults
- pump_fun_fee_recipient_meta: only use event fee_recipient if authorized for
  is_mayhem_mode (avoids 6000 NotAuthorized when standard AMM fee is used on
  Mayhem coins)
- Align PumpFunParams::from_trade/from_dev_trade cached creator_vault with the
  same resolution logic

Made-with: Cursor
2026-04-11 19:37:05 +08:00
0xfnzero 9c9ecf3e5b fix(pumpfun): prefer event creator_vault; align fees with pump-sdk
- Use protocol_params.creator_vault for buy/sell when non-default to avoid
  ConstraintSeeds (2006) when gRPC/event creator drifts from on-chain bonding curve
- Preserve event creator_vault in PumpFunParams::from_trade/from_dev_trade instead
  of always overwriting with PDA(creator)
- Fee recipient: prefer gRPC fee_recipient; else random from CURRENT_FEE_RECIPIENTS
  pool (standard) or MAYHEM pool, matching @pump-fun/pump-sdk fees.ts
- Add cold-path helpers: extend_bonding_curve_account_instruction, PUMP_BONDING_CURVE_MIN_DATA_LEN

Made-with: Cursor
2026-04-11 19:13:12 +08:00
0xfnzero 4b451af5ff feat: Pump.fun mayhem_mode, Solana 3.1.12, TradeConfig builder alignment
- PumpFunParams::from_trade/from_dev_trade: add mayhem_mode Option; infer Mayhem
  via is_mayhem_fee_recipient when None (fixes fee recipient / NotAuthorized 6000
  when AMM protocol fee pubkey is used).
- Add is_mayhem_fee_recipient and is_amm_fee_recipient helpers in pumpfun utils.
- Bump solana-* crates to 3.1.12; align tonic/prost; use solana-message for
  AddressLookupTableAccount; add solana-system-interface 3.0.
- Update examples and latency script for new PumpFunParams signature.
- SWQoS and transaction builder adjustments for dependency changes.

Made-with: Cursor
2026-04-11 18:43:18 +08:00
0xfnzero 8f2f99f3d9 chore: remove duplicate nodejs and python SDK subdirectories
These incomplete SDK implementations in the Rust repository were outdated
and redundant. The complete versions are maintained in separate repositories:
- sol-trade-sdk-ts (Node.js)
- sol-trade-sdk-python (Python)

🤖 Generated with [Qoder](https://qoder.com)
2026-04-10 15:37:53 +08:00
0xfnzero 971ef41fad feat: add global MEV protection and refactor TradeConfig to builder pattern
- Add `mev_protection: bool` to `TradeConfig` and `InfrastructureConfig` (default: false)
  - Astralane QUIC: switches to port 9000 (MEV-protected endpoint) when enabled
  - BlockRazor HTTP: uses `mode=sandwichMitigation` query param when enabled
  - BlockRazor gRPC: uses `mode=sandwichMitigation` when enabled
- Add `SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV` constants (port 9000) to `constants/swqos.rs`
- Fix `astralane_quic.rs` IP candidates to use the actual port from the address (supports both 7000 and 9000)
- Refactor `TradeConfig` to builder pattern via `TradeConfig::builder()`
  - Introduce `TradeConfigBuilder` with all optional fields and clear defaults
  - `TradeConfig::new()` kept as a shortcut (calls `builder().build()`) for backward compatibility
  - Remove old `with_wsol_ata_config` / `with_check_min_tip` / `with_swqos_cores_from_end` / `with_mev_protection` chain methods
- Update all 16 examples to use `TradeConfig::builder()` with commented-out options so users can discover all available settings at a glance
- Update README.md and README_CN.md code snippets to use builder pattern

🤖 Generated with [Qoder][https://qoder.com]
2026-04-08 02:13:37 +08:00
0xfnzero 35bfa93516 fix: add slippage clamping and improve WSOL wrap logic
Changes to src/utils/calc/common.rs:
- Add MAX_SLIPPAGE_BASIS_POINTS constant (9999 = 99.99%)
- Clamp basis_points in calculate_with_slippage_buy to prevent amount doubling

Changes to src/instruction/pumpswap.rs:
- Fix WSOL wrap amount calculation for exact vs non-exact input modes
- Use input_amount for exact mode, sol_amount for non-exact mode

🤖 Generated with [Qoder][https://qoder.com]
2026-04-06 16:53:36 +08:00
0xfnzero c8f9f9f6aa docs: add SDK Versions section with multi-language navigation
Add a dedicated section showing all available SDK language versions
with links to their repositories.

🤖 Generated with [Qoder](https://qoder.com)
2026-04-06 02:37:18 +08:00
0xfnzero 99846a21c2 fix(blockrazor): wrap ArcSwap in Arc for Clone and static ping task
- Store gRPC client as Arc<ArcSwap<BlockRazorGrpcClient>> so BlockRazorBackend
  can derive Clone (ArcSwap does not implement Clone).
- Cloning the outer Arc gives owned handles for tokio::spawn in start_ping_task,
  fixing E0521 (borrowed self escaping to 'static future).

Made-with: Cursor
2026-04-03 10:54:33 +08:00
Wood 062c5415c3 Merge pull request #97 from 520goodyear/main
#让SDK中打印 submit_timing 的数据( SwqosType和 提交耗时)返回给buy 和 sell 方法。
2026-04-02 20:59:31 +08:00
Admin eaa3214e4d #让SDK中打印 submit_timing 的数据( SwqosType和 提交耗时)返回给buy 和 sell 方法。
主要解决问题:
当 wait_tx_confirmed 参数为 false 时,SUBMIT_TIMEOUT_SECS 等待改为2秒,要么全部提交完成返回,要么不等待提交未返回的交易,方便快速创建交易订单。

#Let the SDK print submit_timing data (SwqosType and submission time) and return it to the buy and sell methods.

Main problems solved:

When the wait_tx_confirmed parameter is false, the SUBMIT_TIMEOUT_SECS wait is changed to 2 seconds. Either all submissions are completed and returned, or the unreturned transactions are not waited for submission, which facilitates the rapid creation of transaction orders.
2026-04-01 15:46:01 +08:00
0xfnzero ae890ad976 Add automatic gRPC reconnection for BlockRazor with zero-overhead
Implement lock-free reconnection mechanism using ArcSwap to maintain high
performance for transaction sending while providing automatic recovery
from connection failures.

Key improvements:
- Add automatic reconnection with exponential backoff (1s -> 60s max)
- Use ArcSwap for lock-free atomic reference swapping
- Zero overhead for send_transaction path: only atomic load operation
- Background health check every 30s triggers reconnection on failure
- Reconnection success resets backoff delay to 1s

This ensures transaction sending remains fast (no lock contention) while
providing automatic recovery from network issues or server restarts.

Inspired by the proven reconnection pattern in sol-parser-sdk.
2026-03-30 01:17:33 +08:00
Wood e10ee0de3b Merge pull request #94 from matt-soyas/main
Add soyas tip accounts
2026-03-26 01:00:03 +08:00
Wood 5e1fb1a66b Merge pull request #93 from HelvetiCrypt/feat/pumpswap-from-pool-data
Add PumpSwapParams::from_pool_data() to reuse decoded Pool
2026-03-26 00:59:49 +08:00
matt-soyas a5ea26afaf Add soyas tip accounts 2026-03-25 21:53:07 +08:00
vibes ee4a8f685b Fix slow getProgramAccounts: restore DataSize for both pool sizes, add 3s timeout
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:42:45 +00:00
vibes 322acf8baf Fix getProgramAccounts missing Token2022 pools (643 bytes vs 252)
DataSize(252) filter rejects Token2022 PumpSwap pools which are 643 bytes.
This causes find_by_mint to miss pools that already exist on-chain, leading
to unnecessary 45s+ wait loops during migration detection.

Remove the DataSize filter from find_by_base_mint and find_by_quote_mint —
the Memcmp filter on program-owned accounts is specific enough.

Also add diagnostic logging on find_by_mint failure for debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 12:28:23 +00:00
vibes 378b8fc324 Add PumpSwapParams::from_pool_data() to avoid redundant pool fetch
When using find_by_mint() followed by from_pool_address_by_rpc(), the pool
account is fetched twice via getAccount — once in find_by_mint (which returns
the decoded Pool) and again in from_pool_address_by_rpc.

from_pool_data() accepts a pre-decoded Pool reference and only fetches the
2 token balance RPC calls, saving 1 getAccount per trade.

from_pool_address_by_rpc() now delegates to from_pool_data() internally,
so existing callers are unaffected.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 11:05:41 +00:00
Wood 19f11aa620 Release v4.0.2 with Astralane QUIC reliability updates.
Bump crate/docs version to 4.0.2 and add release notes covering IPv4/IPv6 address-family handling, direct-IP candidate preference, and endpoint failover rotation for more stable low-latency QUIC submission.

Made-with: Cursor
2026-03-24 18:48:44 +08:00
Wood 61cea7546c Fix Astralane QUIC address-family mismatch and add endpoint failover.
Prefer IPv4 candidates (including official Astralane direct IPs), bind local QUIC socket by remote address family, and rotate reconnect attempts across candidates for higher reliability under DNS/Cloudflare variance.

Made-with: Cursor
2026-03-24 18:28:41 +08:00
Wood d52503b8de Release v4.0.1: Fix BlockRazor gRPC endpoints and set gRPC as default transport
## Changes since v4.0.0

### Bug Fixes
- Fixed BlockRazor gRPC endpoints to use correct gRPC service URLs
- Set gRPC as the default transport layer for BlockRazor with HTTP fallback

This release improves the reliability and performance of BlockRazor integration by prioritizing gRPC communication while maintaining HTTP fallback compatibility.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 01:34:48 +08:00
Wood 9e53b7694c Fix BlockRazor gRPC endpoints and set gRPC as default transport
- Add SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC with correct gRPC endpoints
- Add Grpc variant to SwqosTransport enum
- Add get_endpoint_with_transport() method for transport-aware endpoint selection
- Change BlockRazor default: transport=None now uses gRPC (was HTTP)
- Require explicit Some(SwqosTransport::Http) for HTTP fallback

This fixes the issue where BlockRazor was incorrectly using HTTP endpoints
when gRPC was intended. Users can now use gRPC by default or explicitly
specify HTTP transport when needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 01:17:06 +08:00
Wood 07e45d136f Release v4.0.0: SWQoS transport improvements and Binary-Tx response handling
Major changes:
- Switch BlockRazor default transport from gRPC to HTTP to avoid FRAME_SIZE_ERROR
  - gRPC mode still available via explicit SwqosTransport configuration
- Fix ZeroSlot Binary-Tx JSON-RPC 2.0 response parsing
  - Properly handle success responses with "result" field
  - Properly handle error responses with "error" field containing code and message
- Update version to 4.0.0
- Update README files to reflect new version

Technical details:
- BlockRazor: HTTP mode is now the default (SwqosTransport::Http or None)
- ZeroSlot: Parse JSON-RPC 2.0 format responses instead of plain text
- Proto code: Pre-generated gRPC code for BlockRazor (no protoc required)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 21:32:23 +08:00
Wood 20d053bab3 Add gRPC support for BlockRazor with HTTP fallback (gRPC by default)
- Add BlockRazorBackend enum to support both gRPC and HTTP transport
- Default to gRPC for better performance, HTTP available via explicit selection
- Update SwqosConfig::BlockRazor to accept optional SwqosTransport parameter
- Implement keep-alive ping task for both gRPC and HTTP backends
- Follow same backend pattern as Astralane (Quic/Http) and Node1 (Quic/Http)
- Manual gRPC client implementation to avoid proto compilation issues

Usage:
- Default: BlockRazorClient::new() uses gRPC
- HTTP: BlockRazorClient::new_http() for HTTP transport
- Config: SwqosConfig::BlockRazor(token, region, url, None) for gRPC
- Config: SwqosConfig::BlockRazor(token, region, url, Some(Http)) for HTTP

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 18:01:50 +08:00
Wood 5d921f23ff Improve ZeroSlot client: add HTTP keep-alive ping and switch to Binary-Tx
- Add HTTP keep-alive ping task (30s interval) using free getHealth method
- Switch from JSON-RPC to faster Binary-Tx endpoint (/txb) for transaction submission
- Send raw binary transaction bytes directly to avoid encoding/decoding overhead
- Update response handling for Binary-Tx plain text status codes (200/403/419/500)
- Follow same keep-alive pattern as BlockRazor, Stellium, and Astralane clients

These changes reduce first-submit cold start latency and overall transaction submission time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-18 17:29:14 +08:00
Wood a187126554 Add 9 new Astralane tip wallets for improved routing and reduced write-lock contention
Made-with: Cursor
2026-03-18 14:40:06 +08:00
Wood 6ea8c27824 docs: sync README and README_CN version to 3.6.5
Made-with: Cursor
2026-03-17 17:51:09 +08:00
54 changed files with 2236 additions and 534 deletions
+9 -27
View File
@@ -1,30 +1,12 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Proto 生成工具和生成的代码(用户不需要)
/proto/gen/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
# 预生成的 proto Rust 代码(提交到仓库,但用户不应修改)
# /src/swqos/pb/serverpb.rs <- 这个文件已经提交,用户不应修改
# Build artifacts
/target/
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.cargo/
tmp_*.rs
tmp_*.log
.claude/
.serena/
# Proto sources
/proto/
+25 -19
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "3.6.5"
version = "4.0.3"
edition = "2021"
authors = [
"William <byteblock6@gmail.com>",
@@ -45,23 +45,26 @@ perf-trace = [] # 性能追踪特性,生产环境应禁用以获得最佳性
[dependencies]
solana-sdk = "3.0.0"
solana-client = "3.1.9"
solana-client = "3.1.12"
solana-program = "3.0.0"
solana-rpc-client = "3.1.9"
solana-rpc-client-api = "3.1.9"
solana-transaction-status = "3.1.9"
solana-account-decoder = "3.1.9"
solana-rpc-client = "3.1.12"
solana-rpc-client-api = "3.1.12"
solana-transaction-status = "3.1.12"
solana-account-decoder = "3.1.12"
solana-hash = "3.0.0"
solana-entry = "3.1.9"
solana-rpc-client-nonce-utils = "3.1.9"
solana-perf = "3.1.9"
solana-metrics = "3.1.9"
solana-nonce = "3.1.0"
solana-address-lookup-table-interface = "3.0.1"
solana-entry = "3.0.0"
solana-rpc-client-nonce-utils = "3.1.12"
solana-perf = "3.1.12"
solana-metrics = "3.1.12"
solana-tls-utils = "3.1.12"
solana-nonce = "3.2.0"
solana-address-lookup-table-interface = "3.0.0"
solana-message = "3.1.0"
solana-compute-budget-interface = "3.0.0"
solana-commitment-config = { version = "3.1.1", features = ["serde"] }
solana-transaction-status-client-types = "3.1.9"
solana-tls-utils = "3.1.9"
solana-transaction-status-client-types = "3.1.12"
solana-system-interface = { version = "3.0.0", features = ["bincode"] }
borsh = { version = "1.5.3", features = ["derive"] }
isahc = "1.7.2"
@@ -76,7 +79,7 @@ bincode = "1.3.3"
anyhow = "1.0.90"
reqwest = { version = "0.12.12", features = ["json", "multipart"] }
tokio = { version = "1.42.0" , features = ["full", "rt-multi-thread"]}
tonic = { version = "0.14.2", features = ["transport"] }
tonic = { version = "0.12", features = ["transport"] }
rustls = { version = "0.23.23", features = ["ring"] }
rustls-native-certs = "0.8.1"
tokio-rustls = "0.26.1"
@@ -86,10 +89,9 @@ regex = "1"
tracing = "0.1.41"
thiserror = "2.0.11"
async-trait = "0.1.86"
lazy_static = "1.5.0"
once_cell = "1.20.3"
prost = "0.14.1"
prost-types = "0.14.1"
prost = "0.13"
prost-types = "0.13"
num_enum = "0.7.3"
num-derive = "0.4.2"
num-traits = "0.2.19"
@@ -98,7 +100,7 @@ bytemuck = { version = "1.4.0" }
arrayref = "0.3.6"
borsh-derive = "1.5.5"
indicatif = "0.18.0"
solana-system-interface = { version = "2.0.0", features = ["bincode"] }
fnv = "1.0.7"
dashmap = "6.1.0"
clru = "0.6"
@@ -135,6 +137,10 @@ incremental = true # 增量编译 - 大幅加速重新编译
opt-level = 1 # 开发时适度优化
overflow-checks = true # 开发时启用溢出检查
# 🚀 构建依赖
# 注意:proto 代码已预生成在 src/swqos/pb/serverpb.rs
# 开发者如需重新生成代码,请运行 gen_proto 目录下的工具
# 🚀 性能关键依赖的特殊优化
[profile.release.package.solana-sdk]
opt-level = 3
+21 -7
View File
@@ -61,6 +61,17 @@
---
## 📦 SDK Versions
This SDK is available in multiple languages:
| Language | Repository | Description |
|----------|------------|-------------|
| **Rust** | [sol-trade-sdk](https://github.com/0xfnzero/sol-trade-sdk) | Ultra-low latency with zero-copy optimization |
| **Node.js** | [sol-trade-sdk-nodejs](https://github.com/0xfnzero/sol-trade-sdk-nodejs) | TypeScript/JavaScript for Node.js |
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | Async/await native support |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | Concurrent-safe with goroutine support |
## ✨ Features
1. **PumpFun Trading**: Support for `buy` and `sell` operations
@@ -90,14 +101,14 @@ Add the dependency to your `Cargo.toml`:
```toml
# Add to your Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.6.4" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "3.6.4"
sol-trade-sdk = "4.0.3"
```
## 🛠️ Usage Examples
@@ -130,11 +141,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
), // QUIC
];
// Create TradeConfig instance
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
// Optional: customize WSOL ATA and seed optimization
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // default: true - check & create WSOL ATA on init
// .use_seed_optimize(true) // default: true - seed optimization for ATA ops
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
// .check_min_tip(false) // default: false - filter SWQOS below min tip
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
// .mev_protection(false) // default: false - MEV protection (Astralane port 9000 / BlockRazor sandwichMitigation)
.build();
// Create TradingClient
let client = TradingClient::new(Arc::new(payer), trade_config).await;
+21 -7
View File
@@ -61,6 +61,17 @@
---
## 📦 SDK 版本
本 SDK 提供多种语言版本:
| 语言 | 仓库 | 描述 |
|------|------|------|
| **Rust** | [sol-trade-sdk](https://github.com/0xfnzero/sol-trade-sdk) | 超低延迟,零拷贝优化 |
| **Node.js** | [sol-trade-sdk-nodejs](https://github.com/0xfnzero/sol-trade-sdk-nodejs) | TypeScript/JavaScriptNode.js 支持 |
| **Python** | [sol-trade-sdk-python](https://github.com/0xfnzero/sol-trade-sdk-python) | 原生 async/await 支持 |
| **Go** | [sol-trade-sdk-golang](https://github.com/0xfnzero/sol-trade-sdk-golang) | 并发安全,goroutine 支持 |
## ✨ 项目特性
1. **PumpFun 交易**: 支持`购买``卖出`功能
@@ -90,14 +101,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.6.4" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "3.6.4"
sol-trade-sdk = "4.0.3"
```
## 🛠️ 使用示例
@@ -130,11 +141,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
), // QUIC
];
// 创建 TradeConfig 实例
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
// 可选:自定义 WSOL ATA 与 Seed 优化
// let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment)
// .with_wsol_ata_config(true, true); // create_wsol_ata_on_startup, use_seed_optimize
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // 默认: true - 初始化时检查并创建 WSOL ATA
// .use_seed_optimize(true) // 默认: true - ATA 操作启用 seed 优化
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
// .mev_protection(false) // 默认: false - MEV 保护(Astralane 端口 9000 / BlockRazor sandwichMitigation
.build();
// 创建 TradingClient
let client = TradingClient::new(Arc::new(payer), trade_config).await;
-8
View File
@@ -1,8 +0,0 @@
# Release v3.6.4
## Changes from v3.6.3
- **Examples**: All workspace examples are verified to build successfully (`cargo build --workspace`).
- **Docs**: Version references in README.md and README_CN.md updated to 3.6.4.
No API or behavior changes in this release.
+19
View File
@@ -0,0 +1,19 @@
## sol-trade-sdk v4.0.2
This release focuses on QUIC reliability and low-latency submission stability for Astralane.
### Highlights
- Fixed Astralane QUIC address-family mismatch that could produce `invalid remote address` when DNS returned IPv6 first and local endpoint was IPv4-only.
- Added remote-family-aware local QUIC bind selection:
- IPv4 remote -> bind `0.0.0.0:0`
- IPv6 remote -> bind `[::]:0`
- Added Astralane direct-IP candidate support (official region IPs), with IPv4-first selection for better QUIC stability.
- Added automatic endpoint failover and reconnect rotation across candidate addresses, reducing single-endpoint/DNS variance impact.
- Kept existing SDK interfaces compatible while improving submit-path resiliency.
### Also included from recent updates
- BlockRazor gRPC endpoint fixes and gRPC default transport behavior improvements (v4.0.1).
- SWQOS transport path hardening and Binary-Tx response handling improvements (v4.0.0).
+9 -1
View File
@@ -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,
+8 -1
View File
@@ -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)
+8 -1
View File
@@ -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)
+8 -1
View File
@@ -1392,7 +1392,14 @@ async fn initialize_real_client() -> AnyResult<SolanaTrade> {
let rpc_url = RPC_URL.to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // default: true
// .use_seed_optimize(true) // default: true
// .log_enabled(true) // default: true
// .check_min_tip(false) // default: false
// .swqos_cores_from_end(false) // default: false
// .mev_protection(false) // default: false
.build();
let solana_trade = SolanaTrade::new(payer, trade_config).await;
println!("✅ SolanaTrade client initialized successfully!");
Ok(solana_trade)
@@ -107,7 +107,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // default: true
// .use_seed_optimize(true) // default: true
// .log_enabled(true) // default: true
// .check_min_tip(false) // default: false
// .swqos_cores_from_end(false) // default: false
// .mev_protection(false) // default: false
.build();
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
println!("✅ SolanaTrade client initialized successfully!");
Ok(solana_trade)
+8 -1
View File
@@ -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)
+9 -1
View File
@@ -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,
+10 -1
View File
@@ -114,7 +114,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // default: true
// .use_seed_optimize(true) // default: true
// .log_enabled(true) // default: true
// .check_min_tip(false) // default: false
// .swqos_cores_from_end(false) // default: false
// .mev_protection(false) // default: false
.build();
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
}
@@ -150,6 +157,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
@@ -199,6 +207,7 @@ async fn pumpfun_copy_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent)
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
+10 -1
View File
@@ -104,7 +104,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
.unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_string());
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // default: true
// .use_seed_optimize(true) // default: true
// .log_enabled(true) // default: true
// .check_min_tip(false) // default: false
// .swqos_cores_from_end(false) // default: false
// .mev_protection(false) // default: false
.build();
Ok(SolanaTrade::new(Arc::new(payer), trade_config).await)
}
@@ -140,6 +147,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
@@ -183,6 +191,7 @@ async fn pumpfun_sniper_trade(e: sol_parser_sdk::core::events::PumpFunTradeEvent
e.fee_recipient,
e.token_program,
e.is_cashback_coin,
Some(e.mayhem_mode),
)),
address_lookup_table_account: None,
wait_transaction_confirmed: true,
+8 -1
View File
@@ -104,7 +104,14 @@ async fn create_solana_trade_client() -> AnyResult<SolanaTrade> {
let rpc_url = "https://api.mainnet-beta.solana.com".to_string();
let commitment = CommitmentConfig::confirmed();
let swqos_configs: Vec<SwqosConfig> = vec![SwqosConfig::Default(rpc_url.clone())];
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
// .create_wsol_ata_on_startup(true) // default: true
// .use_seed_optimize(true) // default: true
// .log_enabled(true) // default: true
// .check_min_tip(false) // default: false
// .swqos_cores_from_end(false) // default: false
// .mev_protection(false) // default: false
.build();
let solana_trade = SolanaTrade::new(Arc::new(payer), trade_config).await;
println!("✅ SolanaTrade client initialized successfully!");
Ok(solana_trade)
+8 -1
View File
@@ -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)
+8 -1
View File
@@ -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)
+8 -1
View File
@@ -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)
+8 -1
View File
@@ -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)
+4 -5
View File
@@ -58,11 +58,10 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
];
// Optional: Customize WSOL ATA and Seed optimization settings
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment).with_wsol_ata_config(
true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup
true, // use_seed_optimize: Enable seed optimization for all ATA operations
);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.create_wsol_ata_on_startup(true)
.use_seed_optimize(true)
.build();
// Creates new infrastructure internally
let client = TradingClient::new(Arc::new(payer), trade_config).await;
+8 -1
View File
@@ -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)
+26 -4
View File
@@ -1,17 +1,39 @@
use crate::common::SolanaRpcClient;
use anyhow::Result;
use solana_address_lookup_table_interface::state::AddressLookupTable;
use solana_sdk::{message::AddressLookupTableAccount, pubkey::Pubkey};
use solana_message::AddressLookupTableAccount;
use solana_sdk::pubkey::Pubkey;
pub async fn fetch_address_lookup_table_account(
rpc: &SolanaRpcClient,
lookup_table_address: &Pubkey,
) -> Result<AddressLookupTableAccount, anyhow::Error> {
let account = rpc.get_account(lookup_table_address).await?;
let lookup_table = AddressLookupTable::deserialize(&account.data)?;
// Parse address lookup table manually
// Layout: 4 bytes (type) + 4 bytes (deactivation_slot) + 4 bytes (last_extended_slot) + 1 byte (last_extended_slot_start_index) + 1 byte (authority) + padding
// Then addresses start at offset 56, each address is 32 bytes
// First 4 bytes indicate if initialized (should be 1 or 2)
if account.data.len() < 56 {
return Err(anyhow::anyhow!("Address lookup table account data too short"));
}
// Read number of addresses (stored at offset 20 as u32, but we need to scan the bitmap)
// Actually simpler: addresses start at offset 56, count from bitmap at offset 8-20
let mut addresses = Vec::new();
let mut offset = 56;
while offset + 32 <= account.data.len() {
let addr_bytes: [u8; 32] = account.data[offset..offset + 32].try_into()?;
// Skip zero addresses (unused slots)
if addr_bytes != [0u8; 32] {
addresses.push(Pubkey::from(addr_bytes));
}
offset += 32;
}
let address_lookup_table_account = AddressLookupTableAccount {
key: *lookup_table_address,
addresses: lookup_table.addresses.to_vec(),
addresses,
};
Ok(address_lookup_table_account)
}
+14 -14
View File
@@ -1,8 +1,5 @@
use crate::common::SolanaRpcClient;
use solana_hash::Hash;
use solana_nonce::state::State;
use solana_nonce::versions::Versions;
use solana_sdk::account_utils::StateMut;
use solana_sdk::pubkey::Pubkey;
use tracing::error;
@@ -21,18 +18,21 @@ pub async fn fetch_nonce_info(
nonce_account: Pubkey,
) -> Option<DurableNonceInfo> {
match rpc.get_account(&nonce_account).await {
Ok(account) => match account.state() {
Ok(Versions::Current(state)) => {
if let State::Initialized(data) = *state {
let blockhash = data.durable_nonce.as_hash();
return Some(DurableNonceInfo {
nonce_account: Some(nonce_account),
current_nonce: Some(*blockhash),
});
}
Ok(account) => {
// Parse nonce account manually: first 4 bytes is version, then 4 bytes authority type
// For initialized nonce: version=0, authority_type=0, then authority (32 bytes), then blockhash (32 bytes), then fee_calculator
if account.data.len() >= 80 {
// Skip version (4) + authority_type (4) + authority (32) = 40 bytes
// Then blockhash is at offset 40
let blockhash_bytes: [u8; 32] = account.data[40..72].try_into().ok()?;
return Some(DurableNonceInfo {
nonce_account: Some(nonce_account),
current_nonce: Some(Hash::from(blockhash_bytes)),
});
} else {
error!("Nonce account data too short");
}
_ => (),
},
}
Err(e) => {
error!("Failed to get nonce account information: {:?}", e);
}
+4 -3
View File
@@ -3,7 +3,7 @@ use anyhow::anyhow;
use fnv::FnvHasher;
use once_cell::sync::Lazy;
use solana_sdk::{instruction::Instruction, pubkey::Pubkey};
use solana_system_interface::instruction::create_account_with_seed;
use solana_system_interface::instruction as system_instruction;
use std::hash::Hasher;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
@@ -92,9 +92,10 @@ pub fn create_associated_token_account_use_seed(
let ata_like = Pubkey::create_with_seed(payer, seed, token_program)?;
let len = 165;
// 但账户的 owner 仍然使用正确的 token_programToken 或 Token-2022
// 🔧 修复:create_account_with_seed 的第3个参数必须是 payer(与第92行生成地址时使用的 base 一致
// 否则创建的账户地址与 ata_like 不匹配,导致 initializeAccount3 失败
let create_acc =
create_account_with_seed(payer, &ata_like, owner, seed, rent, len, token_program);
system_instruction::create_account_with_seed(payer, &ata_like, payer, seed, rent, len, token_program);
let init_acc = if is_2022_token {
crate::common::spl_token_2022::initialize_account3(&token_program, &ata_like, mint, owner)?
+1 -1
View File
@@ -1,5 +1,5 @@
use solana_sdk::{
message::{AccountMeta, Instruction},
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
+5 -5
View File
@@ -1,6 +1,6 @@
use solana_program::pubkey;
use solana_sdk::{
message::{AccountMeta, Instruction},
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
};
@@ -18,14 +18,14 @@ pub fn close_account(
let mut data = Vec::with_capacity(1);
data.push(9);
let mut accounts = Vec::with_capacity(3 + signer_pubkeys.len());
accounts.push(solana_sdk::message::AccountMeta::new(*account_pubkey, false));
accounts.push(solana_sdk::message::AccountMeta::new(*destination_pubkey, false));
accounts.push(solana_sdk::message::AccountMeta::new_readonly(
accounts.push(AccountMeta::new(*account_pubkey, false));
accounts.push(AccountMeta::new(*destination_pubkey, false));
accounts.push(AccountMeta::new_readonly(
*owner_pubkey,
signer_pubkeys.is_empty(),
));
for signer_pubkey in signer_pubkeys.iter() {
accounts.push(solana_sdk::message::AccountMeta::new_readonly(**signer_pubkey, true));
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
}
Ok(Instruction { program_id: *token_program_id, accounts, data })
}
+1 -1
View File
@@ -1,6 +1,6 @@
use solana_program::pubkey;
use solana_sdk::{
message::{AccountMeta, Instruction},
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
};
+115 -22
View File
@@ -11,6 +11,10 @@ pub struct InfrastructureConfig {
pub commitment: CommitmentConfig,
/// When true, SWQOS sender threads use the *last* N cores instead of the first N. Reduces contention with main thread / default tokio workers that often use low-numbered cores. Default false.
pub swqos_cores_from_end: bool,
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
/// (Astralane QUIC port 9000, BlockRazor revert_protection) will use their MEV-protected
/// endpoints/modes. Default false.
pub mev_protection: bool,
}
impl InfrastructureConfig {
@@ -24,6 +28,7 @@ impl InfrastructureConfig {
swqos_configs,
commitment,
swqos_cores_from_end: false,
mev_protection: false,
}
}
@@ -34,6 +39,7 @@ impl InfrastructureConfig {
swqos_configs: config.swqos_configs.clone(),
commitment: config.commitment.clone(),
swqos_cores_from_end: config.swqos_cores_from_end,
mev_protection: config.mev_protection,
}
}
@@ -53,6 +59,7 @@ impl Hash for InfrastructureConfig {
self.swqos_configs.hash(state);
format!("{:?}", self.commitment).hash(state);
self.swqos_cores_from_end.hash(state);
self.mev_protection.hash(state);
}
}
@@ -62,6 +69,7 @@ impl PartialEq for InfrastructureConfig {
&& self.swqos_configs == other.swqos_configs
&& self.commitment == other.commitment
&& self.swqos_cores_from_end == other.swqos_cores_from_end
&& self.mev_protection == other.mev_protection
}
}
@@ -83,52 +91,137 @@ pub struct TradeConfig {
pub check_min_tip: bool,
/// When true, SWQOS uses the *last* N cores (instead of the first N). Use when main thread / tokio use low-numbered cores to reduce CPU contention. Default false.
pub swqos_cores_from_end: bool,
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
/// (Astralane QUIC port 9000, BlockRazor sandwichMitigation mode) will use their
/// MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
pub mev_protection: bool,
}
impl TradeConfig {
/// Create a new TradeConfig using the builder pattern.
///
/// # Available builder methods
/// - `.create_wsol_ata_on_startup(bool)` — check & create WSOL ATA on init (default: true)
/// - `.use_seed_optimize(bool)` — seed optimization for ATA ops (default: true)
/// - `.log_enabled(bool)` — SDK timing/SWQOS logs (default: true)
/// - `.check_min_tip(bool)` — filter SWQOS below min tip (default: false)
/// - `.swqos_cores_from_end(bool)` — bind SWQOS to last N cores (default: false)
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
///
/// # Example
/// ```rust
/// let config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
/// .mev_protection(true)
/// .check_min_tip(true)
/// .log_enabled(false)
/// .build();
/// ```
pub fn builder(
rpc_url: String,
swqos_configs: Vec<SwqosConfig>,
commitment: CommitmentConfig,
) -> TradeConfigBuilder {
TradeConfigBuilder::new(rpc_url, swqos_configs, commitment)
}
/// Shortcut: create a TradeConfig with all defaults. Equivalent to `builder(...).build()`.
pub fn new(
rpc_url: String,
swqos_configs: Vec<SwqosConfig>,
commitment: CommitmentConfig,
) -> Self {
if crate::common::sdk_log::sdk_log_enabled() {
println!("🔧 TradeConfig create_wsol_ata_on_startup default: true");
println!("🔧 TradeConfig use_seed_optimize default: true");
}
Self::builder(rpc_url, swqos_configs, commitment).build()
}
}
/// Builder for [`TradeConfig`]. Created via [`TradeConfig::builder`].
///
/// All fields are optional and pre-filled with sensible defaults.
/// Call `.build()` to produce the final [`TradeConfig`].
#[derive(Debug, Clone)]
pub struct TradeConfigBuilder {
rpc_url: String,
swqos_configs: Vec<SwqosConfig>,
commitment: CommitmentConfig,
create_wsol_ata_on_startup: bool,
use_seed_optimize: bool,
log_enabled: bool,
check_min_tip: bool,
swqos_cores_from_end: bool,
mev_protection: bool,
}
impl TradeConfigBuilder {
fn new(rpc_url: String, swqos_configs: Vec<SwqosConfig>, commitment: CommitmentConfig) -> Self {
Self {
rpc_url,
swqos_configs,
commitment,
create_wsol_ata_on_startup: true, // default: check and create on startup
use_seed_optimize: true, // default: use seed optimization
log_enabled: true, // default: enable all SDK logs
check_min_tip: false, // default: skip min tip check to reduce latency
create_wsol_ata_on_startup: true,
use_seed_optimize: true,
log_enabled: true,
check_min_tip: false,
swqos_cores_from_end: false,
mev_protection: false,
}
}
/// Create a TradeConfig with custom WSOL ATA settings
pub fn with_wsol_ata_config(
mut self,
create_wsol_ata_on_startup: bool,
use_seed_optimize: bool,
) -> Self {
self.create_wsol_ata_on_startup = create_wsol_ata_on_startup;
self.use_seed_optimize = use_seed_optimize;
/// Check and create WSOL ATA on SDK initialization. Default: `true`.
pub fn create_wsol_ata_on_startup(mut self, v: bool) -> Self {
self.create_wsol_ata_on_startup = v;
self
}
/// Set whether to check minimum tip per SWQOS (filter out configs below min). Default false for lower latency.
pub fn with_check_min_tip(mut self, check_min_tip: bool) -> Self {
self.check_min_tip = check_min_tip;
/// Enable seed optimization for all ATA operations. Default: `true`.
pub fn use_seed_optimize(mut self, v: bool) -> Self {
self.use_seed_optimize = v;
self
}
/// Use the *last* N cores for SWQOS (instead of the first N). Call this when the main thread or tokio workers use low-numbered cores to avoid binding SWQOS to busy cores. Default false.
pub fn with_swqos_cores_from_end(mut self, from_end: bool) -> Self {
self.swqos_cores_from_end = from_end;
/// Enable SDK logs (timing, SWQOS submit/confirm, WSOL, blacklist, etc.). Default: `true`.
pub fn log_enabled(mut self, v: bool) -> Self {
self.log_enabled = v;
self
}
/// Filter out SWQOS providers whose tip is below their minimum requirement.
/// Adds a small check on the hot path; disable for lowest latency. Default: `false`.
pub fn check_min_tip(mut self, v: bool) -> Self {
self.check_min_tip = v;
self
}
/// Bind SWQOS sender threads to the *last* N CPU cores instead of the first N.
/// Useful when main thread / tokio workers occupy low-numbered cores. Default: `false`.
pub fn swqos_cores_from_end(mut self, v: bool) -> Self {
self.swqos_cores_from_end = v;
self
}
/// Enable global MEV protection. When `true`:
/// - **Astralane QUIC** uses port `9000` (MEV-protected endpoint)
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
///
/// May reduce landing speed. Default: `false`.
pub fn mev_protection(mut self, v: bool) -> Self {
self.mev_protection = v;
self
}
/// Consume the builder and produce a [`TradeConfig`].
pub fn build(self) -> TradeConfig {
TradeConfig {
rpc_url: self.rpc_url,
swqos_configs: self.swqos_configs,
commitment: self.commitment,
create_wsol_ata_on_startup: self.create_wsol_ata_on_startup,
use_seed_optimize: self.use_seed_optimize,
log_enabled: self.log_enabled,
check_min_tip: self.check_min_tip,
swqos_cores_from_end: self.swqos_cores_from_end,
mev_protection: self.mev_protection,
}
}
}
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
+43
View File
@@ -111,6 +111,7 @@ pub const BLOCKRAZOR_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"),
];
/// Astralane tip wallets. Extended with new addresses for improved routing and reduced write-lock contention (see portal.astralane.io/blockline).
pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF"),
pubkey!("astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm"),
@@ -120,6 +121,16 @@ pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"),
pubkey!("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"),
pubkey!("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"),
// New tip wallets (2025) for improved performance and reduced write-lock delays
pubkey!("AstrA1ejL4UeXC2SBP4cpeEmtcFPZVLxx3XGKXyCW6to"),
pubkey!("AsTra79FET4aCKWspPqeSFvjJNyp96SvAnrmyAxqg5b7"),
pubkey!("AstrABAu8CBTyuPXpV4eSCJ5fePEPnxN8NqBaPKQ9fHR"),
pubkey!("AsTRADtvb6tTmrsqULQ9Wji9PigDMjhfEMza6zkynEvV"),
pubkey!("AsTRAEoyMofR3vUPpf9k68Gsfb6ymTZttEtsAbv8Bk4d"),
pubkey!("AStrAJv2RN2hKCHxwUMtqmSxgdcNZbihCwc1mCSnG83W"),
pubkey!("Astran35aiQUF57XZsmkWMtNCtXGLzs8upfiqXxth2bz"),
pubkey!("AStRAnpi6kFrKypragExgeRoJ1QnKH7pbSjLAKQVWUum"),
pubkey!("ASTRaoF93eYt73TYvwtsv6fMWHWbGmMUZfVZPo3CRU9C"),
];
pub const STELLIUM_TIP_ACCOUNTS: &[Pubkey] = &[
@@ -141,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] = &[
@@ -266,6 +283,19 @@ pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
];
/// BlockRazor gRPC endpoints. Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
/// Port 80 for gRPC protocol. Auth: apikey metadata in gRPC headers.
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 8] = [
"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://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",
];
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
@@ -293,6 +323,19 @@ pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
"lim.gateway.astralane.io:7000", // Default
];
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
/// Use these when mev_protection=true to route through Astralane's MEV-protected path.
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 8] = [
"ny.gateway.astralane.io:9000", // NewYork
"fr.gateway.astralane.io:9000", // Frankfurt
"ams.gateway.astralane.io:9000", // Amsterdam
"lim.gateway.astralane.io:9000", // SLC (no slc, use lim)
"sg.gateway.astralane.io:9000", // Tokyo (Asia)
"ams.gateway.astralane.io:9000", // London (Europe)
"la.gateway.astralane.io:9000", // LosAngeles
"lim.gateway.astralane.io:9000", // Default
];
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
"http://ewr1.flashrpc.com",
"http://fra1.flashrpc.com",
+54 -50
View File
@@ -8,8 +8,9 @@ use crate::{
};
use crate::{
instruction::utils::pumpfun::{
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
get_mayhem_fee_recipient_meta_random, get_user_volume_accumulator_pda,
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta,
resolve_creator_vault_for_ix,
global_constants::{self},
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
@@ -42,8 +43,20 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
}
let bonding_curve = &protocol_params.bonding_curve;
let creator_vault_pda = protocol_params.creator_vault;
let creator = get_creator(&creator_vault_pda);
// creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived;
// if stream sends a mismatched vault (wrong token / stale), fall back to derived.
let creator = bonding_curve.creator;
let creator_vault_pda = resolve_creator_vault_for_ix(
&creator,
protocol_params.creator_vault,
&params.output_mint,
)
.ok_or_else(|| {
anyhow!(
"creator_vault PDA derivation failed (creator={})",
creator
)
})?;
// ========================================
// Trade calculation and account address preparation
@@ -64,13 +77,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() {
get_bonding_curve_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
})?
} else {
bonding_curve.account
};
// 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子,
// 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。
let bonding_curve_addr = get_bonding_curve_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
})?;
// Determine token program based on mayhem mode
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
@@ -82,15 +93,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
};
let associated_bonding_curve =
if protocol_params.associated_bonding_curve == Pubkey::default() {
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&bonding_curve_addr,
&params.output_mint,
&token_program,
)
} else {
protocol_params.associated_bonding_curve
};
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&bonding_curve_addr,
&params.output_mint,
&token_program,
);
let user_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
@@ -106,6 +113,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
// ========================================
// Build instructions
// ========================================
// Hot path: no RPC here (latency). For legacy curves &lt;151 bytes, use
// `extend_bonding_curve_account_instruction` from a cold path or separate tx.
let mut instructions = Vec::with_capacity(2);
// Create associated token account
@@ -142,12 +151,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
buy_data[24..26].copy_from_slice(&track_volume);
}
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
let fee_recipient_meta = if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
global_constants::FEE_RECIPIENT_META
};
// Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。
let fee_recipient_meta =
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
@@ -197,8 +203,18 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
};
let bonding_curve = &protocol_params.bonding_curve;
let creator_vault_pda = protocol_params.creator_vault;
let creator = get_creator(&creator_vault_pda);
let creator = bonding_curve.creator;
let creator_vault_pda = resolve_creator_vault_for_ix(
&creator,
protocol_params.creator_vault,
&params.input_mint,
)
.ok_or_else(|| {
anyhow!(
"creator_vault PDA derivation failed (creator={})",
creator
)
})?;
// ========================================
// Trade calculation and account address preparation
@@ -218,13 +234,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
),
};
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() {
get_bonding_curve_pda(&params.input_mint).ok_or_else(|| {
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
})?
} else {
bonding_curve.account
};
let bonding_curve_addr = get_bonding_curve_pda(&params.input_mint).ok_or_else(|| {
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
})?;
// Determine token program based on mayhem mode
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
@@ -236,15 +248,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
};
let associated_bonding_curve =
if protocol_params.associated_bonding_curve == Pubkey::default() {
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&bonding_curve_addr,
&params.input_mint,
&token_program,
)
} else {
protocol_params.associated_bonding_curve
};
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&bonding_curve_addr,
&params.input_mint,
&token_program,
);
let user_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
@@ -264,12 +272,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
let fee_recipient_meta = if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
global_constants::FEE_RECIPIENT_META
};
let fee_recipient_meta =
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
+11 -1
View File
@@ -139,8 +139,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let mut instructions = Vec::with_capacity(6);
if create_wsol_ata {
// Determine wrap amount based on instruction type:
// - buy_exact_quote_in: program spends exactly input_amount, wrap input_amount
// - buy: program may spend up to max_quote, wrap max_quote
let wrap_amount = if quote_is_wsol_or_usdc
&& params.use_exact_sol_amount.unwrap_or(true)
{
params.input_amount.unwrap_or(0)
} else {
sol_amount
};
instructions
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), sol_amount));
.extend(crate::trading::common::handle_wsol(&params.payer.pubkey(), wrap_amount));
}
if params.create_output_mint_ata {
+216 -2
View File
@@ -1,9 +1,40 @@
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
use anyhow::anyhow;
use rand::seq::IndexedRandom;
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
// --- Aligned with official `@pump-fun/pump-sdk` (npm) ---
// - `src/fees.ts` `getFeeRecipient(global, mayhemMode)` — fee recipient pools
// - `src/bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient`
// - `src/sdk.ts` `BONDING_CURVE_NEW_SIZE` (151) + `extendAccountInstruction` — **not** called from the
// trade hot path here (no RPC in `PumpFunInstructionBuilder`); use these helpers from a cold path if needed.
/// Minimum bonding curve account data length after protocol upgrades (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
/// Anchor discriminator for `extend_account` (`pump.json`); same as `PumpSdk.extendAccountInstruction`.
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
/// Build `extend_account` for bonding curve (cold path / separate tx only — do not add RPC to hot-path builds).
#[inline]
pub fn extend_bonding_curve_account_instruction(bonding_curve: &Pubkey, user: &Pubkey) -> Instruction {
Instruction {
program_id: accounts::PUMPFUN,
accounts: vec![
AccountMeta::new(*bonding_curve, false),
AccountMeta::new(*user, true),
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::PUMPFUN_META,
],
data: EXTEND_ACCOUNT_DISCRIMINATOR.to_vec(),
}
}
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
pub mod seeds {
/// Seed for bonding curve PDAs
@@ -24,6 +55,9 @@ pub mod seeds {
pub const GLOBAL_VOLUME_ACCUMULATOR_SEED: &[u8] = b"global_volume_accumulator";
pub const FEE_CONFIG_SEED: &[u8] = b"fee_config";
/// `feeSharingConfig` PDA under pump-fees (`@pump-fun/pump-sdk` `feeSharingConfigPda`)
pub const SHARING_CONFIG_SEED: &[u8] = b"sharing-config";
}
pub mod global_constants {
@@ -172,7 +206,26 @@ pub const BUY_DISCRIMINATOR: [u8; 8] = [102, 6, 61, 18, 1, 218, 235, 234];
pub const BUY_EXACT_SOL_IN_DISCRIMINATOR: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95];
pub const SELL_DISCRIMINATOR: [u8; 8] = [51, 230, 133, 164, 1, 127, 131, 173];
/// Returns a random Mayhem fee recipient AccountMeta (pump-public-docs: Bonding Curve 2nd account = Mayhem fee recipient; use any one randomly).
/// Check if a pubkey is one of the Mayhem fee recipients
#[inline]
pub fn is_mayhem_fee_recipient(pubkey: &Pubkey) -> bool {
global_constants::MAYHEM_FEE_RECIPIENTS.iter().any(|p| p == pubkey)
}
/// Check if a pubkey is a Pump.fun AMM protocol fee recipient (PUMPFUN_AMM_FEE_1..7)
#[inline]
pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
pubkey == &global_constants::PUMPFUN_AMM_FEE_1
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_2
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_3
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_4
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_5
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_6
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
}
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
#[inline]
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
@@ -181,6 +234,46 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
}
/// Non-mayhem: random among `Global::fee_recipient` + `Global::fee_recipients[0..7]`.
/// Same pubkey set as `bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient` and `fees.ts` `getFeeRecipient` when `mayhemMode === false`.
#[inline]
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
const POOL: &[Pubkey] = &[
global_constants::FEE_RECIPIENT,
global_constants::PUMPFUN_AMM_FEE_1,
global_constants::PUMPFUN_AMM_FEE_2,
global_constants::PUMPFUN_AMM_FEE_3,
global_constants::PUMPFUN_AMM_FEE_4,
global_constants::PUMPFUN_AMM_FEE_5,
global_constants::PUMPFUN_AMM_FEE_6,
global_constants::PUMPFUN_AMM_FEE_7,
];
let recipient = *POOL
.choose(&mut rand::rng())
.unwrap_or(&global_constants::FEE_RECIPIENT);
AccountMeta {
pubkey: recipient,
is_signer: false,
is_writable: true,
}
}
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。
#[inline]
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
if from_stream != Pubkey::default() {
AccountMeta {
pubkey: from_stream,
is_signer: false,
is_writable: true,
}
} else if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
get_standard_fee_recipient_meta_random()
}
}
pub struct Symbol;
impl Symbol {
@@ -242,6 +335,76 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
)
}
/// `feeSharingConfig` PDA per mint (`pump-sdk` `feeSharingConfigPda` → `pump-fees` program).
#[inline]
pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
Pubkey::try_find_program_address(
&[seeds::SHARING_CONFIG_SEED, mint.as_ref()],
&accounts::FEE_PROGRAM,
)
.map(|(p, _)| p)
}
/// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when
/// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006.
#[inline]
pub fn phantom_default_creator_vault() -> Pubkey {
solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc")
}
#[inline]
pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool {
*pk == phantom_default_creator_vault()
}
/// Resolve `creator_vault` for Pump buy/sell account #10.
///
/// - If `creator` is **missing** in the outer trade-event borsh (`Pubkey::default()`) but
/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9),
/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache).
/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`).
/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix).
/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use
/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right).
#[inline]
pub fn resolve_creator_vault_for_ix(
creator: &Pubkey,
creator_vault_from_event: Pubkey,
mint: &Pubkey,
) -> Option<Pubkey> {
let phantom = phantom_default_creator_vault();
if *creator == Pubkey::default() {
if creator_vault_from_event == Pubkey::default() {
return None;
}
if creator_vault_from_event == phantom {
return None;
}
return Some(creator_vault_from_event);
}
// Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator).
if creator_vault_from_event == phantom {
return get_creator_vault_pda(creator);
}
let v_derived = get_creator_vault_pda(creator)?;
if creator_vault_from_event == Pubkey::default() {
return Some(v_derived);
}
if creator_vault_from_event == v_derived {
return Some(creator_vault_from_event);
}
if let Some(sharing) = get_fee_sharing_config_pda(mint) {
let v_sharing = get_creator_vault_pda(&sharing)?;
if creator_vault_from_event == v_sharing {
return Some(creator_vault_from_event);
}
}
Some(v_derived)
}
#[inline]
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
crate::common::fast_fn::get_cached_pda(
@@ -322,4 +485,55 @@ mod tests {
let b = get_creator_vault_pda(&creator).unwrap();
assert_eq!(a, b);
}
#[test]
fn fee_sharing_config_pda_deterministic() {
let mint = Pubkey::new_unique();
let a = get_fee_sharing_config_pda(&mint).unwrap();
let b = get_fee_sharing_config_pda(&mint).unwrap();
assert_eq!(a, b);
}
#[test]
fn default_creator_yields_fixed_creator_vault() {
let v = get_creator_vault_pda(&Pubkey::default()).unwrap();
assert_eq!(v, phantom_default_creator_vault(), "phantom vault constant must match PDA(default creator)");
}
#[test]
fn resolve_uses_ix_vault_when_creator_borsh_is_default() {
let mint = Pubkey::new_unique();
let ix_vault = Pubkey::new_unique();
let resolved = resolve_creator_vault_for_ix(&Pubkey::default(), ix_vault, &mint);
assert_eq!(resolved, Some(ix_vault));
}
#[test]
fn resolve_returns_none_when_creator_and_vault_missing() {
let mint = Pubkey::new_unique();
assert_eq!(
resolve_creator_vault_for_ix(&Pubkey::default(), Pubkey::default(), &mint),
None
);
}
#[test]
fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() {
let mint = Pubkey::new_unique();
assert_eq!(
resolve_creator_vault_for_ix(&Pubkey::default(), phantom_default_creator_vault(), &mint),
None
);
}
#[test]
fn resolve_remaps_phantom_vault_when_creator_known() {
let creator = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let expected = get_creator_vault_pda(&creator).unwrap();
assert_eq!(
resolve_creator_vault_for_ix(&creator, phantom_default_creator_vault(), &mint),
Some(expected)
);
}
}
+71 -95
View File
@@ -10,9 +10,7 @@ use rand::seq::IndexedRandom;
use solana_account_decoder::UiAccountEncoding;
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
/// PumpSwap 池账户总长度(见 pump-public-docs Breaking Change):8 字节 discriminator + 244 字节 Pool。
/// 官方文档:pool structure needs to be 244 bytes (was 243),含 is_mayhem_mode。DataSize 必须与此一致,否则 getProgramAccounts 会返回 0。
const POOL_ACCOUNT_DATA_LEN: u64 = 8 + 244;
// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22)
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
pub mod seeds {
@@ -301,116 +299,90 @@ pub async fn fetch_pool(
Ok(pool)
}
pub async fn find_by_base_mint(
/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022)
const POOL_DATA_LEN_SPL: u64 = 8 + 244;
const POOL_DATA_LEN_T22: u64 = 643;
/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel.
async fn get_program_accounts_both_sizes(
rpc: &SolanaRpcClient,
base_mint: &Pubkey,
) -> Result<(Pubkey, Pool), anyhow::Error> {
// Use getProgramAccounts to find pools for the given mint.
// base_mint 在账户布局中的偏移:8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
let filters = vec![
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
solana_client::rpc_filter::Memcmp::new_base58_encoded(43, base_mint.as_ref()),
),
];
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
filters: Some(filters),
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
data_slice: None,
commitment: None,
min_context_slot: None,
},
with_context: None,
sort_results: None,
memcmp_offset: usize,
mint: &Pubkey,
) -> Result<Vec<(Pubkey, solana_sdk::account::Account)>, anyhow::Error> {
let make_config = |data_size: u64| {
solana_rpc_client_api::config::RpcProgramAccountsConfig {
filters: Some(vec![
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
),
]),
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
data_slice: None,
commitment: None,
min_context_slot: None,
},
with_context: None,
sort_results: None,
}
};
let program_id = accounts::AMM_PROGRAM;
#[allow(deprecated)]
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", base_mint));
}
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
let mut pools: Vec<_> = accounts
let (spl_result, t22_result) = tokio::join!(
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)),
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)),
);
let mut all = spl_result.unwrap_or_default();
all.extend(t22_result.unwrap_or_default());
Ok(all)
}
fn decode_pool_accounts(accounts: Vec<(Pubkey, solana_sdk::account::Account)>) -> Vec<(Pubkey, Pool)> {
accounts
.into_iter()
.filter_map(|(addr, acc)| {
// 🔧 修复:跳过8字节的discriminator
if acc.data.len() > 8 {
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
} else {
None
}
})
.collect();
.collect()
}
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
if pools.is_empty() {
return Err(anyhow!(
"No valid pool decoded for mint {} (found {} accounts but all decode failed)",
base_mint,
accounts_count
));
pub async fn find_by_base_mint(
rpc: &SolanaRpcClient,
base_mint: &Pubkey,
) -> Result<(Pubkey, Pool), anyhow::Error> {
// base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?;
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", base_mint));
}
let mut pools = decode_pool_accounts(accounts);
if pools.is_empty() {
return Err(anyhow!("No valid pool decoded for mint {}", base_mint));
}
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
let first = &pools[0];
Ok((first.0, first.1.clone()))
Ok((pools[0].0, pools[0].1.clone()))
}
pub async fn find_by_quote_mint(
rpc: &SolanaRpcClient,
quote_mint: &Pubkey,
) -> Result<(Pubkey, Pool), anyhow::Error> {
// Use getProgramAccounts to find pools for the given mint.
// quote_mint 在账户布局中的偏移:8 + 1 + 2 + 32 + 32 = 75
let filters = vec![
solana_rpc_client_api::filter::RpcFilterType::DataSize(POOL_ACCOUNT_DATA_LEN),
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
solana_client::rpc_filter::Memcmp::new_base58_encoded(75, quote_mint.as_ref()),
),
];
let config = solana_rpc_client_api::config::RpcProgramAccountsConfig {
filters: Some(filters),
account_config: solana_rpc_client_api::config::RpcAccountInfoConfig {
encoding: Some(UiAccountEncoding::Base64),
data_slice: None,
commitment: None,
min_context_slot: None,
},
with_context: None,
sort_results: None,
};
let program_id = accounts::AMM_PROGRAM;
#[allow(deprecated)]
let accounts = rpc.get_program_accounts_with_config(&program_id, config).await?;
// quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75
let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?;
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", quote_mint));
}
let accounts_count = accounts.len(); // 🔧 保存长度,因为 into_iter() 会消耗 accounts
let mut pools: Vec<_> = accounts
.into_iter()
.filter_map(|(addr, acc)| {
// 🔧 修复:跳过8字节的discriminator
if acc.data.len() > 8 {
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
} else {
None
}
})
.collect();
// 🔧 修复:检查过滤后的 pools 是否为空(accounts 可能不为空但解码全部失败)
let mut pools = decode_pool_accounts(accounts);
if pools.is_empty() {
return Err(anyhow!(
"No valid pool decoded for quote_mint {} (found {} accounts but all decode failed)",
quote_mint,
accounts_count
));
return Err(anyhow!("No valid pool decoded for quote_mint {}", quote_mint));
}
pools.sort_by(|a, b| b.1.lp_supply.cmp(&a.1.lp_supply));
let first = &pools[0];
Ok((first.0, first.1.clone()))
Ok((pools[0].0, pools[0].1.clone()))
}
/// 按 mint 查找 PumpSwap 池(本函数仅用于 PumpSwap,其他 DEX 勿用)。
@@ -444,20 +416,24 @@ pub async fn find_by_mint(
Err(e) => diag.push(format!("canonical get_account/decode 失败: {}", e)),
}
// 3. 回退:getProgramAccounts base_mint / quote_mint
match find_by_base_mint(rpc, mint).await {
Ok((address, pool)) => return Ok((address, pool)),
Err(e) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
// 3. Fallback: getProgramAccounts by base_mint / quote_mint (with 3s timeout to avoid blocking)
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_base_mint(rpc, mint)).await {
Ok(Ok((address, pool))) => return Ok((address, pool)),
Ok(Err(e)) => diag.push(format!("getProgramAccounts(base_mint): {}", e)),
Err(_) => diag.push("getProgramAccounts(base_mint): timed out (3s)".into()),
}
match find_by_quote_mint(rpc, mint).await {
Ok((address, pool)) => return Ok((address, pool)),
Err(e) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
match tokio::time::timeout(std::time::Duration::from_secs(3), find_by_quote_mint(rpc, mint)).await {
Ok(Ok((address, pool))) => return Ok((address, pool)),
Ok(Err(e)) => diag.push(format!("getProgramAccounts(quote_mint): {}", e)),
Err(_) => diag.push("getProgramAccounts(quote_mint): timed out (3s)".into()),
}
let diag_str = diag.join("; ");
eprintln!("[find_by_mint] {} failed: {}", mint, diag_str);
Err(anyhow!(
"No pool found for mint {}. 诊断: {}。若使用自建 RPC 请确认已开启 getProgramAccounts 或换用公共 RPC 重试;若代币未在 PumpSwap 建池请先在 pump.fun/DEX 上确认",
"No pool found for mint {}. diag: {}",
mint,
diag.join("; ")
diag_str
))
}
+8 -7
View File
@@ -152,6 +152,7 @@ impl TradingInfrastructure {
config.rpc_url.clone(),
config.commitment.clone(),
swqos.clone(),
config.mev_protection,
),
)
.await
@@ -740,7 +741,7 @@ impl TradingClient {
pub async fn buy(
&self,
params: TradeBuyParams,
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
if params.recent_blockhash.is_none() && params.durable_nonce.is_none() {
return Err(anyhow::anyhow!(
"Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)"
@@ -814,8 +815,8 @@ impl TradingClient {
let swap_result = executor.swap(buy_params).await;
let result =
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
return result;
swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings));
result
}
/// Execute a sell order for a specified token
@@ -847,7 +848,7 @@ impl TradingClient {
pub async fn sell(
&self,
params: TradeSellParams,
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
#[cfg(feature = "perf-trace")]
if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
debug!(
@@ -921,8 +922,8 @@ impl TradingClient {
let swap_result = executor.swap(sell_params).await;
let result =
swap_result.map(|(success, sigs, err)| (success, sigs, err.map(TradeError::from)));
return result;
swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings));
result
}
/// Execute a sell order for a percentage of the specified token amount
@@ -956,7 +957,7 @@ impl TradingClient {
mut params: TradeSellParams,
amount_token: u64,
percent: u64,
) -> Result<(bool, Vec<Signature>, Option<TradeError>), anyhow::Error> {
) -> Result<(bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> {
if percent == 0 || percent > 100 {
return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
}
+146 -30
View File
@@ -6,8 +6,9 @@ use quinn::crypto::rustls::QuicClientConfig;
use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, TransportConfig};
use rcgen::{CertificateParams, KeyPair};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::net::SocketAddr;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
@@ -40,49 +41,171 @@ pub struct AstralaneQuicClient {
endpoint: Endpoint,
connection: Mutex<Connection>,
server_addr: SocketAddr,
server_candidates: Vec<SocketAddr>,
next_server_idx: AtomicUsize,
#[allow(dead_code)]
api_key: String,
}
impl AstralaneQuicClient {
#[inline]
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)), 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)), port)]
}
"ams.gateway.astralane.io" => vec![
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)), port)]
}
"lim.gateway.astralane.io" => {
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)), port)]
}
"lit.gateway.astralane.io" => {
vec![SocketAddr::new(IpAddr::V4(Ipv4Addr::new(84, 32, 97, 47)), port)]
}
_ => {
Vec::new()
}
}
}
#[inline]
fn parse_host_port(server_addr: &str) -> Option<(&str, u16)> {
let (host, port_str) = server_addr.rsplit_once(':')?;
let port = port_str.parse::<u16>().ok()?;
Some((host, port))
}
/// Resolve `host:port` and prefer IPv4 result to avoid v6-remote/v4-local mismatch.
#[inline]
fn resolve_server_candidates(server_addr: &str) -> Result<Vec<SocketAddr>> {
if let Ok(addr) = SocketAddr::from_str(server_addr) {
return Ok(vec![addr]);
}
let mut candidates: Vec<SocketAddr> = Vec::with_capacity(8);
if let Some((host, port)) = Self::parse_host_port(server_addr) {
candidates.extend(Self::astralane_quic_ip_candidates(host, port));
}
use std::net::ToSocketAddrs;
let mut addrs: Vec<SocketAddr> = server_addr
.to_socket_addrs()
.with_context(|| format!("Cannot resolve address: {}", server_addr))?
.collect();
if addrs.is_empty() && candidates.is_empty() {
anyhow::bail!("Cannot resolve address: {}", server_addr);
}
// QUIC in many bot/VPS environments is primarily IPv4; prefer A over AAAA.
addrs.sort_by_key(|a| if a.is_ipv4() { 0 } else { 1 });
for addr in addrs {
if !candidates.contains(&addr) {
candidates.push(addr);
}
}
Ok(candidates)
}
#[inline]
fn local_bind_for_remote(remote: SocketAddr) -> SocketAddr {
match remote.ip() {
IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
}
}
/// Connect to an Astralane QUIC server.
/// 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 addr = SocketAddr::from_str(server_addr)
.or_else(|_| {
use std::net::ToSocketAddrs;
server_addr
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.ok_or_else(|| anyhow::anyhow!("Cannot resolve address: {}", server_addr))
})
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)");
let client_config = Self::build_client_config(api_key)?;
let mut endpoint =
Endpoint::client("0.0.0.0:0".parse()?).context("Failed to create QUIC endpoint")?;
let mut endpoint = Endpoint::client(Self::local_bind_for_remote(addr))
.context("Failed to create QUIC endpoint")?;
endpoint.set_default_client_config(client_config);
info!("[astralane-quic] Connecting to {} ...", addr);
let connection = endpoint
.connect(addr, "astralane")?
.await
.context("Failed to connect to Astralane QUIC server")?;
let mut last_err: Option<anyhow::Error> = None;
let mut selected_addr = addr;
let mut connection_opt: Option<Connection> = None;
for candidate in &candidates {
selected_addr = *candidate;
match endpoint.connect(*candidate, "astralane") {
Ok(connecting) => match connecting.await {
Ok(conn) => {
connection_opt = Some(conn);
break;
}
Err(e) => {
warn!("[astralane-quic] connect failed for {}: {}", candidate, e);
last_err = Some(e.into());
}
},
Err(e) => {
warn!("[astralane-quic] connect setup failed for {}: {}", candidate, e);
last_err = Some(e.into());
}
}
}
let connection = connection_opt.ok_or_else(|| {
last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to connect to Astralane QUIC server"))
})?;
info!("[astralane-quic] Connected at {}", addr);
info!("[astralane-quic] Connected at {}", selected_addr);
Ok(Self {
endpoint,
connection: Mutex::new(connection),
server_addr: addr,
server_addr: selected_addr,
server_candidates: candidates,
next_server_idx: AtomicUsize::new(0),
api_key: api_key.to_string(),
})
}
async fn reconnect_next_candidate(&self) -> Result<Connection> {
let total = self.server_candidates.len().max(1);
let start = self.next_server_idx.fetch_add(1, Ordering::Relaxed) % total;
let mut last_err: Option<anyhow::Error> = None;
for offset in 0..total {
let idx = (start + offset) % total;
let addr = self.server_candidates[idx];
match self.endpoint.connect(addr, "astralane") {
Ok(connecting) => match connecting.await {
Ok(conn) => return Ok(conn),
Err(e) => {
warn!("[astralane-quic] reconnect failed for {}: {}", addr, e);
last_err = Some(e.into());
}
},
Err(e) => {
warn!("[astralane-quic] reconnect setup failed for {}: {}", addr, e);
last_err = Some(e.into());
}
}
}
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("Failed to reconnect to Astralane QUIC server")))
}
/// Send a single bincode-serialized `VersionedTransaction`.
/// Fire-and-forget; automatically reconnects if the connection is dead.
pub async fn send_transaction(&self, transaction_bytes: &[u8]) -> Result<()> {
@@ -108,12 +231,9 @@ impl AstralaneQuicClient {
}
}
warn!("[astralane-quic] Connection dead, reconnecting to {} ...", self.server_addr);
*guard = self
.endpoint
.connect(self.server_addr, "astralane")?
.await
.context("Failed to reconnect to Astralane QUIC server")?;
info!("[astralane-quic] Reconnected to {}", self.server_addr);
let new_conn = self.reconnect_next_candidate().await?;
*guard = new_conn.clone();
info!("[astralane-quic] Reconnected");
}
guard.clone()
};
@@ -137,12 +257,8 @@ impl AstralaneQuicClient {
let mut guard = self.connection.lock().await;
if guard.close_reason().is_some() {
info!("[astralane-quic] Reconnecting at {}", self.server_addr);
*guard = self
.endpoint
.connect(self.server_addr, "astralane")?
.await
.context("Failed to reconnect to Astralane QUIC server")?;
info!("[astralane-quic] Reconnected to {}", self.server_addr);
*guard = self.reconnect_next_candidate().await?;
info!("[astralane-quic] Reconnected");
}
Ok(())
}
+369 -100
View File
@@ -7,6 +7,7 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use arc_swap::ArcSwap;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
@@ -17,15 +18,95 @@ 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;
// Include pre-generated gRPC code
pub mod serverpb {
include!("pb/serverpb.rs");
}
// gRPC client wrapper
#[derive(Clone)]
pub struct BlockRazorGrpcClient {
channel: Channel,
auth_token: String,
}
impl BlockRazorGrpcClient {
pub fn new(channel: Channel, auth_token: String) -> Self {
Self { channel, auth_token }
}
pub async fn get_health(&self) -> Result<String> {
let mut client = serverpb::server_client::ServerClient::new(self.channel.clone());
let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str())
.map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?;
let mut request = tonic::Request::new(serverpb::HealthRequest {});
request.metadata_mut().insert("apikey", apikey);
let response = client.get_health(request).await
.map_err(|e| anyhow::anyhow!("gRPC health check failed: {}", e))?;
Ok(response.into_inner().status)
}
pub async fn send_transaction(
&self,
transaction: String,
mode: String,
safe_window: Option<i32>,
revert_protection: bool,
) -> Result<String> {
// 检查交易数据大小
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor transaction size: {} bytes", transaction.len());
}
let mut client = serverpb::server_client::ServerClient::new(self.channel.clone());
let apikey = AsciiMetadataValue::try_from(self.auth_token.as_str())
.map_err(|e| anyhow::anyhow!("Invalid API key format: {}", e))?;
let mut request = tonic::Request::new(serverpb::SendRequest {
transaction,
mode: String::from(mode),
safe_window,
revert_protection,
});
request.metadata_mut().insert("apikey", apikey);
let response = client.send_transaction(request).await
.map_err(|e| anyhow::anyhow!("gRPC send transaction failed: {}", e))?;
Ok(response.into_inner().signature)
}
}
#[derive(Clone)]
pub enum BlockRazorBackend {
Grpc {
endpoint: String,
auth_token: String,
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,
auth_token: String,
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,
},
}
#[derive(Clone)]
pub struct BlockRazorClient {
pub endpoint: String,
pub auth_token: String,
pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client,
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
pub stop_ping: Arc<AtomicBool>,
backend: BlockRazorBackend,
}
#[async_trait::async_trait]
@@ -36,7 +117,7 @@ impl SwqosClientTrait for BlockRazorClient {
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(
@@ -45,7 +126,10 @@ impl SwqosClientTrait for BlockRazorClient {
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
for transaction in transactions {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
fn get_tip_account(&self) -> Result<String> {
@@ -62,21 +146,67 @@ impl SwqosClientTrait for BlockRazorClient {
}
impl BlockRazorClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
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, false))
}
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);
// 官方文档:請求中唯一允許的 header 是 Content-Type: text/plain;避免默认 User-Agent 等导致 500
let http_client = default_http_client_builder().user_agent("").build().unwrap();
// 配置 Channel,增加连接超时
let channel = tonic::transport::Channel::from_shared(endpoint.clone())
.map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))?
.timeout(Duration::from_secs(30))
.connect()
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
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));
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
backend: BlockRazorBackend::Grpc {
endpoint,
auth_token,
grpc_client,
ping_handle,
stop_ping,
mev_protection,
},
};
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
Ok(client)
}
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));
let stop_ping = Arc::new(AtomicBool::new(false));
let client = Self {
rpc_client: Arc::new(rpc_client),
backend: BlockRazorBackend::Http {
endpoint,
auth_token,
http_client,
ping_handle,
stop_ping,
mev_protection,
},
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
@@ -85,47 +215,129 @@ impl BlockRazorClient {
client
}
/// Start periodic ping task to keep connections active
async fn start_ping_task(&self) {
let endpoint = self.endpoint.clone();
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
match &self.backend {
BlockRazorBackend::Grpc {
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 {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor ping request failed: {}", e);
let handle = tokio::spawn(async move {
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);
}
}
}
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
// 健康检查(使用 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);
}
}
}
}
}
}
});
let mut ping_guard = ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
}
*ping_guard = Some(handle);
}
BlockRazorBackend::Http {
endpoint,
auth_token,
http_client,
ping_handle,
stop_ping,
..
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let http_client = http_client.clone();
let ping_handle = ping_handle.clone();
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 crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor HTTP ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
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);
}
}
}
});
let mut ping_guard = ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
}
*ping_guard = Some(handle);
}
*ping_guard = Some(handle);
}
}
/// Send ping request: POST /v2/health?auth=... (Keep Alive). Only required param: auth.
async fn send_ping_request(
async fn send_http_ping(
http_client: &Client,
endpoint: &str,
auth_token: &str,
@@ -142,51 +354,119 @@ impl BlockRazorClient {
let status = response.status();
let _ = response.bytes().await;
if !status.is_success() {
eprintln!("BlockRazor ping request failed with status: {}", status);
eprintln!("BlockRazor HTTP ping request failed with status: {}", status);
}
Ok(())
}
/// Send transaction via v2 API: plain Base64 body, Content-Type: text/plain. Only required URI param: auth.
/// 文档要求:auth 以 URI 参数传入;body 为纯 Base64 编码交易;唯一允许的 header 为 Content-Type: text/plain。
pub async fn send_transaction(
/// 重新建立 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,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let response = self
.http_client
.post(&self.endpoint)
.query(&[("auth", self.auth_token.as_str())])
.header("Content-Type", "text/plain")
.body(content)
.send()
.await?;
match &self.backend {
BlockRazorBackend::Grpc {
grpc_client,
mev_protection,
..
} => {
let (content, _signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let status = response.status();
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());
// 使用 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());
}
} 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());
}
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));
}
return Err(anyhow::anyhow!("BlockRazor gRPC sendTransaction failed: {}", e));
}
}
}
} 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));
BlockRazorBackend::Http {
endpoint,
auth_token,
http_client,
mev_protection,
..
} => {
let (content, _signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let mut query_params: Vec<(&str, &str)> = vec![
("auth", auth_token.as_str()),
// mev_protection=true: sandwichMitigation mode skips blacklisted Leader slots (MEV protection).
// revertProtection is unrelated to MEV; not set.
("mode", if *mev_protection { "sandwichMitigation" } else { "fast" }),
];
let response = http_client
.post(endpoint)
.query(&query_params)
.header("Content-Type", "text/plain")
.body(content)
.send()
.await?;
let status = response.status();
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());
}
} 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));
}
return Err(anyhow::anyhow!(
"BlockRazor HTTP sendTransaction failed: status {} body: {}",
status,
body
));
}
}
return Err(anyhow::anyhow!(
"BlockRazor sendTransaction failed: status {} body: {}",
status,
body
));
}
let start_time = Instant::now();
let signature = transaction.signatures[0];
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
@@ -210,34 +490,23 @@ impl BlockRazorClient {
Ok(())
}
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
}
impl Drop for BlockRazorClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
match &self.backend {
BlockRazorBackend::Grpc { stop_ping, ping_handle, .. } | BlockRazorBackend::Http { stop_ping, ping_handle, .. } => {
stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
tokio::spawn(async move {
let mut ping_guard = ping_handle.lock().await;
if let Some(handle) = ping_guard.as_ref() {
handle.abort();
let ping_handle = ping_handle.clone();
tokio::spawn(async move {
let mut ping_guard = ping_handle.lock().await;
if let Some(handle) = ping_guard.as_ref() {
handle.abort();
}
*ping_guard = None;
});
}
*ping_guard = None;
});
}
}
}
+88 -26
View File
@@ -22,23 +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_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, 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,
@@ -50,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
@@ -63,11 +61,14 @@ pub const SWQOS_BLACKLIST: &[SwqosType] = &[
SwqosType::NextBlock, // NextBlock is disabled by default
];
/// SWQOS 提交通道:HTTP 或 QUIC(低延迟)。部分提供商(如 Astralane)支持 QUIC。
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
/// BlockRazor 支持 gRPC 和 HTTP。
/// Astralane 和 Node1 支持 QUIC。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SwqosTransport {
#[default]
Http,
Grpc,
Quic,
}
@@ -224,8 +225,8 @@ pub enum SwqosConfig {
Node1(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// FlashBlock(api_token, region, custom_url)
FlashBlock(String, SwqosRegion, Option<String>),
/// BlockRazor(api_token, region, custom_url)
BlockRazor(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>),
/// Stellium(api_token, region, custom_url)
@@ -255,7 +256,7 @@ impl SwqosConfig {
SwqosConfig::ZeroSlot(_, _, _) => SwqosType::ZeroSlot,
SwqosConfig::Node1(_, _, _, _) => SwqosType::Node1,
SwqosConfig::FlashBlock(_, _, _) => SwqosType::FlashBlock,
SwqosConfig::BlockRazor(_, _, _) => SwqosType::BlockRazor,
SwqosConfig::BlockRazor(_, _, _, _) => SwqosType::BlockRazor,
SwqosConfig::Astralane(_, _, _, _) => SwqosType::Astralane,
SwqosConfig::Stellium(_, _, _) => SwqosType::Stellium,
SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed,
@@ -294,10 +295,57 @@ impl SwqosConfig {
}
}
pub fn get_endpoint_with_transport(
swqos_type: SwqosType,
region: SwqosRegion,
url: Option<String>,
transport: Option<SwqosTransport>,
mev_protection: bool,
) -> String {
if let Some(custom_url) = url {
return custom_url;
}
match swqos_type {
SwqosType::BlockRazor => {
// transport=None 或 transport=Grpc => gRPC; transport=Http => HTTP
let use_http = transport.map_or(false, |t| t == SwqosTransport::Http);
if use_http {
SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC[region as usize].to_string()
}
}
SwqosType::Node1 => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
SWQOS_ENDPOINTS_NODE1_QUIC[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
}
}
SwqosType::Astralane => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
// MEV protection: port 9000; standard: port 7000
if mev_protection {
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
}
} else {
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
}
}
_ => Self::get_endpoint(swqos_type, region, None),
}
}
pub async fn get_swqos_client(
rpc_url: String,
commitment: CommitmentConfig,
swqos_config: SwqosConfig,
mev_protection: bool,
) -> Result<Arc<SwqosClient>> {
match swqos_config {
SwqosConfig::Jito(auth_token, region, url) => {
@@ -351,17 +399,31 @@ impl SwqosConfig {
FlashBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(flashblock_client))
}
SwqosConfig::BlockRazor(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url);
let blockrazor_client =
BlockRazorClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(blockrazor_client))
SwqosConfig::BlockRazor(auth_token, region, url, transport) => {
// BlockRazor: transport=None 或 transport=Grpc 时使用 gRPCtransport=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, mev_protection);
if use_http {
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, 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()
// MEV protection: port 9000; standard: port 7000
if mev_protection {
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
}
});
let astralane_client =
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
+42
View File
@@ -0,0 +1,42 @@
# Proto 生成的代码说明
## 概述
这个目录包含了从 `.proto` 文件预生成的 Rust 代码。
## 文件说明
- `serverpb.rs` - 从 `blockrazor.proto` 生成的 gRPC 代码
- 消息类型: `SendRequest`, `SendResponse`, `HealthRequest`, `HealthResponse`
- gRPC 客户端: `server_client::ServerClient`
- gRPC 服务端: `server_server::Server`
## 用户使用
用户**不需要**安装 `protoc` 或编译 proto 文件。这些代码已经预生成好了,可以直接使用。
`blockrazor.rs` 中使用:
```rust
pub mod serverpb {
include!("pb/serverpb.rs");
}
```
## 开发者如何重新生成代码
如果你修改了 `.proto` 文件并需要重新生成代码:
```bash
cd sol-trade-sdk/proto/gen
cargo run
```
这会在 `src/swqos/pb/serverpb.rs` 生成新的代码。
## 技术细节
生成工具使用 `tonic-prost-build` crate
- 输出目录: `src/swqos/pb`
- Proto 文件: `proto/blockrazor.proto`
- 生成工具: `proto/gen/`
- 包含完整的客户端和服务端代码
+383
View File
@@ -0,0 +1,383 @@
// This file is @generated by prost-build.
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SendRequest {
#[prost(string, tag = "1")]
pub transaction: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub mode: ::prost::alloc::string::String,
/// only take effect in sandwichMitigation mode
#[prost(int32, optional, tag = "3")]
pub safe_window: ::core::option::Option<i32>,
#[prost(bool, tag = "4")]
pub revert_protection: bool,
}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct SendResponse {
#[prost(string, tag = "1")]
pub signature: ::prost::alloc::string::String,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
pub struct HealthRequest {}
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
pub struct HealthResponse {
#[prost(string, tag = "1")]
pub status: ::prost::alloc::string::String,
}
/// Generated client implementations.
pub mod server_client {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
#[derive(Debug, Clone)]
pub struct ServerClient<T> {
inner: tonic::client::Grpc<T>,
}
impl ServerClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> ServerClient<T>
where
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,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> ServerClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
ServerClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
pub async fn send_transaction(
&mut self,
request: impl tonic::IntoRequest<super::SendRequest>,
) -> std::result::Result<tonic::Response<super::SendResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/serverpb.Server/SendTransaction",
);
let mut req = request.into_request();
req.extensions_mut()
.insert(GrpcMethod::new("serverpb.Server", "SendTransaction"));
self.inner.unary(req, path, codec).await
}
pub async fn get_health(
&mut self,
request: impl tonic::IntoRequest<super::HealthRequest>,
) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::unknown(
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/serverpb.Server/GetHealth",
);
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("serverpb.Server", "GetHealth"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod server_server {
#![allow(
unused_variables,
dead_code,
missing_docs,
clippy::wildcard_imports,
clippy::let_unit_value,
)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with ServerServer.
#[async_trait]
pub trait Server: std::marker::Send + std::marker::Sync + 'static {
async fn send_transaction(
&self,
request: tonic::Request<super::SendRequest>,
) -> std::result::Result<tonic::Response<super::SendResponse>, tonic::Status>;
async fn get_health(
&self,
request: tonic::Request<super::HealthRequest>,
) -> std::result::Result<tonic::Response<super::HealthResponse>, tonic::Status>;
}
#[derive(Debug)]
pub struct ServerServer<T> {
inner: Arc<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
impl<T> ServerServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for ServerServer<T>
where
T: Server,
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
match req.uri().path() {
"/serverpb.Server/SendTransaction" => {
#[allow(non_camel_case_types)]
struct SendTransactionSvc<T: Server>(pub Arc<T>);
impl<T: Server> tonic::server::UnaryService<super::SendRequest>
for SendTransactionSvc<T> {
type Response = super::SendResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::SendRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Server>::send_transaction(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = SendTransactionSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
"/serverpb.Server/GetHealth" => {
#[allow(non_camel_case_types)]
struct GetHealthSvc<T: Server>(pub Arc<T>);
impl<T: Server> tonic::server::UnaryService<super::HealthRequest>
for GetHealthSvc<T> {
type Response = super::HealthResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::HealthRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as Server>::get_health(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let method = GetHealthSvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::BoxBody::default(),
);
let headers = response.headers_mut();
headers
.insert(
tonic::Status::GRPC_STATUS,
(tonic::Code::Unimplemented as i32).into(),
);
headers
.insert(
http::header::CONTENT_TYPE,
tonic::metadata::GRPC_CONTENT_TYPE,
);
Ok(response)
})
}
}
}
}
impl<T> Clone for ServerServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
/// Generated gRPC service name
pub const SERVICE_NAME: &str = "serverpb.Server";
impl<T> tonic::server::NamedService for ServerServer<T> {
const NAME: &'static str = SERVICE_NAME;
}
}
+61 -2
View File
@@ -8,7 +8,6 @@ use quinn::{
use rand::seq::IndexedRandom as _;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use std::time::Instant;
use std::{
net::{SocketAddr, ToSocketAddrs as _},
@@ -25,6 +24,66 @@ use crate::{
swqos::{SwqosType, TradeType},
};
// Skip server verification implementation
#[derive(Debug)]
struct SkipServerVerification;
impl SkipServerVerification {
fn new() -> Arc<Self> {
Arc::new(Self)
}
}
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
}
}
// Generate dummy self-signed certificate using rcgen
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
// Generate a new key pair for the certificate
let key_pair = rcgen::KeyPair::generate()?;
let params = rcgen::CertificateParams::default();
let cert = params.self_signed(&key_pair)?;
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
Ok((cert_der, key_der))
}
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const SOYAS_SERVER: &str = "soyas-landing";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
@@ -43,7 +102,7 @@ impl SoyasClient {
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
let keypair = Keypair::from_base58_string(&api_key);
let (cert, key) = new_dummy_x509_certificate(&keypair);
let (cert, key) = generate_self_signed_cert(&keypair)?;
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
+61 -2
View File
@@ -7,7 +7,6 @@ use quinn::{
};
use rand::seq::IndexedRandom as _;
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use std::time::Instant;
use std::{
net::{SocketAddr, ToSocketAddrs as _},
@@ -26,6 +25,66 @@ use crate::{
swqos::{SwqosType, TradeType},
};
// Skip server verification implementation
#[derive(Debug)]
struct SkipServerVerification;
impl SkipServerVerification {
fn new() -> Arc<Self> {
Arc::new(Self)
}
}
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
}
}
// Generate dummy self-signed certificate using rcgen
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
// Generate a new key pair for the certificate
let key_pair = rcgen::KeyPair::generate()?;
let params = rcgen::CertificateParams::default();
let cert = params.self_signed(&key_pair)?;
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
Ok((cert_der, key_der))
}
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
/// Fallback SNI when endpoint is IP or cannot extract host (keeps legacy behavior).
const SPEED_SERVER_FALLBACK: &str = "speed-landing";
@@ -66,7 +125,7 @@ impl SpeedlandingClient {
let rpc_client = SolanaRpcClient::new(rpc_url);
let server_name = Self::server_name_from_endpoint(&endpoint_string);
let keypair = Keypair::from_base58_string(&api_key);
let (cert, key) = new_dummy_x509_certificate(&keypair);
let (cert, key) = generate_self_signed_cert(&keypair)?;
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
+158 -33
View File
@@ -1,12 +1,12 @@
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
default_http_client_builder, poll_transaction_confirmation,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use std::{sync::Arc, time::Instant, time::Duration};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
use bincode;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
@@ -21,6 +21,8 @@ pub struct ZeroSlotClient {
pub auth_token: String,
pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client,
pub ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
pub stop_ping: Arc<AtomicBool>,
}
#[async_trait::async_trait]
@@ -60,7 +62,79 @@ impl ZeroSlotClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
/// Start periodic ping task to keep connections active
async fn start_ping_task(&self) {
let endpoint = self.endpoint.clone();
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("0slot ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive under 65s server timeout
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
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);
}
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
if let Some(old_handle) = ping_guard.as_ref() {
old_handle.abort();
}
*ping_guard = Some(handle);
}
}
/// Send ping request: POST with getHealth method (Keep Alive). Free operation, not counted toward TPS.
async fn send_ping_request(
http_client: &Client,
endpoint: &str,
auth_token: &str,
) -> Result<()> {
let url = format!("{}/?api-key={}", endpoint, auth_token);
let response = http_client
.post(&url)
.header("Content-Type", "application/json")
.timeout(Duration::from_millis(1500))
.body(r#"{"jsonrpc":"2.0","id":1,"method":"getHealth"}"#)
.send()
.await?;
let _ = response.bytes().await;
Ok(())
}
pub async fn send_transaction(
@@ -70,47 +144,80 @@ impl ZeroSlotClient {
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": 1,
"method": "sendTransaction",
"params": [
content,
{ "encoding": "base64", "skipPreflight": true }
]
}))?;
// Binary-Tx: Send raw binary transaction bytes directly
// This is faster than JSON-RPC as it avoids unnecessary encoding/decoding
let tx_bytes = bincode::serialize(transaction)?;
// Build URL for Binary-Tx endpoint: {endpoint}/txb?api-key={auth_token}
let mut url = String::with_capacity(self.endpoint.len() + self.auth_token.len() + 20);
url.push_str(&self.endpoint);
url.push_str("/?api-key=");
url.push_str("/txb?api-key=");
url.push_str(&self.auth_token);
// 4. Use `text().await?` directly, avoiding async JSON parsing from `json().await?`
let response_text = self
// Send binary transaction directly
let response = self
.http_client
.post(&url)
.body(request_body) // Pass string directly, avoiding `json()` overhead
.header("Content-Type", "application/json") // Explicitly specify JSON header
.header("User-Agent", "") // Optional: 0slot recommends empty User-Agent
.body(tx_bytes)
.send()
.await?
.text()
.await?;
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
crate::common::sdk_log::log_swqos_submitted("0slot", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" [0slot] {} submission failed after {:?}: {:?}", trade_type, start_time.elapsed(), _error);
let status = response.status();
let response_text = response.text().await?;
// Binary-Tx returns JSON-RPC 2.0 format responses
// 200: success with result field containing signature, or error field with code/message
// 403: api-key error (null, doesn't exist, or expired)
// 419: rate limit exceeded
// 500: submission failed
match status.as_u16() {
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());
} else if let Some(error) = json_value.get("error") {
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")
.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));
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));
}
} else {
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());
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());
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());
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));
}
} else {
crate::common::sdk_log::log_swqos_submission_failed("0slot", trade_type, start_time.elapsed(), response_text);
}
let start_time: Instant = Instant::now();
// Get transaction signature from the transaction for confirmation polling
let signature = transaction.signatures[0];
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
@@ -139,3 +246,21 @@ impl ZeroSlotClient {
Ok(())
}
}
impl Drop for ZeroSlotClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
tokio::spawn(async move {
let mut ping_guard = ping_handle.lock().await;
if let Some(handle) = ping_guard.as_ref() {
handle.abort();
}
*ping_guard = None;
});
}
}
+4 -3
View File
@@ -1,9 +1,10 @@
use solana_hash::Hash;
use solana_sdk::{
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
instruction::Instruction, pubkey::Pubkey,
signature::Keypair, signer::Signer, transaction::VersionedTransaction,
};
use solana_system_interface::instruction::transfer;
use solana_message::AddressLookupTableAccount;
use solana_system_interface::instruction as system_instruction;
use std::sync::Arc;
use super::nonce_manager::{add_nonce_instruction, get_transaction_blockhash};
@@ -51,7 +52,7 @@ pub async fn build_transaction(
if with_tip && tip_amount > 0.0 {
let tip_lamports = sol_f64_to_lamports(tip_amount);
instructions.push(transfer(&payer.pubkey(), tip_account, tip_lamports));
instructions.push(system_instruction::transfer(&payer.pubkey(), tip_account, tip_lamports));
}
super::compute_budget_manager::extend_compute_budget_instructions(
+2 -2
View File
@@ -1,5 +1,5 @@
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer, transaction::Transaction};
use solana_system_interface::instruction::transfer;
use solana_system_interface::instruction as system_instruction;
use crate::common::{
fast_fn::{
@@ -88,7 +88,7 @@ pub async fn transfer_sol(
return Err(anyhow!("Insufficient balance"));
}
let transfer_instruction = transfer(&payer.pubkey(), receive_wallet, amount);
let transfer_instruction = system_instruction::transfer(&payer.pubkey(), receive_wallet, amount);
let recent_blockhash = rpc.get_latest_blockhash().await?;
+4 -4
View File
@@ -7,8 +7,8 @@ use crate::common::{
spl_token::close_account,
};
use smallvec::SmallVec;
use solana_sdk::{instruction::Instruction, message::AccountMeta, pubkey::Pubkey};
use solana_system_interface::instruction::transfer;
use solana_sdk::{instruction::Instruction, instruction::AccountMeta, pubkey::Pubkey};
use solana_system_interface::instruction as system_instruction;
#[inline]
pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]> {
@@ -27,7 +27,7 @@ pub fn handle_wsol(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 3]>
&crate::constants::TOKEN_PROGRAM,
));
insts.extend([
transfer(&payer, &wsol_token_account, amount_in),
system_instruction::transfer(&payer, &wsol_token_account, amount_in),
// sync_native
Instruction {
program_id: crate::constants::TOKEN_PROGRAM,
@@ -91,7 +91,7 @@ pub fn wrap_sol_only(payer: &Pubkey, amount_in: u64) -> SmallVec<[Instruction; 2
let mut insts = SmallVec::<[Instruction; 2]>::new();
insts.extend([
transfer(&payer, &wsol_token_account, amount_in),
system_instruction::transfer(&payer, &wsol_token_account, amount_in),
// sync_native
Instruction {
program_id: crate::constants::TOKEN_PROGRAM,
+2 -2
View File
@@ -15,7 +15,7 @@ use anyhow::{anyhow, Result};
use crossbeam_queue::ArrayQueue;
use once_cell::sync::OnceCell;
use solana_hash::Hash;
use solana_sdk::message::AddressLookupTableAccount;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{
instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature,
};
@@ -562,7 +562,7 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
const SUBMIT_TIMEOUT_SECS: u64 = 30;
const SUBMIT_TIMEOUT_SECS: u64 = 2;//无需确认的交易,一般2秒合适了 一般2秒内发送全都返回 没返回的也不等了,没返回的就是太慢的swqos
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
false,
vec![],
+15 -7
View File
@@ -1,9 +1,10 @@
use anyhow::Result;
use solana_hash::Hash;
use solana_sdk::{
instruction::Instruction, message::AddressLookupTableAccount, pubkey::Pubkey,
instruction::Instruction, pubkey::Pubkey,
signature::Keypair, signature::Signature,
};
use solana_message::AddressLookupTableAccount;
use std::{
sync::Arc,
time::{Duration, Instant},
@@ -25,6 +26,7 @@ use crate::{
trading::MiddlewareManager,
};
use once_cell::sync::Lazy;
use crate::swqos::{ SwqosType};
/// Global syscall bypass manager (reserved for future time/IO optimizations).
/// 全局系统调用绕过管理器(预留,后续可接入时间/IO 等优化)。
@@ -55,7 +57,7 @@ impl TradeExecutor for GenericTradeExecutor {
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
// Sample total start only when logging or simulate. 仅在有日志或 simulate 时取起点。
let total_start = (params.log_enabled || params.simulate).then(Instant::now);
let timing_start_us: Option<i64> = if params.log_enabled {
@@ -215,7 +217,9 @@ impl TradeExecutor for GenericTradeExecutor {
} else {
(ok, signatures, err)
};
Ok(confirm_result)
//就是把confirm_result 拆开 再加上 submit_timings
Ok((confirm_result.0, confirm_result.1, confirm_result.2, submit_timings))
} else {
// Not waiting for confirmation: confirmed is not measured (-); total is per-channel submit time only.
if log_enabled {
@@ -229,7 +233,11 @@ impl TradeExecutor for GenericTradeExecutor {
None,
);
}
Ok((ok, signatures, err))
Ok((ok, signatures, err, submit_timings))
};
result
@@ -254,7 +262,7 @@ async fn simulate_transaction(
is_buy: bool,
with_tip: bool,
gas_fee_strategy: GasFeeStrategy,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)> {
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
use crate::trading::common::build_transaction;
use solana_client::rpc_config::RpcSimulateTransactionConfig;
use solana_commitment_config::CommitmentLevel;
@@ -330,7 +338,7 @@ async fn simulate_transaction(
trace!(target: "sol_trade_sdk", "Compute Units Consumed: {}", units_consumed);
}
}
return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err))));
return Ok((false, vec![signature], Some(anyhow::anyhow!("{:?}", err)), Vec::new()));
}
// Simulation succeeded
@@ -345,7 +353,7 @@ async fn simulate_transaction(
}
}
Ok((true, vec![signature], None))
Ok((true, vec![signature], None, Vec::new()))
}
#[cfg(test)]
+70 -21
View File
@@ -4,7 +4,7 @@ use crate::common::spl_associated_token_account::get_associated_token_address_wi
use crate::common::{GasFeeStrategy, SolanaRpcClient};
use crate::constants::TOKEN_PROGRAM;
use core_affinity::CoreId;
use crate::instruction::utils::pumpfun::global_constants::MAYHEM_FEE_RECIPIENT;
use crate::instruction::utils::pumpfun::is_mayhem_fee_recipient;
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
#[derive(Clone)]
@@ -18,7 +18,7 @@ use crate::swqos::{SwqosClient, TradeType};
use crate::trading::common::get_multi_token_balances;
use crate::trading::MiddlewareManager;
use solana_hash::Hash;
use solana_sdk::message::AddressLookupTableAccount;
use solana_message::AddressLookupTableAccount;
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
use std::sync::Arc;
@@ -119,20 +119,22 @@ impl std::fmt::Debug for SwapParams {
/// PumpFun protocol specific parameters
/// Configuration parameters specific to PumpFun trading protocol.
///
/// **Creator Rewards Sharing**: Some coins use a dynamic `creator_vault` (fee-sharing config).
/// Always use the latest on-chain creator/vault when building params for **sell**; do not reuse
/// cached params from buy. Either fetch fresh data via RPC, or pass `creator_vault` from gRPC
/// using [`from_trade`](PumpFunParams::from_trade) / [`from_dev_trade`](PumpFunParams::from_dev_trade),
/// or override with [`with_creator_vault`](PumpFunParams::with_creator_vault).
/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` =
/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`].
/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct
/// does not affect ix building.
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey,
/// Creator vault PDA. For Creator Rewards Sharing coins this can change; pass latest from gRPC when selling.
/// Resolved by [`resolve_creator_vault_for_ix`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix): use ix vault when it matches `PDA(creator)` or fee-sharing vault; else `PDA(creator)`.
pub creator_vault: Pubkey,
pub token_program: Pubkey,
/// Whether to close token account when selling, only effective during sell operations
pub close_token_account_when_sell: Option<bool>,
/// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
pub fee_recipient: Pubkey,
}
impl PumpFunParams {
@@ -147,11 +149,14 @@ impl PumpFunParams {
creator_vault: creator_vault,
token_program: token_program,
close_token_account_when_sell: Some(close_token_account_when_sell),
fee_recipient: Pubkey::default(),
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
pub fn from_dev_trade(
mint: Pubkey,
token_amount: u64,
@@ -164,8 +169,10 @@ impl PumpFunParams {
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
let is_mayhem_mode =
mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient));
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
bonding_curve,
&mint,
@@ -175,17 +182,32 @@ impl PumpFunParams {
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
&bonding_curve_account.creator,
creator_vault,
&mint,
)
.or_else(|| {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
})
.unwrap_or_default();
Self {
bonding_curve: Arc::new(bonding_curve_account),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault,
creator_vault: creator_vault_resolved,
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
}
}
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
/// so that sell instructions include the correct remaining accounts for cashback.
///
/// `mayhem_mode`:
/// - **`Some(v)`**(推荐):显式使用链上事件中的值。gRPC 日志解析对应 Explorer 的 `tradeEvent.mayhemMode`
/// **不会**再用 `fee_recipient` 覆盖。
/// - **`None`**:无该字段时(例如 ShredStream 仅解外层指令、或冷路径),才用 `fee_recipient` 是否落在 Mayhem 静态列表上推断。
pub fn from_trade(
bonding_curve: Pubkey,
associated_bonding_curve: Pubkey,
@@ -200,8 +222,12 @@ impl PumpFunParams {
fee_recipient: Pubkey,
token_program: Pubkey,
is_cashback_coin: bool,
mayhem_mode: Option<bool>,
) -> Self {
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT;
let is_mayhem_mode = match mayhem_mode {
Some(v) => v,
None => is_mayhem_fee_recipient(&fee_recipient),
};
let bonding_curve = BondingCurveAccount::from_trade(
bonding_curve,
mint,
@@ -213,12 +239,22 @@ impl PumpFunParams {
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
&bonding_curve.creator,
creator_vault,
&mint,
)
.or_else(|| {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
})
.unwrap_or_default();
Self {
bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault,
creator_vault: creator_vault_resolved,
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
}
}
@@ -255,11 +291,11 @@ impl PumpFunParams {
creator_vault: creator_vault.unwrap(),
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
})
}
/// Override `creator_vault` with a value from gRPC/event (e.g. for Creator Rewards Sharing).
/// Use when selling so the instruction uses the latest on-chain vault and avoids "seeds constraint violated" (2006).
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
#[inline]
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
self.creator_vault = creator_vault;
@@ -404,8 +440,21 @@ impl PumpSwapParams {
pool_address: &Pubkey,
) -> Result<Self, anyhow::Error> {
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
Self::from_pool_data(rpc, pool_address, &pool_data).await
}
/// Build params from an already-decoded Pool, only fetching token balances.
///
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
/// decoded Pool).
pub async fn from_pool_data(
rpc: &SolanaRpcClient,
pool_address: &Pubkey,
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
) -> Result<Self, anyhow::Error> {
let (pool_base_token_reserves, pool_quote_token_reserves) =
crate::instruction::utils::pumpswap::get_token_balances(&pool_data, rpc).await?;
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
let creator = pool_data.coin_creator;
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
creator,
@@ -415,12 +464,12 @@ impl PumpSwapParams {
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
let base_token_program_ata = get_associated_token_address_with_program_id(
&pool_address,
pool_address,
&pool_data.base_mint,
&crate::constants::TOKEN_PROGRAM,
);
let quote_token_program_ata = get_associated_token_address_with_program_id(
&pool_address,
pool_address,
&pool_data.quote_mint,
&crate::constants::TOKEN_PROGRAM,
);
@@ -431,10 +480,10 @@ impl PumpSwapParams {
quote_mint: pool_data.quote_mint,
pool_base_token_account: pool_data.pool_base_token_account,
pool_quote_token_account: pool_data.pool_quote_token_account,
pool_base_token_reserves: pool_base_token_reserves,
pool_quote_token_reserves: pool_quote_token_reserves,
coin_creator_vault_ata: coin_creator_vault_ata,
coin_creator_vault_authority: coin_creator_vault_authority,
pool_base_token_reserves,
pool_quote_token_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
+2 -5
View File
@@ -1,7 +1,7 @@
use crate::trading::SwapParams;
use anyhow::Result;
use solana_sdk::{instruction::Instruction, signature::Signature};
use crate::swqos::{SwqosType};
/// 交易执行器trait - 定义了所有交易协议都需要实现的核心方法
#[async_trait::async_trait]
pub trait TradeExecutor: Send + Sync {
@@ -9,10 +9,7 @@ pub trait TradeExecutor: Send + Sync {
/// - bool: 是否至少有一个交易成功
/// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
/// - Option<anyhow::Error>: 最后一个错误(如果全部失败)
async fn swap(
&self,
params: SwapParams,
) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>)>;
async fn swap(&self, params: SwapParams) -> Result<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)>;
/// 获取协议名称
fn protocol_name(&self) -> &'static str;
}
+2 -1
View File
@@ -22,9 +22,10 @@ use once_cell::sync::Lazy;
use solana_sdk::{
hash::Hash,
instruction::Instruction,
message::{v0, AddressLookupTableAccount, Message, VersionedMessage},
message::{v0, Message, VersionedMessage},
pubkey::Pubkey,
};
use solana_message::AddressLookupTableAccount;
use std::sync::Arc;
/// 预分配的交易构建器
pub struct PreallocatedTxBuilder {
+14 -1
View File
@@ -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
+13 -1
View File
@@ -1,10 +1,22 @@
use solana_sdk::{native_token::sol_str_to_lamports, pubkey::Pubkey};
// Note: sol_to_lamports moved to solana_native_token crate in 3.x
// Using manual conversion: 1 SOL = 1_000_000_000 lamports
use solana_sdk::pubkey::Pubkey;
fn sol_to_lamports(sol: f64) -> u64 {
(sol * 1_000_000_000.0) as u64
}
use crate::{
instruction::utils::pumpfun::global_constants::{CREATOR_FEE, FEE_BASIS_POINTS},
utils::calc::common::compute_fee,
};
/// Converts SOL string to lamports (wrapper for sol_to_lamports)
#[inline]
fn sol_str_to_lamports(sol: &str) -> Option<u64> {
sol.parse::<f64>().ok().map(|s| sol_to_lamports(s))
}
/// Calculates the amount of tokens that can be purchased with a given SOL amount
/// using the bonding curve formula.
///
+6 -1
View File
@@ -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_modeNone=按 fee_recipient 推断)
);
let buy_params = TradeBuyParams {