Compare commits

...

28 Commits

Author SHA1 Message Date
0xfnzero e8ec9103ab release: sol-trade-sdk 4.0.4
fix(swqos): extend parallel-submit wait/grace windows for late HTTP responses

- Scale primary submit timeout with configured channel count (clamped 3–5s)
- After primary expiry, optionally poll longer so slow providers still land before drain
- Short settle sleeps to reduce counter vs TaskResult races and dropped signatures when
  wait_transaction_confirmed is false

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-03 03:01:20 +08:00
Wood 0fe54f0e94 Merge pull request #100 from 0xfnzero/feat/pump-fee-recipient-upgrade
feat(pump): add protocol fee recipient accounts for PumpFun and PumpSwap
2026-04-27 14:53:23 +08:00
0xfnzero 7ac07247a3 feat(pump): add protocol fee recipient accounts for PumpFun and PumpSwap
- PumpFun buy/sell: append bonding-curve-v2 and random extra fee recipient (writable)
- PumpSwap buy/sell: append pool-v2, protocol fee pubkey, and fee quote mint ATA
- Keep utils helpers and constants aligned with on-chain layout (2026-04 upgrade)

Made-with: Cursor
2026-04-23 12:35:39 +08:00
0xfnzero 06ed710869 fix(swqos): Speedlanding QUIC TLS, quinn runtime, and SWQoS fallback
- Enable quinn feature runtime-tokio so QUIC clients (Speedlanding/Soyas) initialize instead of failing with "no async runtime found".
- Align Speedlanding with the official client: use solana_tls_utils new_dummy_x509_certificate and SkipServerVerification; use fixed TLS SNI "speed-landing" (not the PoP hostname).
- Raise per-SWQoS init timeout to 15s; always log init failures/timeouts; if every SWQoS client fails, fall back to Default RPC and print active channel labels.
- Improve SWQoS submission visibility (Default RPC path and related logging); adjust Soyas TLS credential handling where applicable.

Made-with: Cursor
2026-04-16 20:11:41 +08:00
0xfnzero d711346c55 fix(meteora-damm-v2): use correct token program for output ATA creation
- Pass token_a/token_b program based on swap direction (is_a_in) when
  creating the output mint ATA in buy flow, matching ATA address derivation
  for SPL Token vs Token-2022 mints.
- Resolve token_a_program and token_b_program from on-chain mint account
  owners in MeteoraDammV2Params::from_pool_address_by_rpc instead of
  hardcoding the classic SPL Token program.

Fixes incorrect ATA creation for Token-2022 output mints (issue #99).

Made-with: Cursor
2026-04-14 13:04:01 +08:00
0xfnzero 06ef2fed84 feat(swqos): align endpoint tables with SwqosRegion and document fallbacks
- Extend SWQOS regional constants to 10 entries (Dublin, Singapore, Default)
  so indices match SwqosRegion 0..9 everywhere
- Document geographic nearest-fill among published PoPs; call out Default and
  single-PoP provider constraints in comments
- Fix Node1 QUIC Default endpoint to match HTTP; tune ZeroSlot/Blox/Speedlanding
  regional mappings where a closer published PoP exists
- Add unit tests for table length and Astralane/Node1 cross-table consistency
- Update SwqosRegion rustdoc, README, and trading_client example accordingly

Made-with: Cursor
2026-04-14 02:13:42 +08:00
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
53 changed files with 1779 additions and 594 deletions
+23 -20
View File
@@ -1,6 +1,6 @@
[package]
name = "sol-trade-sdk"
version = "4.0.1"
version = "4.0.4"
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"
@@ -107,7 +109,8 @@ parking_lot = "0.12"
arc-swap = "1.7"
sha2 = "0.10"
tonic-prost = "0.14.2"
quinn = { version = "0.11", default-features = false, features = ["rustls"] }
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime foundQUICSpeedlanding/Soyas)无法初始化
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
rcgen = "0.13"
uuid = "1.11"
+34 -20
View File
@@ -48,7 +48,7 @@
- [⚡ Trading Parameters](#-trading-parameters)
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
- [⚙️ SWQoS Service Configuration](#-swqos-service-configuration)
- [Astralane QUIC (Low-Latency)](#astralane-quic-low-latency)
- [Astralane (Binary / Plain / QUIC)](#astralane-binary--plain--quic)
- [🔧 Middleware System](#-middleware-system)
- [🔍 Address Lookup Tables](#-address-lookup-tables)
- [🔍 Nonce Cache](#-nonce-cache)
@@ -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 = "4.0.1" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
```
### Use crates.io
```toml
# Add to your Cargo.toml
sol-trade-sdk = "4.0.1"
sol-trade-sdk = "4.0.3"
```
## 🛠️ Usage Examples
@@ -120,21 +131,24 @@ let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
// Astralane: HTTP (4th param None) or QUIC (Some(SwqosTransport::Quic)); same API key
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
SwqosConfig::Astralane(
"your_astralane_api_key".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
Some(AstralaneTransport::Quic),
), // 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 (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
.build();
// Create TradingClient
let client = TradingClient::new(Arc::new(payer), trade_config).await;
@@ -266,28 +280,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
#### Astralane QUIC (Low-Latency)
#### Astralane (Binary / Plain HTTP / QUIC)
Astralane supports both HTTP and **QUIC** transport. QUIC reduces connection overhead and can lower submission latency. To use the QUIC channel, pass `Some(SwqosTransport::Quic)` as the fourth parameter of `SwqosConfig::Astralane`. Astralanes QUIC service uses a **single endpoint** (no per-region endpoints); the SDK ignores the `region` (and optional custom URL) when QUIC is selected. You can pass the same region as your other SWQoS configs for consistency.
Astralane supports **Binary** HTTP (`/irisb`), **Plain** HTTP (`/iris`), and **QUIC** (`host:7000`, or `:9000` when global `mev_protection` is true). Pass `Some(AstralaneTransport::Plain)`, `Some(AstralaneTransport::Quic)`, or use `None` / omit for **Binary** (default). Global `mev_protection` adds `mev-protect=true` on HTTP or selects QUIC port 9000.
```rust
use sol_trade_sdk::{SwqosConfig, SwqosRegion, SwqosTransport};
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
// Astralane over QUIC (low-latency); region is ignored (single QUIC endpoint)
let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Astralane(
"your_astralane_api_key".to_string(),
SwqosRegion::Frankfurt, // same as other services; ignored for QUIC
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
Some(AstralaneTransport::Quic),
),
];
// Then create TradeConfig / TradingClient as usual with swqos_configs
```
- **HTTP** (default): use `None` or `Some(SwqosTransport::Http)`; region and optional custom URL apply.
- **QUIC**: use `Some(SwqosTransport::Quic)`; the SDK uses a single QUIC endpoint and ignores region. Same API key as HTTP.
- **Binary** (default): `None` or `Some(AstralaneTransport::Binary)``/irisb`, bincode body.
- **Plain**: `Some(AstralaneTransport::Plain)``/iris`.
- **QUIC**: `Some(AstralaneTransport::Quic)` — regional `host:7000` / `:9000` (MEV); same API key.
---
@@ -350,7 +364,7 @@ You can apply for a key through the official website: [Community Website](https:
- **FlashBlock**: High-speed transaction execution with API key authentication
- **BlockRazor**: High-speed transaction execution with API key authentication
- **Node1**: High-speed transaction execution with API key authentication
- **Astralane**: Blockchain network acceleration (supports HTTP and QUIC; see [Astralane QUIC](#astralane-quic-low-latency) above)
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC; see [Astralane](#astralane-binary--plain--quic) above)
## 📁 Project Structure
+34 -20
View File
@@ -48,7 +48,7 @@
- [⚡ 交易参数](#-交易参数)
- [📊 使用示例汇总表格](#-使用示例汇总表格)
- [⚙️ SWQoS 服务配置说明](#-swqos-服务配置说明)
- [Astralane QUIC(低延迟](#astralane-quic低延迟)
- [AstralaneBinary / Plain / QUIC](#astralanebinary--plain--quic)
- [🔧 中间件系统说明](#-中间件系统说明)
- [🔍 地址查找表](#-地址查找表)
- [🔍 Nonce 缓存](#-nonce-缓存)
@@ -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 = "4.0.1" }
sol-trade-sdk = { path = "./sol-trade-sdk", version = "4.0.3" }
```
### 使用 crates.io
```toml
# 添加到您的 Cargo.toml
sol-trade-sdk = "4.0.1"
sol-trade-sdk = "4.0.3"
```
## 🛠️ 使用示例
@@ -120,21 +131,24 @@ let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
// Astralane:第4个参数 None 为 HTTPSome(SwqosTransport::Quic) 为 QUIC;同一 API key
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain/iris)或 Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
SwqosConfig::Astralane(
"your_astralane_api_key".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
Some(AstralaneTransport::Quic),
), // 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 - MEVAstralane QUIC :9000 或 HTTP mev-protect / BlockRazor
.build();
// 创建 TradingClient
let client = TradingClient::new(Arc::new(payer), trade_config).await;
@@ -265,28 +279,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
#### Astralane QUIC(低延迟
#### AstralaneBinary / Plain / QUIC
Astralane 支持 **HTTP****QUIC** 两种传输方式。QUIC 可减少连接开销,降低提交延迟。使用 QUIC 时,将 `SwqosConfig::Astralane`第四个参数设为 `Some(SwqosTransport::Quic)`。Astralane 的 QUIC 服务使用**单一端点**(无分区域端点),选 QUIC 时 SDK 会忽略 `region` 与可选自定义 URL;为与其他 SWQoS 配置一致,可传入相同 region
Astralane 支持 **Binary** HTTP`/irisb`)、**Plain** HTTP`/iris`)与 **QUIC**`host:7000`,全局 `mev_protection` 为 true 时用 `:9000`)。第四个参数`Some(AstralaneTransport::Plain)``Some(AstralaneTransport::Quic)`,或 `None` 表示 **Binary**(默认)。全局 `mev_protection` 会在 HTTP 上附加 `mev-protect=true`,或为 QUIC 选择 9000 端口
```rust
use sol_trade_sdk::{SwqosConfig, SwqosRegion, SwqosTransport};
use sol_trade_sdk::{SwqosConfig, SwqosRegion, AstralaneTransport};
// Astralane 使用 QUIC(低延迟);region 会被忽略(QUIC 单一端点)
let swqos_configs: Vec<SwqosConfig> = vec![
SwqosConfig::Default(rpc_url.clone()),
SwqosConfig::Astralane(
"your_astralane_api_key".to_string(),
SwqosRegion::Frankfurt, // 与其他服务一致即可;QUIC 时会被忽略
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
Some(AstralaneTransport::Quic),
),
];
// 然后照常使用 swqos_configs 创建 TradeConfig / TradingClient
```
- **HTTP**(默认):第四个参数为 `None``Some(SwqosTransport::Http)`region 与可选自定义 URL 生效
- **QUIC**:第四个参数为 `Some(SwqosTransport::Quic)`;SDK 使用单一 QUIC 端点并忽略 region。与 HTTP 使用同一 API key
- **Binary**(默认):`None``Some(AstralaneTransport::Binary)``/irisb`bincode 正文
- **Plain**`Some(AstralaneTransport::Plain)``/iris`
- **QUIC**`Some(AstralaneTransport::Quic)` — 按区域的 `host:7000` / `:9000`MEV);同一 API key。
---
@@ -349,7 +363,7 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
- **Node1**: 高速交易执行,支持 API 密钥认证
- **Astralane**: 区块链网络加速(支持 HTTP 与 QUIC,见上方 [Astralane QUIC](#astralane-quic低延迟)
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC,见 [Astralane](#astralanebinary--plain--quic)
## 📁 项目结构
+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)
+7 -8
View File
@@ -10,7 +10,7 @@
use sol_trade_sdk::{
common::{AnyResult, InfrastructureConfig, TradeConfig},
swqos::{SwqosConfig, SwqosRegion},
SwqosTransport, TradingClient, TradingInfrastructure,
AstralaneTransport, TradingClient, TradingInfrastructure,
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::signature::Keypair;
@@ -52,17 +52,16 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
"your_api_token".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
), // QUIC; use None for HTTP
Some(AstralaneTransport::Quic),
), // QUICNone / Some(Binary) / Some(Plain) 为 HTTP
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
];
// Optional: Customize WSOL ATA and Seed optimization settings
let trade_config = TradeConfig::new(rpc_url, swqos_configs, commitment).with_wsol_ata_config(
true, // create_wsol_ata_on_startup: Check and create WSOL ATA on startup
true, // use_seed_optimize: Enable seed optimization for all ATA operations
);
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
.create_wsol_ata_on_startup(true)
.use_seed_optimize(true)
.build();
// Creates new infrastructure internally
let client = TradingClient::new(Arc::new(payer), trade_config).await;
+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 `:9000` or HTTP `mev-protect=true`, BlockRazor) use MEV-protected
/// endpoints/modes. Default false.
pub mev_protection: bool,
}
impl InfrastructureConfig {
@@ -24,6 +28,7 @@ impl InfrastructureConfig {
swqos_configs,
commitment,
swqos_cores_from_end: false,
mev_protection: false,
}
}
@@ -34,6 +39,7 @@ impl InfrastructureConfig {
swqos_configs: config.swqos_configs.clone(),
commitment: config.commitment.clone(),
swqos_cores_from_end: config.swqos_cores_from_end,
mev_protection: config.mev_protection,
}
}
@@ -53,6 +59,7 @@ impl Hash for InfrastructureConfig {
self.swqos_configs.hash(state);
format!("{:?}", self.commitment).hash(state);
self.swqos_cores_from_end.hash(state);
self.mev_protection.hash(state);
}
}
@@ -62,6 +69,7 @@ impl PartialEq for InfrastructureConfig {
&& self.swqos_configs == other.swqos_configs
&& self.commitment == other.commitment
&& self.swqos_cores_from_end == other.swqos_cores_from_end
&& self.mev_protection == other.mev_protection
}
}
@@ -83,52 +91,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 `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
pub mev_protection: bool,
}
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`; **Astralane HTTP** adds `mev-protect=true`
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
///
/// May reduce landing speed. Default: `false`.
pub fn mev_protection(mut self, v: bool) -> Self {
self.mev_protection = v;
self
}
/// 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;
+223 -82
View File
@@ -152,6 +152,12 @@ pub const SOYAS_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"),
pubkey!("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"),
pubkey!("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"),
pubkey!("soyasi59njacMUPvo3TM5paHjeK8pYSdovXgFi32gRt"),
pubkey!("soyasQYhJxv8uZgWDxhg72td6piAf7XTkoyWHtSATEz"),
pubkey!("soyastP66xyYC8XADXZjdMM5BAVGD2YRvz8dwtLsqb8"),
pubkey!("soyasvdgUJWYcUCzDxpmjUnNjH7KamXLXTzLwFvdVPE"),
pubkey!("soyasvxAunisNxaoRxkKGjNir7KmbwYnr37JmefkX9G"),
pubkey!("soyas5doVFUwH8s5zK8gEvCL5KR5ogDmf52LsrJEZ9h"),
];
pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
@@ -165,202 +171,267 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
];
// NewYork,
// Frankfurt,
// Amsterdam,
// SLC,
// Tokyo,
// London,
// LosAngeles,
// Default,
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
//
// **地理就近(用户语义)**:当某枚举区域没有该服务商**独立公布**的 PoP 时,在**该服务商已出现的端点集合内**,按真实地理位置选**大圆距离最近**的一项作为填充;行尾注释说明依据。
// **例外**`SwqosRegion::Default`(下标 9)不表示地球上的点,表中为全局 URL 或文档默认枢纽,**不适用**地理就近,仅表示「未指定区域时的回退」。
// 若某区域仅有一种「大区」级入口(例如全美只有一个美东 PoP),则地理上非最优但只能复用,注释会标明「受服务商可用区限制」。
pub const SWQOS_ENDPOINTS_JITO: [&str; 8] = [
/// Jito mainnet block engines (`https://<region>.mainnet.block-engine.jito.wtf`).
/// There is no Los Angeles engine → use Salt Lake City for `LosAngeles`; `SwqosRegion::Default` uses the global mainnet URL.
pub const SWQOS_ENDPOINTS_JITO: [&str; 10] = [
"https://ny.mainnet.block-engine.jito.wtf",
"https://frankfurt.mainnet.block-engine.jito.wtf",
"https://amsterdam.mainnet.block-engine.jito.wtf",
"https://dublin.mainnet.block-engine.jito.wtf",
"https://slc.mainnet.block-engine.jito.wtf",
"https://tokyo.mainnet.block-engine.jito.wtf",
"https://singapore.mainnet.block-engine.jito.wtf",
"https://london.mainnet.block-engine.jito.wtf",
"https://ny.mainnet.block-engine.jito.wtf",
"https://slc.mainnet.block-engine.jito.wtf", // LosAngeles: no LA PoP; nearest US-West is SLC
"https://mainnet.block-engine.jito.wtf",
];
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 8] = [
/// NextBlock regional HTTP hosts (see provider docs). `SwqosRegion` order; no dedicated LA PoP → SLC as US-West fallback.
pub const SWQOS_ENDPOINTS_NEXTBLOCK: [&str; 10] = [
"http://ny.nextblock.io",
"http://frankfurt.nextblock.io",
"http://amsterdam.nextblock.io",
"http://fra.nextblock.io",
"http://ams.nextblock.io",
"http://dublin.nextblock.io",
"http://slc.nextblock.io",
"http://tokyo.nextblock.io",
"http://sgp.nextblock.io",
"http://london.nextblock.io",
"http://singapore.nextblock.io",
"http://frankfurt.nextblock.io",
"http://slc.nextblock.io",
"http://fra.nextblock.io", // Default: 非地理区域;服务商无「全局」主机名时用 EU 枢纽作未选区回退
];
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 8] = [
pub const SWQOS_ENDPOINTS_ZERO_SLOT: [&str; 10] = [
"http://ny.0slot.trade",
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
"http://ams.0slot.trade",
"http://ny.0slot.trade",
"http://ams.0slot.trade", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams(相对 de2 等)
"http://la.0slot.trade", // SLC: no UT PoP; nearest US-West published host
"http://jp.0slot.trade",
"http://ams.0slot.trade",
"http://jp.0slot.trade", // SG: 无本地 PoP;已公布 APAC 仅 jp,为表中离新加坡最近的大圆距离
"http://ams.0slot.trade", // London: 无 UK 专用;已公布 EU 点中 ams 距伦敦最近之一
"http://la.0slot.trade",
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
"http://de2.0slot.trade", // Default: 非地理区域;EU 枢纽 de2 作未选区回退
];
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 8] = [
"http://ewr1.nozomi.temporal.xyz",
/// Nozomi Direct regions: ewr1, fra2, ams1, lon1, lax1, tyo1, sgp1, …
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 10] = [
"http://ewr1.nozomi.temporal.xyz", // NewYork → Newark
"http://fra2.nozomi.temporal.xyz",
"http://ams1.nozomi.temporal.xyz",
"http://ewr1.nozomi.temporal.xyz",
"http://lon1.nozomi.temporal.xyz", // Dublin: no IE host; UK nearest Direct PoP
"http://lax1.nozomi.temporal.xyz", // SLC: US-West
"http://tyo1.nozomi.temporal.xyz",
"http://sgp1.nozomi.temporal.xyz",
"http://pit1.nozomi.temporal.xyz",
"http://fra2.nozomi.temporal.xyz",
"http://lon1.nozomi.temporal.xyz",
"http://lax1.nozomi.temporal.xyz",
"http://fra2.nozomi.temporal.xyz", // Default: 非地理区域;EU Direct 枢纽
];
pub const SWQOS_ENDPOINTS_BLOX: [&str; 8] = [
pub const SWQOS_ENDPOINTS_BLOX: [&str; 10] = [
"https://ny.solana.dex.blxrbdn.com",
"https://germany.solana.dex.blxrbdn.com",
"https://amsterdam.solana.dex.blxrbdn.com",
"https://ny.solana.dex.blxrbdn.com",
"https://uk.solana.dex.blxrbdn.com", // Dublin: IE/UK edge
"https://la.solana.dex.blxrbdn.com", // SLC: no Mountain PoP; US-West LA
"https://tokyo.solana.dex.blxrbdn.com",
"https://tokyo.solana.dex.blxrbdn.com", // SG: 文档无 SGP 区域;已公布 APAC 仅 Tokyo,为距 SG 最近选项
"https://uk.solana.dex.blxrbdn.com",
"https://la.solana.dex.blxrbdn.com",
"https://global.solana.dex.blxrbdn.com",
"https://global.solana.dex.blxrbdn.com", // Default: 非地理区域;全球任播
];
pub const SWQOS_ENDPOINTS_NODE1: [&str; 8] = [
pub const SWQOS_ENDPOINTS_NODE1: [&str; 10] = [
"http://ny.node1.me",
"http://fra.node1.me",
"http://ams.node1.me",
"http://ny.node1.me",
"http://lon.node1.me", // Dublin: 已公布中英爱区域用 lon(地理上近爱尔兰)
"http://ny.node1.me", // SLC: 已公布美国仅 ny;美西无 PoP,受可用区限制复用美东
"http://tk.node1.me",
"http://tk.node1.me", // SG: 已公布 APAC 仅 tk;地理上为表中离 SG 最近
"http://lon.node1.me",
"http://ny.node1.me",
"http://fra.node1.me",
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
"http://fra.node1.me", // Default: 非地理区域;与 QUIC 对齐为 EU 枢纽
];
/// Node1 QUIC: port 16666. Region order: NewYork, Frankfurt, Amsterdam, SLC→ny, Tokyo, London, LosAngeles→ny, Default→ny.
/// Node1 QUIC: port 16666. Region order matches [`SwqosRegion`].
/// server_name = host part (e.g. ny.node1.me). Auth: first bi stream = 16-byte UUID; each tx = new bi stream, bincode body.
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 8] = [
pub const SWQOS_ENDPOINTS_NODE1_QUIC: [&str; 10] = [
"ny.node1.me:16666",
"fra.node1.me:16666",
"ams.node1.me:16666",
"ny.node1.me:16666", // SLC → ny
"lon.node1.me:16666",
"ny.node1.me:16666",
"tk.node1.me:16666",
"tk.node1.me:16666",
"lon.node1.me:16666",
"ny.node1.me:16666", // LA → ny
"ny.node1.me:16666", // Default → ny
"ny.node1.me:16666",
"fra.node1.me:16666", // Default: 非地理区域;与 HTTP 对齐为 EU 枢纽
];
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [
/// Published: ny, slc, ams, fra, singapore, london, tokyo (no IE/UK split → london for Dublin).
pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 10] = [
"http://ny.flashblock.trade",
"http://fra.flashblock.trade",
"http://ams.flashblock.trade",
"http://london.flashblock.trade", // Dublin: no IE host; UK nearest
"http://slc.flashblock.trade",
"http://tokyo.flashblock.trade",
"http://singapore.flashblock.trade",
"http://london.flashblock.trade",
"http://ny.flashblock.trade",
"http://ny.flashblock.trade",
"http://slc.flashblock.trade",
"http://fra.flashblock.trade", // Default: 非地理区域;EU 枢纽
];
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
/// 若 HTTP 返回 500,可尝试 HTTPShttps://<region>.solana.blockrazor.io/v2/sendTransactionFrankfurt/NewYork/Tokyo),通过 custom_url 覆盖。
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 10] = [
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://amsterdam.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction", // Dublin: UK nearest published
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // SLC: 文档无美西;美国仅 NY,受可用区限制
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction", // SG: 已公布 APAC 仅 Tokyo,为距 SG 最近
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // LosAngeles: 无美西入口;美国仅 NY
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction", // Default: 非地理区域;EU 枢纽
];
/// BlockRazor gRPC endpoints. Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
/// BlockRazor gRPC endpoints. Region order matches [`SwqosRegion`].
/// Port 80 for gRPC protocol. Auth: apikey metadata in gRPC headers.
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 8] = [
pub const SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC: [&str; 10] = [
"http://newyork.solana-grpc.blockrazor.xyz:80",
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
"http://amsterdam.solana-grpc.blockrazor.xyz:80",
"http://newyork.solana-grpc.blockrazor.xyz:80",
"http://london.solana-grpc.blockrazor.xyz:80",
"http://newyork.solana-grpc.blockrazor.xyz:80", // SLC: 与 HTTP 一致;美国仅 NY
"http://tokyo.solana-grpc.blockrazor.xyz:80",
"http://tokyo.solana-grpc.blockrazor.xyz:80",
"http://london.solana-grpc.blockrazor.xyz:80",
"http://newyork.solana-grpc.blockrazor.xyz:80",
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
"http://newyork.solana-grpc.blockrazor.xyz:80", // LosAngeles: 与 HTTP 一致
"http://frankfurt.solana-grpc.blockrazor.xyz:80", // Default: 非地理区域
];
/// Astralane binary API path (no Base64; use with ?api-key=...&method=sendTransaction|getHealth).
/// Plain HTTP API path (`/iris?api-key=&method=…`).
pub const ASTRALANE_PATH_IRIS: &str = "iris";
/// Binary HTTP API path (`/irisb?api-key=…&method=sendTransaction`, raw bincode body).
pub const ASTRALANE_PATH_IRISB: &str = "irisb";
pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [
/// Astralane **Plain** HTTP gateways (`/iris`). Pair with [`ASTRALANE_PATH_IRIS`].
pub const SWQOS_ENDPOINTS_ASTRALANE_PLAIN: [&str; 10] = [
"http://ny.gateway.astralane.io/iris",
"http://fr.gateway.astralane.io/iris",
"http://ams.gateway.astralane.io/iris",
"http://ams.gateway.astralane.io/iris", // Dublin: 无 IE 专用;在已公布 EU 点中选距爱尔兰最近的 ams
"http://la.gateway.astralane.io/iris",
"http://jp.gateway.astralane.io/iris",
"http://sg.gateway.astralane.io/iris",
"http://ams.gateway.astralane.io/iris", // London: 无 UK 专用;在已公布 EU 点中选距英国最近的 ams
"http://la.gateway.astralane.io/iris",
"https://edge.astralane.io/iris", // Default: 非地理区域;全局任播边缘
];
/// Astralane **Binary** HTTP gateways (`/irisb`). Pair with [`ASTRALANE_PATH_IRISB`].
pub const SWQOS_ENDPOINTS_ASTRALANE_BINARY: [&str; 10] = [
"http://ny.gateway.astralane.io/irisb",
"http://fr.gateway.astralane.io/irisb",
"http://ams.gateway.astralane.io/irisb",
"http://ny.gateway.astralane.io/irisb",
"http://ams.gateway.astralane.io/irisb", // Dublin: 同 Plain
"http://la.gateway.astralane.io/irisb",
"http://jp.gateway.astralane.io/irisb",
"http://ny.gateway.astralane.io/irisb",
"http://lax.gateway.astralane.io/irisb",
"http://lim.gateway.astralane.io/irisb",
"http://sg.gateway.astralane.io/irisb",
"http://ams.gateway.astralane.io/irisb", // London: 同 Plain
"http://la.gateway.astralane.io/irisb",
"https://edge.astralane.io/irisb", // Default: 同 Plain
];
/// Astralane QUIC endpoints (port 7000). Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default.
/// See: https://github.com/Astralane/astralane-quic-client. We use fr, ams, la, ny, lim, sg only (avoid ams2/fr2 for lower latency).
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
"ny.gateway.astralane.io:7000", // NewYork
"fr.gateway.astralane.io:7000", // Frankfurt
"ams.gateway.astralane.io:7000", // Amsterdam
"lim.gateway.astralane.io:7000", // SLC (no slc, use lim)
"sg.gateway.astralane.io:7000", // Tokyo (Asia)
"ams.gateway.astralane.io:7000", // London (Europe, avoid ams2)
"la.gateway.astralane.io:7000", // LosAngeles
"lim.gateway.astralane.io:7000", // Default
/// Astralane QUIC endpoints (port 7000). Region order matches [`SwqosRegion`].
/// See: https://github.com/Astralane/astralane-quic-client.
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 10] = [
"ny.gateway.astralane.io:7000",
"fr.gateway.astralane.io:7000",
"ams.gateway.astralane.io:7000",
"ams.gateway.astralane.io:7000", // Dublin: 同 HTTP
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
"jp.gateway.astralane.io:7000",
"sg.gateway.astralane.io:7000",
"ams.gateway.astralane.io:7000", // London: 同 HTTP
"la.gateway.astralane.io:7000",
"lim.gateway.astralane.io:7000", // Default: 非地理区域;全局边缘
];
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 10] = [
"ny.gateway.astralane.io:9000",
"fr.gateway.astralane.io:9000",
"ams.gateway.astralane.io:9000",
"ams.gateway.astralane.io:9000",
"la.gateway.astralane.io:9000",
"jp.gateway.astralane.io:9000",
"sg.gateway.astralane.io:9000",
"ams.gateway.astralane.io:9000",
"la.gateway.astralane.io:9000",
"lim.gateway.astralane.io:9000",
];
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 10] = [
"http://ewr1.flashrpc.com",
"http://fra1.flashrpc.com",
"http://ams1.flashrpc.com",
"http://ewr1.flashrpc.com",
"http://lhr1.flashrpc.com", // Dublin: 已公布 UK 用 lhr;地理上近爱尔兰
"http://ewr1.flashrpc.com", // SLC: 已公布美国仅 ewr;无美西 PoP,受可用区限制
"http://tyo1.flashrpc.com",
"http://tyo1.flashrpc.com", // SG: 表中无 SGPAPAC 仅 tyo,为距 SG 最近
"http://lhr1.flashrpc.com",
"http://ewr1.flashrpc.com",
"http://fra1.flashrpc.com",
"http://ewr1.flashrpc.com", // LosAngeles: 同上,美国仅 ewr
"http://fra1.flashrpc.com", // Default: 非地理区域;EU 枢纽
];
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 8] = [
pub const SWQOS_ENDPOINTS_SOYAS: [&str; 10] = [
"nyc.landing.soyas.xyz:9000",
"fra.landing.soyas.xyz:9000",
"ams.landing.soyas.xyz:9000",
"nyc.landing.soyas.xyz:9000",
"lon.landing.soyas.xyz:9000", // Dublin: 已公布用 lon;地理近爱尔兰
"nyc.landing.soyas.xyz:9000", // SLC: 已公布美国仅 nyc;无美西
"tyo.landing.soyas.xyz:9000",
"tyo.landing.soyas.xyz:9000", // SG: 表中 APAC 仅 tyo
"lon.landing.soyas.xyz:9000",
"nyc.landing.soyas.xyz:9000",
"fra.landing.soyas.xyz:9000",
"nyc.landing.soyas.xyz:9000", // LosAngeles: 同上
"fra.landing.soyas.xyz:9000", // Default: 非地理区域;EU 枢纽
];
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [
pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 10] = [
"nyc.speedlanding.trade:17778",
"fra.speedlanding.trade:17778",
"ams.speedlanding.trade:17778",
"nyc.speedlanding.trade:17778",
"ams.speedlanding.trade:17778", // Dublin: 已公布 EU 点中 ams 地理近爱尔兰
"nyc.speedlanding.trade:17778", // SLC: 表中美国仅 nyc;无美西 PoP,受可用区限制
"tyo.speedlanding.trade:17778",
"fra.speedlanding.trade:17778",
"nyc.speedlanding.trade:17778",
"fra.speedlanding.trade:17778",
"sgp.speedlanding.trade:17778",
"ams.speedlanding.trade:17778", // London: 已公布 EU 中 ams 距英国最近之一
"nyc.speedlanding.trade:17778", // LosAngeles: 同上,美国仅 nyc
"fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽
];
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
/// Region order: NewYork(EWR), Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles(SG), Default(Global).
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
"http://ewr-sender.helius-rpc.com/fast",
"http://fra-sender.helius-rpc.com/fast",
"http://ams-sender.helius-rpc.com/fast",
"http://lon-sender.helius-rpc.com/fast", // Dublin: IE → UK/EU routing
"http://slc-sender.helius-rpc.com/fast",
"http://tyo-sender.helius-rpc.com/fast",
"http://lon-sender.helius-rpc.com/fast",
"http://sg-sender.helius-rpc.com/fast",
"https://sender.helius-rpc.com/fast",
"http://lon-sender.helius-rpc.com/fast",
"http://slc-sender.helius-rpc.com/fast",
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
];
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
@@ -381,3 +452,73 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
#[cfg(test)]
mod tests {
use super::*;
const SWQOS_REGION_ENDPOINT_TABLES: &[&[&str]] = &[
&SWQOS_ENDPOINTS_JITO,
&SWQOS_ENDPOINTS_NEXTBLOCK,
&SWQOS_ENDPOINTS_ZERO_SLOT,
&SWQOS_ENDPOINTS_TEMPORAL,
&SWQOS_ENDPOINTS_BLOX,
&SWQOS_ENDPOINTS_NODE1,
&SWQOS_ENDPOINTS_NODE1_QUIC,
&SWQOS_ENDPOINTS_FLASHBLOCK,
&SWQOS_ENDPOINTS_BLOCKRAZOR,
&SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC,
&SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
&SWQOS_ENDPOINTS_ASTRALANE_BINARY,
&SWQOS_ENDPOINTS_ASTRALANE_QUIC,
&SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
&SWQOS_ENDPOINTS_STELLIUM,
&SWQOS_ENDPOINTS_SOYAS,
&SWQOS_ENDPOINTS_SPEEDLANDING,
&SWQOS_ENDPOINTS_HELIUS,
];
#[test]
fn all_swqos_endpoint_tables_align_with_swqos_region() {
const EXPECT: usize = 10;
for (idx, table) in SWQOS_REGION_ENDPOINT_TABLES.iter().enumerate() {
assert_eq!(
table.len(),
EXPECT,
"SWQOS endpoint table index {} length must match SwqosRegion (10 variants)",
idx
);
}
}
#[test]
fn astralane_quic_hosts_match_mev_row_by_row() {
for i in 0..10 {
let base = SWQOS_ENDPOINTS_ASTRALANE_QUIC[i].trim_end_matches(":7000");
let mev = SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[i].trim_end_matches(":9000");
assert_eq!(base, mev, "Astralane QUIC vs MEV host mismatch at index {}", i);
}
}
#[test]
fn node1_http_host_matches_quic_without_port() {
for i in 0..10 {
let http_host = SWQOS_ENDPOINTS_NODE1[i]
.strip_prefix("http://")
.expect("NODE1 HTTP URL");
let quic_host = SWQOS_ENDPOINTS_NODE1_QUIC[i]
.strip_suffix(":16666")
.expect("NODE1 QUIC endpoint");
assert_eq!(http_host, quic_host, "Node1 HTTP vs QUIC host mismatch at index {}", i);
}
}
#[test]
fn astralane_plain_and_binary_same_origin_per_region() {
for i in 0..10 {
let plain = SWQOS_ENDPOINTS_ASTRALANE_PLAIN[i].trim_end_matches("/iris");
let binary = SWQOS_ENDPOINTS_ASTRALANE_BINARY[i].trim_end_matches("/irisb");
assert_eq!(plain, binary, "Astralane Plain vs Binary base URL mismatch at index {}", i);
}
}
}
+5 -1
View File
@@ -87,7 +87,11 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
&params.payer.pubkey(),
&params.payer.pubkey(),
&params.output_mint,
&crate::constants::TOKEN_PROGRAM,
if is_a_in {
&protocol_params.token_b_program
} else {
&protocol_params.token_a_program
},
params.open_seed_optimize,
),
);
+57 -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_protocol_extra_fee_recipient_random, 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)
@@ -171,6 +177,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
accounts::FEE_PROGRAM_META,
];
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // remainingAccounts: @pump-fun/pump-sdk 要求末尾传 bondingCurveV2Pda(mint),勿删
// Apr 2026: extra protocol fee recipient after bonding-curve-v2 (writable)
accounts.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, accounts));
@@ -197,8 +205,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 +236,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 +250,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 +274,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,
@@ -300,6 +306,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint)
})?;
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false));
accounts.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, accounts));
+29 -6
View File
@@ -2,9 +2,9 @@ use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::utils::pumpswap::{
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
get_user_volume_accumulator_pda, get_user_volume_accumulator_quote_ata,
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR,
SELL_DISCRIMINATOR,
get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
get_user_volume_accumulator_quote_ata, get_user_volume_accumulator_wsol_ata,
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
trading::{
common::wsol_manager,
@@ -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 {
@@ -156,7 +166,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
}
// Create buy instruction
let mut accounts = Vec::with_capacity(23);
let mut accounts = Vec::with_capacity(28);
accounts.extend([
AccountMeta::new(pool, false), // pool_id
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
@@ -196,6 +206,13 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let pool_v2 = get_pool_v2_pda(&base_mint)
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
accounts.push(AccountMeta::new_readonly(pool_v2, false));
// Apr 2026: protocol fee recipient + quote ATA (after pool-v2)
let protocol_extra = get_protocol_extra_fee_recipient_random();
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
accounts.push(AccountMeta::new(
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
false,
));
// Create instruction databuy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数)
let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
@@ -349,7 +366,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
}
// Create sell instruction
let mut accounts = Vec::with_capacity(23);
let mut accounts = Vec::with_capacity(28);
accounts.extend([
AccountMeta::new(pool, false), // pool_id
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
@@ -397,6 +414,12 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let pool_v2 = get_pool_v2_pda(&base_mint)
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
accounts.push(AccountMeta::new_readonly(pool_v2, false));
let protocol_extra = get_protocol_extra_fee_recipient_random();
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
accounts.push(AccountMeta::new(
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
false,
));
// Create instruction data
let mut data = [0u8; 24];
+237 -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 {
@@ -102,6 +136,19 @@ pub mod global_constants {
pub const PUMPFUN_AMM_FEE_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
// Pump.fun AMM: Protocol Fee 7
/// Protocol extra fee recipients (Apr 2026 breaking upgrade). One is appended after `bonding-curve-v2`, **writable**.
/// See: <https://github.com/pump-fun/pump-public-docs/blob/main/docs/BREAKING_FEE_RECIPIENT.md>
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
];
}
/// Constants related to program accounts and authorities
@@ -172,7 +219,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 +247,54 @@ 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,
}
}
/// Random entry from [`global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (must be last account after bonding-curve-v2, writable).
#[inline]
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
*global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS
.choose(&mut rand::rng())
.unwrap_or(&global_constants::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
}
/// 账户 #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 +356,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 +506,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)
);
}
}
+91 -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 {
@@ -93,6 +91,18 @@ pub mod accounts {
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
/// Protocol extra fee recipients (Apr 2026 breaking upgrade). After `pool-v2`: recipient (readonly), then quote ATA (writable).
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
];
// META
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
@@ -173,6 +183,14 @@ pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
(recipient, meta)
}
/// Random entry from [`accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS`] (readonly; paired with [`fee_recipient_ata`] as last account).
#[inline]
pub fn get_protocol_extra_fee_recipient_random() -> Pubkey {
*accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS
.choose(&mut rand::rng())
.unwrap_or(&accounts::PROTOCOL_EXTRA_FEE_RECIPIENTS[0])
}
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
#[inline]
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
@@ -301,116 +319,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 +436,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
))
}
+68 -11
View File
@@ -19,8 +19,8 @@ use crate::swqos::common::TradeError;
use crate::swqos::SwqosClient;
use crate::swqos::SwqosConfig;
use crate::swqos::TradeType;
// Re-export for SWQOS HTTP/QUIC choice in SwqosConfig (e.g. Astralane)
pub use crate::swqos::SwqosTransport;
// Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode)
pub use crate::swqos::{AstralaneTransport, SwqosTransport};
use crate::trading::core::params::BonkParams;
use crate::trading::core::params::DexParamEnum;
use crate::trading::core::params::MeteoraDammV2Params;
@@ -136,8 +136,8 @@ impl TradingInfrastructure {
}
common::seed::start_rent_updater(rpc.clone());
// Create SWQOS clients with blacklist checking(单节点超时 5s,避免某一家卡死整段初始化
const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
// Create SWQOS clients with blacklist checkingQUIC 握手可能较慢,单节点超时 15s
const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
for swqos in &config.swqos_configs {
if swqos.is_blacklisted() {
@@ -152,12 +152,18 @@ impl TradingInfrastructure {
config.rpc_url.clone(),
config.commitment.clone(),
swqos.clone(),
config.mev_protection,
),
)
.await
{
Ok(Ok(swqos_client)) => swqos_clients.push(swqos_client),
Ok(Err(err)) => {
eprintln!(
"⚠️ SWQOS {:?} 初始化失败: {}(已从列表中排除)",
swqos.swqos_type(),
err
);
if sdk_log::sdk_log_enabled() {
warn!(
target: "sol_trade_sdk",
@@ -167,6 +173,11 @@ impl TradingInfrastructure {
}
}
Err(_) => {
eprintln!(
"⚠️ SWQOS {:?} 初始化超时({}s),已跳过",
swqos.swqos_type(),
SWQOS_CLIENT_TIMEOUT.as_secs()
);
if sdk_log::sdk_log_enabled() {
warn!(
target: "sol_trade_sdk",
@@ -179,6 +190,52 @@ impl TradingInfrastructure {
}
}
// 若全部失败、被黑名单跳过或仅配置了不可用通道,至少保留一条 Rpc Default,否则 execute_parallel 会因 swqos_clients 为空直接报错。
if swqos_clients.is_empty() {
eprintln!(
"⚠️ 无任何 SWQOS 客户端初始化成功,将回退为普通 RPC 发送: {}",
config.rpc_url
);
if sdk_log::sdk_log_enabled() {
warn!(
target: "sol_trade_sdk",
"no SWQOS clients initialized; falling back to Rpc Default ({})",
config.rpc_url
);
}
match SwqosConfig::get_swqos_client(
config.rpc_url.clone(),
config.commitment.clone(),
SwqosConfig::Default(config.rpc_url.clone()),
config.mev_protection,
)
.await
{
Ok(c) => swqos_clients.push(c),
Err(e) => {
if sdk_log::sdk_log_enabled() {
warn!(
target: "sol_trade_sdk",
"fallback Rpc Default client failed: {}",
e
);
}
}
}
}
if !swqos_clients.is_empty() {
let labels: Vec<&str> = swqos_clients
.iter()
.map(|c| c.get_swqos_type().as_str())
.collect();
eprintln!(
"️ SWQOS 通道已就绪: {} 条 → [{}]",
swqos_clients.len(),
labels.join(", ")
);
}
let swqos_count = swqos_clients.len();
let (max_sender_concurrency, effective_core_ids) = {
let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0);
@@ -740,7 +797,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 +871,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 +904,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 +978,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 +1013,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"));
}
+13 -5
View File
@@ -27,6 +27,8 @@ pub enum AstralaneBackend {
Http {
endpoint: String,
auth_token: String,
/// Mirrors global `mev_protection`: adds `mev-protect=true` on HTTP sends (QUIC uses :9000 instead).
mev_http: bool,
http_client: Client,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
@@ -77,8 +79,8 @@ impl SwqosClientTrait for AstralaneClient {
}
impl AstralaneClient {
/// 使用 HTTPirisb)提交
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
/// HTTP 提交:`/iris`Plain)或 `/irisb`Binary),由 `endpoint` URL 路径区分;`mev_http` 为 true 时附加 `mev-protect=true`
pub fn new(rpc_url: String, endpoint: String, auth_token: String, mev_http: bool) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
@@ -89,6 +91,7 @@ impl AstralaneClient {
backend: AstralaneBackend::Http {
endpoint,
auth_token,
mev_http,
http_client,
ping_handle,
stop_ping,
@@ -119,6 +122,7 @@ impl AstralaneClient {
http_client,
ping_handle,
stop_ping,
..
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
@@ -182,10 +186,14 @@ impl AstralaneClient {
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
match &self.backend {
AstralaneBackend::Http { endpoint, auth_token, http_client, .. } => {
let response = http_client
AstralaneBackend::Http { endpoint, auth_token, mev_http, http_client, .. } => {
let mut req = http_client
.post(endpoint)
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")])
.query(&[("api-key", auth_token.as_str()), ("method", "sendTransaction")]);
if *mev_http {
req = req.query(&[("mev-protect", "true")]);
}
let response = req
.header("Content-Type", "application/octet-stream")
.body(body_bytes)
.send()
+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(())
}
+91 -14
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};
@@ -85,9 +86,11 @@ pub enum BlockRazorBackend {
Grpc {
endpoint: String,
auth_token: String,
grpc_client: Arc<BlockRazorGrpcClient>,
grpc_client: Arc<ArcSwap<BlockRazorGrpcClient>>,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
/// When true, gRPC send_transaction sets revert_protection=true for MEV protection.
mev_protection: bool,
},
Http {
endpoint: String,
@@ -95,6 +98,8 @@ pub enum BlockRazorBackend {
http_client: Client,
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
/// When true, HTTP request adds revertProtection=true query param for MEV protection.
mev_protection: bool,
},
}
@@ -143,10 +148,10 @@ impl SwqosClientTrait for BlockRazorClient {
impl BlockRazorClient {
pub async fn new(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
// 默认使用 HTTP 模式,避免 gRPC FRAME_SIZE_ERROR
Ok(Self::new_http(rpc_url, endpoint, auth_token))
Ok(Self::new_http(rpc_url, endpoint, auth_token, false))
}
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String) -> Result<Self> {
pub async fn new_grpc(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
// 配置 Channel,增加连接超时
@@ -157,7 +162,10 @@ impl BlockRazorClient {
.await
.map_err(|e| anyhow::anyhow!("Failed to connect to gRPC endpoint: {}", e))?;
let grpc_client = Arc::new(BlockRazorGrpcClient::new(channel, auth_token.clone()));
let grpc_client = Arc::new(ArcSwap::from_pointee(BlockRazorGrpcClient::new(
channel,
auth_token.clone(),
)));
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
let stop_ping = Arc::new(AtomicBool::new(false));
@@ -169,6 +177,7 @@ impl BlockRazorClient {
grpc_client,
ping_handle,
stop_ping,
mev_protection,
},
};
@@ -180,7 +189,7 @@ impl BlockRazorClient {
Ok(client)
}
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String) -> Self {
pub fn new_http(rpc_url: String, endpoint: String, auth_token: String, mev_protection: bool) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().user_agent("").build().unwrap();
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
@@ -194,6 +203,7 @@ impl BlockRazorClient {
http_client,
ping_handle,
stop_ping,
mev_protection,
},
};
@@ -211,27 +221,68 @@ impl BlockRazorClient {
grpc_client,
ping_handle,
stop_ping,
endpoint,
auth_token,
..
} => {
let grpc_client = grpc_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let handle = tokio::spawn(async move {
if let Err(e) = grpc_client.get_health().await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC ping request failed: {}", e);
let mut delay = 1u64;
// 初始健康检查
{
let client = grpc_client.load();
if let Err(e) = client.get_health().await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC initial health check failed: {}", e);
}
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = grpc_client.get_health().await {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC ping request failed: {}", e);
// 健康检查(使用 load() 无锁读取)
let client = grpc_client.load();
match client.get_health().await {
Ok(_) => {
delay = 1; // 成功,重置延迟
}
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC health check failed: {} - reconnecting in {}s", e, delay);
}
// 等待指数退避时间
tokio::time::sleep(Duration::from_secs(delay)).await;
delay = (delay * 2).min(60);
// 尝试重连
match Self::reconnect_grpc(&endpoint, &auth_token).await {
Ok(new_client) => {
// 使用 swap() 无锁替换客户端
grpc_client.swap(Arc::new(new_client));
delay = 1; // 重置延迟
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC reconnected successfully");
}
}
Err(reconnect_err) => {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor gRPC reconnect failed: {}", reconnect_err);
}
}
}
}
}
}
@@ -249,6 +300,7 @@ impl BlockRazorClient {
http_client,
ping_handle,
stop_ping,
..
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
@@ -307,6 +359,18 @@ impl BlockRazorClient {
Ok(())
}
/// 重新建立 gRPC 连接
async fn reconnect_grpc(endpoint: &str, auth_token: &str) -> Result<BlockRazorGrpcClient> {
let channel = tonic::transport::Channel::from_shared(endpoint.to_string())
.map_err(|e| anyhow::anyhow!("Invalid gRPC endpoint: {}", e))?
.timeout(Duration::from_secs(30))
.connect()
.await
.map_err(|e| anyhow::anyhow!("Failed to reconnect to gRPC endpoint: {}", e))?;
Ok(BlockRazorGrpcClient::new(channel, auth_token.to_string()))
}
async fn send_transaction_impl(
&self,
trade_type: TradeType,
@@ -318,14 +382,19 @@ impl BlockRazorClient {
match &self.backend {
BlockRazorBackend::Grpc {
grpc_client,
mev_protection,
..
} => {
let (content, _signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let signature = grpc_client.send_transaction(
// 使用 load() 无锁获取客户端引用
let client = grpc_client.load();
let signature = client.send_transaction(
content,
"fast".to_string(),
// 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;
@@ -354,14 +423,22 @@ impl BlockRazorClient {
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(&[("auth", auth_token.as_str())])
.query(&query_params)
.header("Content-Type", "text/plain")
.body(content)
.send()
+76 -38
View File
@@ -22,14 +22,15 @@ 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_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
SWQOS_ENDPOINTS_BLOCKRAZOR,
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
@@ -51,11 +52,8 @@ use crate::{
},
};
lazy_static::lazy_static! {
/// Reserved for future per-SWQOS tip account caching (currently unused).
#[allow(dead_code)]
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
}
// Tip 账户:`SwqosClient::get_tip_account()` 在各实现里多为静态常量;同一批多路提交时,
// 在 `trading::core::async_executor::execute_parallel` 内用局部 `tip_cache`(按 client 指针)去重解析。
/// SWQOS provider blacklist configuration
/// Providers added here will be disabled even if configured by user
@@ -66,7 +64,7 @@ pub const SWQOS_BLACKLIST: &[SwqosType] = &[
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
/// BlockRazor 支持 gRPC 和 HTTP。
/// Astralane 和 Node1 支持 QUIC。
/// Node1 支持 QUIC。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum SwqosTransport {
#[default]
@@ -75,6 +73,19 @@ pub enum SwqosTransport {
Quic,
}
/// Astralane 三种提交方式:QUIC TPU、Plain HTTP`/iris`)、Binary HTTP`/irisb` + bincode)。
/// 与全局 [`crate::common::TradeConfig::mev_protection`] 配合:HTTP 加 `mev-protect=true`QUIC 选 `:9000` / `:7000`。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum AstralaneTransport {
/// Binary over HTTP`…/irisb?api-key=…&method=sendTransaction`(与 `AstralaneClient` 当前序列化一致)。
#[default]
Binary,
/// Plain HTTP`…/iris?…`(非 irisb 路径)。
Plain,
/// QUIC`host:7000`MEV 时 `host:9000`)。
Quic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TradeType {
Create,
@@ -199,15 +210,23 @@ pub trait SwqosClientTrait {
}
}
/// 地理区域,用于默认 SWQOS 端点下标(见 `constants::swqos`)。
///
/// 各服务商常量表在**缺独立 PoP**时,于**已公布的端点集合内**按地理距离选最近项;[`SwqosRegion::Default`] 不表示地球上的位置,表中为全局/枢纽回退,不适用地理就近。
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SwqosRegion {
NewYork,
Frankfurt,
Amsterdam,
/// Ireland (EU); Jito publishes `dublin.mainnet.block-engine.jito.wtf`.
Dublin,
SLC,
Tokyo,
/// Southeast Asia (Singapore); not interchangeable with [`SwqosRegion::Tokyo`].
Singapore,
London,
LosAngeles,
/// 非地理区域:未指定区域时的回退,对应表中全局 URL 或枢纽,**不按地理距离选取**。
Default,
}
@@ -230,8 +249,8 @@ pub enum SwqosConfig {
FlashBlock(String, SwqosRegion, Option<String>),
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// Astralane(api_token, region, custom_url, transport). transport=None 表示 Http
Astralane(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`]`/irisb`
Astralane(String, SwqosRegion, Option<String>, Option<AstralaneTransport>),
/// Stellium(api_token, region, custom_url)
Stellium(String, SwqosRegion, Option<String>),
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
@@ -288,7 +307,7 @@ impl SwqosConfig {
SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(),
SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(),
SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(),
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string(),
SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string(),
SwqosType::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(),
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
@@ -303,6 +322,7 @@ impl SwqosConfig {
region: SwqosRegion,
url: Option<String>,
transport: Option<SwqosTransport>,
_mev_protection: bool,
) -> String {
if let Some(custom_url) = url {
return custom_url;
@@ -326,15 +346,7 @@ impl SwqosConfig {
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
}
}
SwqosType::Astralane => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
}
}
_ => Self::get_endpoint(swqos_type, region, url),
_ => Self::get_endpoint(swqos_type, region, None),
}
}
@@ -342,6 +354,7 @@ impl SwqosConfig {
rpc_url: String,
commitment: CommitmentConfig,
swqos_config: SwqosConfig,
mev_protection: bool,
) -> Result<Arc<SwqosClient>> {
match swqos_config {
SwqosConfig::Jito(auth_token, region, url) => {
@@ -398,33 +411,58 @@ impl SwqosConfig {
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);
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);
BlockRazorClient::new_http(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection);
Ok(Arc::new(blockrazor_client))
} else {
// 使用 gRPC 模式(默认或用户明确指定了 gRPC)
let blockrazor_client =
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
BlockRazorClient::new_grpc(rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection).await?;
Ok(Arc::new(blockrazor_client))
}
}
SwqosConfig::Astralane(auth_token, region, url, transport) => {
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
if use_quic {
let quic_endpoint = url.unwrap_or_else(|| {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
});
let astralane_client =
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
.await?;
Ok(Arc::new(astralane_client))
} else {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
let astralane_client =
AstralaneClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(astralane_client))
SwqosConfig::Astralane(auth_token, region, url, mode) => {
let mode = mode.unwrap_or_default();
match mode {
AstralaneTransport::Quic => {
let quic_endpoint = url.unwrap_or_else(|| {
if mev_protection {
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
} else {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
}
});
let astralane_client =
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
.await?;
Ok(Arc::new(astralane_client))
}
AstralaneTransport::Plain => {
let endpoint = url.unwrap_or_else(|| {
SWQOS_ENDPOINTS_ASTRALANE_PLAIN[region as usize].to_string()
});
let astralane_client = AstralaneClient::new(
rpc_url.clone(),
endpoint,
auth_token,
mev_protection,
);
Ok(Arc::new(astralane_client))
}
AstralaneTransport::Binary => {
let endpoint = url.unwrap_or_else(|| {
SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string()
});
let astralane_client = AstralaneClient::new(
rpc_url.clone(),
endpoint,
auth_token,
mev_protection,
);
Ok(Arc::new(astralane_client))
}
}
}
SwqosConfig::Stellium(auth_token, region, url) => {
+10 -10
View File
@@ -51,7 +51,7 @@ pub mod server_client {
}
impl<T> ServerClient<T>
where
T: tonic::client::GrpcService<tonic::body::Body>,
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
@@ -72,13 +72,13 @@ pub mod server_client {
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::Body>,
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::Body>,
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
{
ServerClient::new(InterceptedService::new(inner, interceptor))
@@ -126,7 +126,7 @@ pub mod server_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/serverpb.Server/SendTransaction",
);
@@ -147,7 +147,7 @@ pub mod server_client {
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic_prost::ProstCodec::default();
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static(
"/serverpb.Server/GetHealth",
);
@@ -244,7 +244,7 @@ pub mod server_server {
B: Body + std::marker::Send + 'static,
B::Error: Into<StdError> + std::marker::Send + 'static,
{
type Response = http::Response<tonic::body::Body>;
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
@@ -283,7 +283,7 @@ pub mod server_server {
let inner = self.inner.clone();
let fut = async move {
let method = SendTransactionSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -326,7 +326,7 @@ pub mod server_server {
let inner = self.inner.clone();
let fut = async move {
let method = GetHealthSvc(inner);
let codec = tonic_prost::ProstCodec::default();
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
@@ -344,7 +344,7 @@ pub mod server_server {
_ => {
Box::pin(async move {
let mut response = http::Response::new(
tonic::body::Body::default(),
tonic::body::BoxBody::default(),
);
let headers = response.headers_mut();
headers
+4 -1
View File
@@ -7,7 +7,7 @@ use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::{
common::SolanaRpcClient,
common::{sdk_log, SolanaRpcClient},
swqos::{common::poll_transaction_confirmation, SwqosType, TradeType},
};
use anyhow::Result;
@@ -25,6 +25,7 @@ impl SwqosClientTrait for SolRpcClient {
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let submit_start = Instant::now();
let signature = self
.rpc_client
.send_transaction_with_config(
@@ -39,6 +40,8 @@ impl SwqosClientTrait for SolRpcClient {
)
.await?;
sdk_log::log_swqos_submitted("Default", trade_type, submit_start.elapsed());
let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
+69 -3
View File
@@ -6,9 +6,11 @@ use quinn::{
TransportConfig,
};
use rand::seq::IndexedRandom as _;
use rcgen::{CertificateParams, KeyPair as RcgenKeyPair};
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::signer::Signer;
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use std::time::Instant;
use std::{
net::{SocketAddr, ToSocketAddrs as _},
@@ -25,6 +27,65 @@ use crate::{
swqos::{SwqosType, TradeType},
};
// Skip server verification implementation
#[derive(Debug)]
struct SkipServerVerification;
impl SkipServerVerification {
fn new() -> Arc<Self> {
Arc::new(Self)
}
}
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
fn verify_server_cert(
&self,
_end_entity: &rustls::pki_types::CertificateDer<'_>,
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
_server_name: &rustls::pki_types::ServerName<'_>,
_ocsp_response: &[u8],
_now: rustls::pki_types::UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &rustls::pki_types::CertificateDer<'_>,
_dss: &rustls::DigitallySignedStruct,
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
}
}
/// TLS 客户端证书:ECDSA P-256 + CN=钱包公钥(与 Speedlanding / Astralane QUIC 策略一致)。
fn generate_client_tls_credentials(keypair: &Keypair) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
let tls_key = RcgenKeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
let mut cert_params = CertificateParams::new(vec![])?;
cert_params.distinguished_name.push(
rcgen::DnType::CommonName,
rcgen::DnValue::Utf8String(keypair.pubkey().to_string()),
);
let cert = cert_params.self_signed(&tls_key)?;
let cert_der = CertificateDer::from(cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(tls_key.serialize_der()));
Ok((cert_der, key_der))
}
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const SOYAS_SERVER: &str = "soyas-landing";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
@@ -42,8 +103,13 @@ pub struct SoyasClient {
impl SoyasClient {
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
let keypair = Keypair::from_base58_string(&api_key);
let (cert, key) = new_dummy_x509_certificate(&keypair);
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
anyhow::anyhow!(
"Soyas api_token 无法解析为 Solana keypair base58QUIC mTLS 用): {}",
e
)
})?;
let (cert, key) = generate_client_tls_credentials(&keypair)?;
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
+59 -51
View File
@@ -6,6 +6,7 @@ use quinn::{
TransportConfig,
};
use rand::seq::IndexedRandom as _;
use solana_sdk::signer::Signer;
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use std::time::Instant;
@@ -27,8 +28,8 @@ use crate::{
};
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
/// Fallback SNI when endpoint is IP or cannot extract host (keeps legacy behavior).
const SPEED_SERVER_FALLBACK: &str = "speed-landing";
/// QUIC TLS SNI:与 Speedlanding 官方客户端一致,固定为 `speed-landing`(勿用 PoP 主机名,否则易握手失败)。
const SPEED_SERVER: &str = "speed-landing";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -39,33 +40,20 @@ pub struct SpeedlandingClient {
endpoint: Endpoint,
client_config: ClientConfig,
addr: SocketAddr,
/// TLS SNI: host from endpoint URL so server presents the right cert (e.g. nyc.speedlanding.trade).
server_name: String,
connection: ArcSwap<Connection>,
reconnect: Mutex<()>,
}
impl SpeedlandingClient {
/// Extract TLS SNI (host) from endpoint URL. Uses fallback "speed-landing" for IP or when host cannot be determined.
fn server_name_from_endpoint(endpoint: &str) -> String {
let without_scheme = endpoint
.strip_prefix("https://")
.or_else(|| endpoint.strip_prefix("http://"))
.unwrap_or(endpoint);
let host = without_scheme.split(':').next().unwrap_or("").trim();
if host.is_empty() {
return SPEED_SERVER_FALLBACK.to_string();
}
if !host.chars().any(|c| c.is_ascii_alphabetic()) {
return SPEED_SERVER_FALLBACK.to_string();
}
host.to_string()
}
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
let server_name = Self::server_name_from_endpoint(&endpoint_string);
let keypair = Keypair::from_base58_string(&api_key);
// Speedlanding QUIC:与官方一致使用 `solana_tls_utils::new_dummy_x509_certificate`Ed25519 dummy cert+ SNI `speed-landing`。
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
anyhow::anyhow!(
"Speedlanding api_token 无法解析为 Solana keypair base58(用于 mTLS);请确认粘贴的是机器人提供的密钥而非其它字符串: {}",
e
)
})?;
let (cert, key) = new_dummy_x509_certificate(&keypair);
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
@@ -89,18 +77,23 @@ impl SpeedlandingClient {
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
let connecting = endpoint.connect(addr, &server_name)?;
let connecting = endpoint.connect(addr, SPEED_SERVER)?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Speedlanding QUIC connect timeout")?
.context("Speedlanding QUIC handshake failed")?;
.with_context(|| {
format!(
"Speedlanding QUIC handshake failed(请确认:1) 机器人登记的身份与钱包公钥 {} 一致 2) 本机 UDP 可访问 {} 3) region 与 PoP 匹配)",
keypair.pubkey(),
endpoint_string
)
})?;
Ok(Self {
rpc_client: Arc::new(rpc_client),
endpoint,
client_config,
addr,
server_name,
connection: ArcSwap::from_pointee(connection),
reconnect: Mutex::new(()),
})
@@ -122,12 +115,17 @@ impl SpeedlandingClient {
let connecting = self.endpoint.connect_with(
self.client_config.clone(),
self.addr,
self.server_name.as_str(),
SPEED_SERVER,
)?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Speedlanding QUIC reconnect timeout")?
.context("Speedlanding QUIC re-handshake failed")?;
.with_context(|| {
format!(
"Speedlanding QUIC re-handshake failed(对端 {} SNI {}",
self.addr, SPEED_SERVER
)
})?;
self.connection.store(Arc::new(connection));
return Ok(self.connection.load_full());
}
@@ -160,51 +158,61 @@ impl SwqosClientTrait for SpeedlandingClient {
Ok(Err(_)) | Err(_) => true,
};
if need_retry {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [Speedlanding] {} submission failed after {:?}, reconnecting", trade_type, start_time.elapsed());
}
eprintln!(
" [Speedlanding] {} QUIC 首次发送失败 {:?},正在重试",
trade_type,
start_time.elapsed()
);
let connection = self.ensure_connected().await?;
send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
}
match send_result.context("Speedlanding QUIC send timeout") {
Ok(Ok(())) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
}
// 提交结果与「详细耗时/SDK 开关」无关,便于确认当前通道确实在执行
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
}
Ok(Err(e)) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), &e);
}
crate::common::sdk_log::log_swqos_submission_failed(
"Speedlanding",
trade_type,
start_time.elapsed(),
&e,
);
return Err(e.into());
}
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), "timeout");
}
crate::common::sdk_log::log_swqos_submission_failed(
"Speedlanding",
trade_type,
start_time.elapsed(),
"timeout",
);
return Err(e.into());
}
}
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(
" [{:width$}] {} confirmation failed: {:?}",
"Speedlanding",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
println!(" signature: {:?}", signature);
crate::common::sdk_log::log_swqos_submission_failed(
"Speedlanding",
trade_type,
start_time.elapsed(),
&e,
);
return Err(e);
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [{:width$}] {} confirmed: {:?}", "Speedlanding", trade_type, start_time.elapsed(), width = crate::common::sdk_log::SWQOS_LABEL_WIDTH);
println!(
" [{:width$}] {} confirmed: {:?}",
"Speedlanding",
trade_type,
start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
);
}
Ok(())
}
+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,
+45 -13
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,
};
@@ -23,7 +23,11 @@ use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use parking_lot::Mutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::{str::FromStr, sync::Arc, time::Instant};
use std::{
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::Notify;
use fnv::FnvHasher;
@@ -402,14 +406,31 @@ impl ResultCollector {
timeout_secs: u64,
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
let start = Instant::now();
let timeout = std::time::Duration::from_secs(timeout_secs);
let poll_interval = std::time::Duration::from_millis(2);
let primary = Duration::from_secs(timeout_secs);
let poll_interval = Duration::from_millis(2);
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
if start.elapsed() > timeout {
if start.elapsed() > primary {
break;
}
tokio::time::sleep(poll_interval).await;
}
// 「不等待链上确认」仍会等各 SWQOS 的 HTTP 回包;主循环在收齐或触达 `timeout_secs` 后结束。
// 若主窗口到时仍有未回包通道,晚到的 TaskResult 若立刻 drain 会丢签名——仅在该路径上拉长 grace。
let all_submitted =
self.completed_count.load(Ordering::Acquire) >= self.total_tasks;
if all_submitted {
// 全数已登记:仅留极短 settle,避免极端情况下最后一笔与计数可见性竞态。
tokio::time::sleep(Duration::from_millis(35)).await;
} else {
tokio::time::sleep(Duration::from_millis(600)).await;
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
if start.elapsed() > primary + Duration::from_secs(6) {
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
tokio::time::sleep(Duration::from_millis(120)).await;
}
self.get_first()
}
}
@@ -492,7 +513,11 @@ pub async fn execute_parallel(
}
// Task preparation completed: one shared context (clone once per batch), then minimal per-task data.
let collector = Arc::new(ResultCollector::new(task_configs.len()));
let channel_count = task_configs.len().max(1);
let collector = Arc::new(ResultCollector::new(channel_count));
// 上限最多 5s:单路卡死时才会等满;若所有通道在窗口内回完,主循环会提前结束(不睡满秒数)。
let submit_timeout_secs: u64 =
(3u64 + (channel_count.min(16) as u64).div_ceil(2).min(6)).clamp(3, 5);
let shared = Arc::new(SwqosSharedContext {
payer,
instructions,
@@ -562,13 +587,20 @@ pub async fn execute_parallel(
// All jobs enqueued (no spawn on hot path)
if !wait_transaction_confirmed {
const SUBMIT_TIMEOUT_SECS: u64 = 30;
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
false,
vec![],
Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)),
vec![],
));
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
let ret = collector
.wait_for_all_submitted(submit_timeout_secs)
.await
.unwrap_or((
false,
vec![],
Some(anyhow!(
"No SWQOS result within grace window (primary {}s)",
submit_timeout_secs
)),
vec![],
));
let (success, signatures, last_error, submit_timings) = ret;
return Ok((success, signatures, last_error, submit_timings));
}
+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)]
+85 -24
View File
@@ -2,9 +2,8 @@ use crate::common::bonding_curve::BondingCurveAccount;
use crate::common::nonce_cache::DurableNonceInfo;
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::{GasFeeStrategy, SolanaRpcClient};
use crate::constants::TOKEN_PROGRAM;
use core_affinity::CoreId;
use crate::instruction::utils::pumpfun::global_constants::MAYHEM_FEE_RECIPIENT;
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 +17,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 +118,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 +148,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 +168,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 +181,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 +221,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 +238,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 +290,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 +439,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 +463,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 +479,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 {
@@ -805,14 +853,27 @@ impl MeteoraDammV2Params {
) -> Result<Self, anyhow::Error> {
let pool_data =
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
let mint_accounts = rpc
.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint])
.await?;
let token_a_program = mint_accounts
.get(0)
.and_then(|a| a.as_ref())
.map(|a| a.owner)
.ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?;
let token_b_program = mint_accounts
.get(1)
.and_then(|a| a.as_ref())
.map(|a| a.owner)
.ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?;
Ok(Self {
pool: *pool_address,
token_a_vault: pool_data.token_a_vault,
token_b_vault: pool_data.token_b_vault,
token_a_mint: pool_data.token_a_mint,
token_b_mint: pool_data.token_b_mint,
token_a_program: TOKEN_PROGRAM,
token_b_program: TOKEN_PROGRAM,
token_a_program,
token_b_program,
})
}
}
+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 {