Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55c13e2f25 | ||
|
|
fda211ea87 | ||
|
|
85d2c602f3 | ||
|
|
02d939b3cf | ||
|
|
e8ec9103ab | ||
|
|
0fe54f0e94 | ||
|
|
7ac07247a3 | ||
|
|
06ed710869 | ||
|
|
d711346c55 | ||
|
|
06ef2fed84 |
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "sol-trade-sdk"
|
name = "sol-trade-sdk"
|
||||||
version = "4.0.3"
|
version = "4.0.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = [
|
authors = [
|
||||||
"William <byteblock6@gmail.com>",
|
"William <byteblock6@gmail.com>",
|
||||||
@@ -37,7 +37,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["rlib"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
@@ -109,7 +109,8 @@ parking_lot = "0.12"
|
|||||||
arc-swap = "1.7"
|
arc-swap = "1.7"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
tonic-prost = "0.14.2"
|
tonic-prost = "0.14.2"
|
||||||
quinn = { version = "0.11", default-features = false, features = ["rustls"] }
|
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime found,QUIC(Speedlanding/Soyas)无法初始化
|
||||||
|
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
|
||||||
rcgen = "0.13"
|
rcgen = "0.13"
|
||||||
uuid = "1.11"
|
uuid = "1.11"
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
- [⚡ Trading Parameters](#-trading-parameters)
|
- [⚡ Trading Parameters](#-trading-parameters)
|
||||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||||
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||||
- [Astralane QUIC (Low-Latency)](#astralane-quic-low-latency)
|
- [Astralane (Binary / Plain / QUIC)](#astralane-binary--plain--quic)
|
||||||
- [🔧 Middleware System](#-middleware-system)
|
- [🔧 Middleware System](#-middleware-system)
|
||||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||||
- [🔍 Nonce Cache](#-nonce-cache)
|
- [🔍 Nonce Cache](#-nonce-cache)
|
||||||
@@ -131,13 +131,13 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
|||||||
SwqosConfig::Default(rpc_url.clone()),
|
SwqosConfig::Default(rpc_url.clone()),
|
||||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
// Astralane: HTTP (4th param None) or QUIC (Some(SwqosTransport::Quic)); same API key
|
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
|
||||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
|
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
|
||||||
SwqosConfig::Astralane(
|
SwqosConfig::Astralane(
|
||||||
"your_astralane_api_key".to_string(),
|
"your_astralane_api_key".to_string(),
|
||||||
SwqosRegion::Frankfurt,
|
SwqosRegion::Frankfurt,
|
||||||
None,
|
None,
|
||||||
Some(SwqosTransport::Quic),
|
Some(AstralaneTransport::Quic),
|
||||||
), // QUIC
|
), // QUIC
|
||||||
];
|
];
|
||||||
// Create TradeConfig instance
|
// Create TradeConfig instance
|
||||||
@@ -147,7 +147,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
|||||||
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
|
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
|
||||||
// .check_min_tip(false) // default: false - filter SWQOS below min tip
|
// .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
|
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
|
||||||
// .mev_protection(false) // default: false - MEV protection (Astralane port 9000 / BlockRazor sandwichMitigation)
|
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// Create TradingClient
|
// Create TradingClient
|
||||||
@@ -280,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.
|
When using multiple MEV services, you need to use `Durable Nonce`. You need to use the `fetch_nonce_info` function to get the latest `nonce` value, and use it as the `durable_nonce` when trading.
|
||||||
|
|
||||||
#### Astralane QUIC (Low-Latency)
|
#### Astralane (Binary / Plain HTTP / QUIC)
|
||||||
|
|
||||||
Astralane supports both HTTP and **QUIC** transport. QUIC reduces connection overhead and can lower submission latency. To use the QUIC channel, pass `Some(SwqosTransport::Quic)` as the fourth parameter of `SwqosConfig::Astralane`. Astralane’s QUIC service uses a **single endpoint** (no per-region endpoints); the SDK ignores the `region` (and optional custom URL) when QUIC is selected. You can pass the same region as your other SWQoS configs for consistency.
|
Astralane supports **Binary** HTTP (`/irisb`), **Plain** HTTP (`/iris`), and **QUIC** (`host:7000`, or `:9000` when global `mev_protection` is true). Pass `Some(AstralaneTransport::Plain)`, `Some(AstralaneTransport::Quic)`, or use `None` / omit for **Binary** (default). Global `mev_protection` adds `mev-protect=true` on HTTP or selects QUIC port 9000.
|
||||||
|
|
||||||
```rust
|
```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![
|
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||||
SwqosConfig::Default(rpc_url.clone()),
|
SwqosConfig::Default(rpc_url.clone()),
|
||||||
SwqosConfig::Astralane(
|
SwqosConfig::Astralane(
|
||||||
"your_astralane_api_key".to_string(),
|
"your_astralane_api_key".to_string(),
|
||||||
SwqosRegion::Frankfurt, // same as other services; ignored for QUIC
|
SwqosRegion::Frankfurt,
|
||||||
None,
|
None,
|
||||||
Some(SwqosTransport::Quic),
|
Some(AstralaneTransport::Quic),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
// Then create TradeConfig / TradingClient as usual with swqos_configs
|
// Then create TradeConfig / TradingClient as usual with swqos_configs
|
||||||
```
|
```
|
||||||
|
|
||||||
- **HTTP** (default): use `None` or `Some(SwqosTransport::Http)`; region and optional custom URL apply.
|
- **Binary** (default): `None` or `Some(AstralaneTransport::Binary)` — `/irisb`, bincode body.
|
||||||
- **QUIC**: use `Some(SwqosTransport::Quic)`; the SDK uses a single QUIC endpoint and ignores region. Same API key as HTTP.
|
- **Plain**: `Some(AstralaneTransport::Plain)` — `/iris`.
|
||||||
|
- **QUIC**: `Some(AstralaneTransport::Quic)` — regional `host:7000` / `:9000` (MEV); same API key.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -364,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
|
- **FlashBlock**: High-speed transaction execution with API key authentication
|
||||||
- **BlockRazor**: 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
|
- **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
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -48,7 +48,7 @@
|
|||||||
- [⚡ 交易参数](#-交易参数)
|
- [⚡ 交易参数](#-交易参数)
|
||||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||||
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||||
- [Astralane QUIC(低延迟)](#astralane-quic低延迟)
|
- [Astralane(Binary / Plain / QUIC)](#astralanebinary--plain--quic)
|
||||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||||
- [🔍 地址查找表](#-地址查找表)
|
- [🔍 地址查找表](#-地址查找表)
|
||||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||||
@@ -131,13 +131,13 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
|||||||
SwqosConfig::Default(rpc_url.clone()),
|
SwqosConfig::Default(rpc_url.clone()),
|
||||||
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Jito("your uuid".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None),
|
||||||
// Astralane:第4个参数 None 为 HTTP,Some(SwqosTransport::Quic) 为 QUIC;同一 API key
|
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain(/iris)或 Quic
|
||||||
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP
|
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
|
||||||
SwqosConfig::Astralane(
|
SwqosConfig::Astralane(
|
||||||
"your_astralane_api_key".to_string(),
|
"your_astralane_api_key".to_string(),
|
||||||
SwqosRegion::Frankfurt,
|
SwqosRegion::Frankfurt,
|
||||||
None,
|
None,
|
||||||
Some(SwqosTransport::Quic),
|
Some(AstralaneTransport::Quic),
|
||||||
), // QUIC
|
), // QUIC
|
||||||
];
|
];
|
||||||
// 创建 TradeConfig 实例
|
// 创建 TradeConfig 实例
|
||||||
@@ -147,7 +147,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
|||||||
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
|
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
|
||||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||||
// .mev_protection(false) // 默认: false - MEV 保护(Astralane 端口 9000 / BlockRazor sandwichMitigation)
|
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
// 创建 TradingClient
|
// 创建 TradingClient
|
||||||
@@ -279,28 +279,28 @@ let bloxroute_config = SwqosConfig::Bloxroute(
|
|||||||
|
|
||||||
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
当使用多个MEV服务时,需要使用`Durable Nonce`。你需要使用`fetch_nonce_info`函数获取最新的`nonce`值,并在交易的时候将`durable_nonce`填入交易参数。
|
||||||
|
|
||||||
#### Astralane QUIC(低延迟)
|
#### Astralane(Binary / Plain / QUIC)
|
||||||
|
|
||||||
Astralane 支持 **HTTP** 与 **QUIC** 两种传输方式。QUIC 可减少连接开销,降低提交延迟。使用 QUIC 时,将 `SwqosConfig::Astralane` 的第四个参数设为 `Some(SwqosTransport::Quic)`。Astralane 的 QUIC 服务使用**单一端点**(无分区域端点),选 QUIC 时 SDK 会忽略 `region` 与可选自定义 URL;为与其他 SWQoS 配置一致,可传入相同 region。
|
Astralane 支持 **Binary** HTTP(`/irisb`)、**Plain** HTTP(`/iris`)与 **QUIC**(`host:7000`,全局 `mev_protection` 为 true 时用 `:9000`)。第四个参数:`Some(AstralaneTransport::Plain)`、`Some(AstralaneTransport::Quic)`,或 `None` 表示 **Binary**(默认)。全局 `mev_protection` 会在 HTTP 上附加 `mev-protect=true`,或为 QUIC 选择 9000 端口。
|
||||||
|
|
||||||
```rust
|
```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![
|
let swqos_configs: Vec<SwqosConfig> = vec![
|
||||||
SwqosConfig::Default(rpc_url.clone()),
|
SwqosConfig::Default(rpc_url.clone()),
|
||||||
SwqosConfig::Astralane(
|
SwqosConfig::Astralane(
|
||||||
"your_astralane_api_key".to_string(),
|
"your_astralane_api_key".to_string(),
|
||||||
SwqosRegion::Frankfurt, // 与其他服务一致即可;QUIC 时会被忽略
|
SwqosRegion::Frankfurt,
|
||||||
None,
|
None,
|
||||||
Some(SwqosTransport::Quic),
|
Some(AstralaneTransport::Quic),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
// 然后照常使用 swqos_configs 创建 TradeConfig / TradingClient
|
// 然后照常使用 swqos_configs 创建 TradeConfig / TradingClient
|
||||||
```
|
```
|
||||||
|
|
||||||
- **HTTP**(默认):第四个参数为 `None` 或 `Some(SwqosTransport::Http)`;region 与可选自定义 URL 生效。
|
- **Binary**(默认):`None` 或 `Some(AstralaneTransport::Binary)` — `/irisb`,bincode 正文。
|
||||||
- **QUIC**:第四个参数为 `Some(SwqosTransport::Quic)`;SDK 使用单一 QUIC 端点并忽略 region。与 HTTP 使用同一 API key。
|
- **Plain**:`Some(AstralaneTransport::Plain)` — `/iris`。
|
||||||
|
- **QUIC**:`Some(AstralaneTransport::Quic)` — 按区域的 `host:7000` / `:9000`(MEV);同一 API key。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟)
|
|||||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
||||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||||
- **Node1**: 高速交易执行,支持 API 密钥认证
|
- **Node1**: 高速交易执行,支持 API 密钥认证
|
||||||
- **Astralane**: 区块链网络加速(支持 HTTP 与 QUIC,见上方 [Astralane QUIC](#astralane-quic低延迟))
|
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC,见 [Astralane](#astralanebinary--plain--quic))
|
||||||
|
|
||||||
## 📁 项目结构
|
## 📁 项目结构
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any
|
|||||||
trade_info.base_token_program,
|
trade_info.base_token_program,
|
||||||
trade_info.quote_token_program,
|
trade_info.quote_token_program,
|
||||||
trade_info.protocol_fee_recipient,
|
trade_info.protocol_fee_recipient,
|
||||||
|
pool_data.coin_creator,
|
||||||
pool_data.is_cashback_coin,
|
pool_data.is_cashback_coin,
|
||||||
|
trade_info.cashback_fee_basis_points,
|
||||||
);
|
);
|
||||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||||
@@ -191,7 +193,9 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A
|
|||||||
trade_info.base_token_program,
|
trade_info.base_token_program,
|
||||||
trade_info.quote_token_program,
|
trade_info.quote_token_program,
|
||||||
trade_info.protocol_fee_recipient,
|
trade_info.protocol_fee_recipient,
|
||||||
|
pool_data.coin_creator,
|
||||||
pool_data.is_cashback_coin,
|
pool_data.is_cashback_coin,
|
||||||
|
trade_info.cashback_fee_basis_points,
|
||||||
);
|
);
|
||||||
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|
||||||
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
|| trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
use sol_trade_sdk::{
|
use sol_trade_sdk::{
|
||||||
common::{AnyResult, InfrastructureConfig, TradeConfig},
|
common::{AnyResult, InfrastructureConfig, TradeConfig},
|
||||||
swqos::{SwqosConfig, SwqosRegion},
|
swqos::{SwqosConfig, SwqosRegion},
|
||||||
SwqosTransport, TradingClient, TradingInfrastructure,
|
AstralaneTransport, TradingClient, TradingInfrastructure,
|
||||||
};
|
};
|
||||||
use solana_commitment_config::CommitmentConfig;
|
use solana_commitment_config::CommitmentConfig;
|
||||||
use solana_sdk::signature::Keypair;
|
use solana_sdk::signature::Keypair;
|
||||||
@@ -52,8 +52,8 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
|||||||
"your_api_token".to_string(),
|
"your_api_token".to_string(),
|
||||||
SwqosRegion::Frankfurt,
|
SwqosRegion::Frankfurt,
|
||||||
None,
|
None,
|
||||||
Some(SwqosTransport::Quic),
|
Some(AstralaneTransport::Quic),
|
||||||
), // QUIC; use None for HTTP
|
), // QUIC;None / Some(Binary) / Some(Plain) 为 HTTP
|
||||||
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
|
// 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)),
|
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
|
||||||
];
|
];
|
||||||
|
|||||||
+1222
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -12,7 +12,7 @@ pub struct InfrastructureConfig {
|
|||||||
/// 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.
|
/// 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,
|
pub swqos_cores_from_end: bool,
|
||||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||||
/// (Astralane QUIC port 9000, BlockRazor revert_protection) will use their MEV-protected
|
/// (Astralane QUIC `:9000` or HTTP `mev-protect=true`, BlockRazor) use MEV-protected
|
||||||
/// endpoints/modes. Default false.
|
/// endpoints/modes. Default false.
|
||||||
pub mev_protection: bool,
|
pub mev_protection: bool,
|
||||||
}
|
}
|
||||||
@@ -92,8 +92,8 @@ pub struct TradeConfig {
|
|||||||
/// 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.
|
/// 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,
|
pub swqos_cores_from_end: bool,
|
||||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||||
/// (Astralane QUIC port 9000, BlockRazor sandwichMitigation mode) will use their
|
/// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
|
||||||
/// MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||||
pub mev_protection: bool,
|
pub mev_protection: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ impl TradeConfigBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Enable global MEV protection. When `true`:
|
/// Enable global MEV protection. When `true`:
|
||||||
/// - **Astralane QUIC** uses port `9000` (MEV-protected endpoint)
|
/// - **Astralane QUIC** uses port `9000`; **Astralane HTTP** adds `mev-protect=true`
|
||||||
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
|
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
|
||||||
///
|
///
|
||||||
/// May reduce landing speed. Default: `false`.
|
/// May reduce landing speed. Default: `false`.
|
||||||
|
|||||||
+214
-92
@@ -171,215 +171,267 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
|||||||
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
||||||
];
|
];
|
||||||
|
|
||||||
// NewYork,
|
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||||
// Frankfurt,
|
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||||
// Amsterdam,
|
//
|
||||||
// SLC,
|
// **地理就近(用户语义)**:当某枚举区域没有该服务商**独立公布**的 PoP 时,在**该服务商已出现的端点集合内**,按真实地理位置选**大圆距离最近**的一项作为填充;行尾注释说明依据。
|
||||||
// Tokyo,
|
// **例外**:`SwqosRegion::Default`(下标 9)不表示地球上的点,表中为全局 URL 或文档默认枢纽,**不适用**地理就近,仅表示「未指定区域时的回退」。
|
||||||
// London,
|
// 若某区域仅有一种「大区」级入口(例如全美只有一个美东 PoP),则地理上非最优但只能复用,注释会标明「受服务商可用区限制」。
|
||||||
// LosAngeles,
|
|
||||||
// Default,
|
|
||||||
|
|
||||||
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://ny.mainnet.block-engine.jito.wtf",
|
||||||
"https://frankfurt.mainnet.block-engine.jito.wtf",
|
"https://frankfurt.mainnet.block-engine.jito.wtf",
|
||||||
"https://amsterdam.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://slc.mainnet.block-engine.jito.wtf",
|
||||||
"https://tokyo.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://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",
|
"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://ny.nextblock.io",
|
||||||
"http://frankfurt.nextblock.io",
|
"http://fra.nextblock.io",
|
||||||
"http://amsterdam.nextblock.io",
|
"http://ams.nextblock.io",
|
||||||
|
"http://dublin.nextblock.io",
|
||||||
"http://slc.nextblock.io",
|
"http://slc.nextblock.io",
|
||||||
"http://tokyo.nextblock.io",
|
"http://tokyo.nextblock.io",
|
||||||
|
"http://sgp.nextblock.io",
|
||||||
"http://london.nextblock.io",
|
"http://london.nextblock.io",
|
||||||
"http://singapore.nextblock.io",
|
"http://slc.nextblock.io",
|
||||||
"http://frankfurt.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://ny.0slot.trade",
|
||||||
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
"http://de2.0slot.trade", // Use de2 for TSW, and de1 for OVH
|
||||||
"http://ams.0slot.trade",
|
"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://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://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] = [
|
/// Nozomi Direct regions: ewr1, fra2, ams1, lon1, lax1, tyo1, sgp1, …
|
||||||
"http://ewr1.nozomi.temporal.xyz",
|
pub const SWQOS_ENDPOINTS_TEMPORAL: [&str; 10] = [
|
||||||
|
"http://ewr1.nozomi.temporal.xyz", // NewYork → Newark
|
||||||
"http://fra2.nozomi.temporal.xyz",
|
"http://fra2.nozomi.temporal.xyz",
|
||||||
"http://ams1.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://tyo1.nozomi.temporal.xyz",
|
||||||
"http://sgp1.nozomi.temporal.xyz",
|
"http://sgp1.nozomi.temporal.xyz",
|
||||||
"http://pit1.nozomi.temporal.xyz",
|
"http://lon1.nozomi.temporal.xyz",
|
||||||
"http://fra2.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://ny.solana.dex.blxrbdn.com",
|
||||||
"https://germany.solana.dex.blxrbdn.com",
|
"https://germany.solana.dex.blxrbdn.com",
|
||||||
"https://amsterdam.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",
|
||||||
|
"https://tokyo.solana.dex.blxrbdn.com", // SG: 文档无 SGP 区域;已公布 APAC 仅 Tokyo,为距 SG 最近选项
|
||||||
"https://uk.solana.dex.blxrbdn.com",
|
"https://uk.solana.dex.blxrbdn.com",
|
||||||
"https://la.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://ny.node1.me",
|
||||||
"http://fra.node1.me",
|
"http://fra.node1.me",
|
||||||
"http://ams.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",
|
||||||
|
"http://tk.node1.me", // SG: 已公布 APAC 仅 tk;地理上为表中离 SG 最近
|
||||||
"http://lon.node1.me",
|
"http://lon.node1.me",
|
||||||
"http://ny.node1.me",
|
"http://ny.node1.me", // LosAngeles: 同上,美国仅 ny 入口
|
||||||
"http://fra.node1.me",
|
"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.
|
/// 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",
|
"ny.node1.me:16666",
|
||||||
"fra.node1.me:16666",
|
"fra.node1.me:16666",
|
||||||
"ams.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",
|
"tk.node1.me:16666",
|
||||||
"lon.node1.me:16666",
|
"lon.node1.me:16666",
|
||||||
"ny.node1.me:16666", // LA → ny
|
"ny.node1.me:16666",
|
||||||
"ny.node1.me:16666", // Default → ny
|
"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://ny.flashblock.trade",
|
||||||
"http://fra.flashblock.trade",
|
"http://fra.flashblock.trade",
|
||||||
"http://ams.flashblock.trade",
|
"http://ams.flashblock.trade",
|
||||||
|
"http://london.flashblock.trade", // Dublin: no IE host; UK nearest
|
||||||
"http://slc.flashblock.trade",
|
"http://slc.flashblock.trade",
|
||||||
|
"http://tokyo.flashblock.trade",
|
||||||
"http://singapore.flashblock.trade",
|
"http://singapore.flashblock.trade",
|
||||||
"http://london.flashblock.trade",
|
"http://london.flashblock.trade",
|
||||||
"http://ny.flashblock.trade",
|
"http://slc.flashblock.trade",
|
||||||
"http://ny.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.
|
/// BlockRazor Send Transaction v2: plain-text Base64 body, auth in URI, Content-Type: text/plain. Keep-alive: POST /v2/health.
|
||||||
/// 若 HTTP 返回 500,可尝试 HTTPS:https://<region>.solana.blockrazor.io/v2/sendTransaction(Frankfurt/NewYork/Tokyo),通过 custom_url 覆盖。
|
/// 若 HTTP 返回 500,可尝试 HTTPS:https://<region>.solana.blockrazor.io/v2/sendTransaction(Frankfurt/NewYork/Tokyo),通过 custom_url 覆盖。
|
||||||
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 10] = [
|
||||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://amsterdam.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",
|
||||||
|
"http://tokyo.solana.blockrazor.xyz:443/v2/sendTransaction", // SG: 已公布 APAC 仅 Tokyo,为距 SG 最近
|
||||||
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
"http://london.solana.blockrazor.xyz:443/v2/sendTransaction",
|
||||||
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction",
|
"http://newyork.solana.blockrazor.xyz:443/v2/sendTransaction", // LosAngeles: 无美西入口;美国仅 NY
|
||||||
"http://frankfurt.solana.blockrazor.xyz:443/v2/sendTransaction",
|
"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.
|
/// 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://newyork.solana-grpc.blockrazor.xyz:80",
|
||||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
||||||
"http://amsterdam.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://tokyo.solana-grpc.blockrazor.xyz:80",
|
||||||
"http://london.solana-grpc.blockrazor.xyz:80",
|
"http://london.solana-grpc.blockrazor.xyz:80",
|
||||||
"http://newyork.solana-grpc.blockrazor.xyz:80",
|
"http://newyork.solana-grpc.blockrazor.xyz:80", // LosAngeles: 与 HTTP 一致
|
||||||
"http://frankfurt.solana-grpc.blockrazor.xyz:80",
|
"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 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://ny.gateway.astralane.io/irisb",
|
||||||
"http://fr.gateway.astralane.io/irisb",
|
"http://fr.gateway.astralane.io/irisb",
|
||||||
"http://ams.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://jp.gateway.astralane.io/irisb",
|
||||||
"http://ny.gateway.astralane.io/irisb",
|
"http://sg.gateway.astralane.io/irisb",
|
||||||
"http://lax.gateway.astralane.io/irisb",
|
"http://ams.gateway.astralane.io/irisb", // London: 同 Plain
|
||||||
"http://lim.gateway.astralane.io/irisb",
|
"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.
|
/// Astralane QUIC endpoints (port 7000). Region order matches [`SwqosRegion`].
|
||||||
/// See: https://github.com/Astralane/astralane-quic-client. We use fr, ams, la, ny, lim, sg only (avoid ams2/fr2 for lower latency).
|
/// See: https://github.com/Astralane/astralane-quic-client.
|
||||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC: [&str; 10] = [
|
||||||
"ny.gateway.astralane.io:7000", // NewYork
|
"ny.gateway.astralane.io:7000",
|
||||||
"fr.gateway.astralane.io:7000", // Frankfurt
|
"fr.gateway.astralane.io:7000",
|
||||||
"ams.gateway.astralane.io:7000", // Amsterdam
|
"ams.gateway.astralane.io:7000",
|
||||||
"lim.gateway.astralane.io:7000", // SLC (no slc, use lim)
|
"ams.gateway.astralane.io:7000", // Dublin: 同 HTTP
|
||||||
"sg.gateway.astralane.io:7000", // Tokyo (Asia)
|
"la.gateway.astralane.io:7000", // SLC: 美西 la 为最近已公布美区入口
|
||||||
"ams.gateway.astralane.io:7000", // London (Europe, avoid ams2)
|
"jp.gateway.astralane.io:7000",
|
||||||
"la.gateway.astralane.io:7000", // LosAngeles
|
"sg.gateway.astralane.io:7000",
|
||||||
"lim.gateway.astralane.io:7000", // Default
|
"ams.gateway.astralane.io:7000", // London: 同 HTTP
|
||||||
|
"la.gateway.astralane.io:7000",
|
||||||
|
"lim.gateway.astralane.io:7000", // Default: 非地理区域;全局边缘
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
|
/// Astralane QUIC MEV-protected endpoints (port 9000). Same region order as SWQOS_ENDPOINTS_ASTRALANE_QUIC.
|
||||||
/// Use these when mev_protection=true to route through Astralane's MEV-protected path.
|
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 10] = [
|
||||||
pub const SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV: [&str; 8] = [
|
"ny.gateway.astralane.io:9000",
|
||||||
"ny.gateway.astralane.io:9000", // NewYork
|
"fr.gateway.astralane.io:9000",
|
||||||
"fr.gateway.astralane.io:9000", // Frankfurt
|
"ams.gateway.astralane.io:9000",
|
||||||
"ams.gateway.astralane.io:9000", // Amsterdam
|
"ams.gateway.astralane.io:9000",
|
||||||
"lim.gateway.astralane.io:9000", // SLC (no slc, use lim)
|
"la.gateway.astralane.io:9000",
|
||||||
"sg.gateway.astralane.io:9000", // Tokyo (Asia)
|
"jp.gateway.astralane.io:9000",
|
||||||
"ams.gateway.astralane.io:9000", // London (Europe)
|
"sg.gateway.astralane.io:9000",
|
||||||
"la.gateway.astralane.io:9000", // LosAngeles
|
"ams.gateway.astralane.io:9000",
|
||||||
"lim.gateway.astralane.io:9000", // Default
|
"la.gateway.astralane.io:9000",
|
||||||
|
"lim.gateway.astralane.io:9000",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [
|
pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 10] = [
|
||||||
"http://ewr1.flashrpc.com",
|
"http://ewr1.flashrpc.com",
|
||||||
"http://fra1.flashrpc.com",
|
"http://fra1.flashrpc.com",
|
||||||
"http://ams1.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",
|
||||||
|
"http://tyo1.flashrpc.com", // SG: 表中无 SGP;APAC 仅 tyo,为距 SG 最近
|
||||||
"http://lhr1.flashrpc.com",
|
"http://lhr1.flashrpc.com",
|
||||||
"http://ewr1.flashrpc.com",
|
"http://ewr1.flashrpc.com", // LosAngeles: 同上,美国仅 ewr
|
||||||
"http://fra1.flashrpc.com",
|
"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",
|
"nyc.landing.soyas.xyz:9000",
|
||||||
"fra.landing.soyas.xyz:9000",
|
"fra.landing.soyas.xyz:9000",
|
||||||
"ams.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",
|
||||||
|
"tyo.landing.soyas.xyz:9000", // SG: 表中 APAC 仅 tyo
|
||||||
"lon.landing.soyas.xyz:9000",
|
"lon.landing.soyas.xyz:9000",
|
||||||
"nyc.landing.soyas.xyz:9000",
|
"nyc.landing.soyas.xyz:9000", // LosAngeles: 同上
|
||||||
"fra.landing.soyas.xyz:9000",
|
"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",
|
"nyc.speedlanding.trade:17778",
|
||||||
"fra.speedlanding.trade:17778",
|
"fra.speedlanding.trade:17778",
|
||||||
"ams.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",
|
"tyo.speedlanding.trade:17778",
|
||||||
"fra.speedlanding.trade:17778",
|
"sgp.speedlanding.trade:17778",
|
||||||
"nyc.speedlanding.trade:17778",
|
"ams.speedlanding.trade:17778", // London: 已公布 EU 中 ams 距英国最近之一
|
||||||
"fra.speedlanding.trade:17778",
|
"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).
|
/// 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; 10] = [
|
||||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
|
|
||||||
"http://ewr-sender.helius-rpc.com/fast",
|
"http://ewr-sender.helius-rpc.com/fast",
|
||||||
"http://fra-sender.helius-rpc.com/fast",
|
"http://fra-sender.helius-rpc.com/fast",
|
||||||
"http://ams-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://slc-sender.helius-rpc.com/fast",
|
||||||
"http://tyo-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",
|
"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默认最低小费
|
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
|
||||||
@@ -400,3 +452,73 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
|
|||||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const SWQOS_REGION_ENDPOINT_TABLES: &[&[&str]] = &[
|
||||||
|
&SWQOS_ENDPOINTS_JITO,
|
||||||
|
&SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||||
|
&SWQOS_ENDPOINTS_ZERO_SLOT,
|
||||||
|
&SWQOS_ENDPOINTS_TEMPORAL,
|
||||||
|
&SWQOS_ENDPOINTS_BLOX,
|
||||||
|
&SWQOS_ENDPOINTS_NODE1,
|
||||||
|
&SWQOS_ENDPOINTS_NODE1_QUIC,
|
||||||
|
&SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||||
|
&SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||||
|
&SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC,
|
||||||
|
&SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||||
|
&SWQOS_ENDPOINTS_ASTRALANE_BINARY,
|
||||||
|
&SWQOS_ENDPOINTS_ASTRALANE_QUIC,
|
||||||
|
&SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||||
|
&SWQOS_ENDPOINTS_STELLIUM,
|
||||||
|
&SWQOS_ENDPOINTS_SOYAS,
|
||||||
|
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||||
|
&SWQOS_ENDPOINTS_HELIUS,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn all_swqos_endpoint_tables_align_with_swqos_region() {
|
||||||
|
const EXPECT: usize = 10;
|
||||||
|
for (idx, table) in SWQOS_REGION_ENDPOINT_TABLES.iter().enumerate() {
|
||||||
|
assert_eq!(
|
||||||
|
table.len(),
|
||||||
|
EXPECT,
|
||||||
|
"SWQOS endpoint table index {} length must match SwqosRegion (10 variants)",
|
||||||
|
idx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn astralane_quic_hosts_match_mev_row_by_row() {
|
||||||
|
for i in 0..10 {
|
||||||
|
let base = SWQOS_ENDPOINTS_ASTRALANE_QUIC[i].trim_end_matches(":7000");
|
||||||
|
let mev = SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[i].trim_end_matches(":9000");
|
||||||
|
assert_eq!(base, mev, "Astralane QUIC vs MEV host mismatch at index {}", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn node1_http_host_matches_quic_without_port() {
|
||||||
|
for i in 0..10 {
|
||||||
|
let http_host = SWQOS_ENDPOINTS_NODE1[i]
|
||||||
|
.strip_prefix("http://")
|
||||||
|
.expect("NODE1 HTTP URL");
|
||||||
|
let quic_host = SWQOS_ENDPOINTS_NODE1_QUIC[i]
|
||||||
|
.strip_suffix(":16666")
|
||||||
|
.expect("NODE1 QUIC endpoint");
|
||||||
|
assert_eq!(http_host, quic_host, "Node1 HTTP vs QUIC host mismatch at index {}", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn astralane_plain_and_binary_same_origin_per_region() {
|
||||||
|
for i in 0..10 {
|
||||||
|
let plain = SWQOS_ENDPOINTS_ASTRALANE_PLAIN[i].trim_end_matches("/iris");
|
||||||
|
let binary = SWQOS_ENDPOINTS_ASTRALANE_BINARY[i].trim_end_matches("/irisb");
|
||||||
|
assert_eq!(plain, binary, "Astralane Plain vs Binary base URL mismatch at index {}", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ impl InstructionBuilder for MeteoraDammV2InstructionBuilder {
|
|||||||
¶ms.payer.pubkey(),
|
¶ms.payer.pubkey(),
|
||||||
¶ms.payer.pubkey(),
|
¶ms.payer.pubkey(),
|
||||||
¶ms.output_mint,
|
¶ms.output_mint,
|
||||||
&crate::constants::TOKEN_PROGRAM,
|
if is_a_in {
|
||||||
|
&protocol_params.token_b_program
|
||||||
|
} else {
|
||||||
|
&protocol_params.token_a_program
|
||||||
|
},
|
||||||
params.open_seed_optimize,
|
params.open_seed_optimize,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ use crate::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
instruction::utils::pumpfun::{
|
instruction::utils::pumpfun::{
|
||||||
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
|
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
|
||||||
get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta,
|
get_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
|
||||||
resolve_creator_vault_for_ix,
|
pump_fun_fee_recipient_meta, resolve_creator_vault_for_ix,
|
||||||
global_constants::{self},
|
global_constants::{self},
|
||||||
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||||
},
|
},
|
||||||
@@ -177,6 +177,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
accounts::FEE_PROGRAM_META,
|
accounts::FEE_PROGRAM_META,
|
||||||
];
|
];
|
||||||
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // remainingAccounts: @pump-fun/pump-sdk 要求末尾传 bondingCurveV2Pda(mint),勿删
|
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));
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, accounts));
|
||||||
|
|
||||||
@@ -304,6 +306,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
|||||||
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint)
|
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_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));
|
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, accounts));
|
||||||
|
|
||||||
|
|||||||
+38
-13
@@ -2,9 +2,9 @@ use crate::{
|
|||||||
constants::trade::trade::DEFAULT_SLIPPAGE,
|
constants::trade::trade::DEFAULT_SLIPPAGE,
|
||||||
instruction::utils::pumpswap::{
|
instruction::utils::pumpswap::{
|
||||||
accounts, fee_recipient_ata, get_mayhem_fee_recipient_random, get_pool_v2_pda,
|
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_protocol_extra_fee_recipient_random, get_user_volume_accumulator_pda,
|
||||||
get_user_volume_accumulator_wsol_ata, BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR,
|
get_user_volume_accumulator_quote_ata, get_user_volume_accumulator_wsol_ata,
|
||||||
SELL_DISCRIMINATOR,
|
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
|
||||||
},
|
},
|
||||||
trading::{
|
trading::{
|
||||||
common::wsol_manager,
|
common::wsol_manager,
|
||||||
@@ -76,6 +76,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||||
creator = params_coin_creator_vault_authority;
|
creator = params_coin_creator_vault_authority;
|
||||||
}
|
}
|
||||||
|
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||||
|
|
||||||
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc {
|
||||||
let result = buy_quote_input_internal(
|
let result = buy_quote_input_internal(
|
||||||
@@ -84,6 +85,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
&creator,
|
&creator,
|
||||||
|
cashback_fee_bps,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// base_amount_out, max_quote_amount_in
|
// base_amount_out, max_quote_amount_in
|
||||||
@@ -95,6 +97,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
&creator,
|
&creator,
|
||||||
|
cashback_fee_bps,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// min_quote_amount_out, base_amount_in
|
// min_quote_amount_out, base_amount_in
|
||||||
@@ -166,7 +169,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create buy instruction
|
// Create buy instruction
|
||||||
let mut accounts = Vec::with_capacity(23);
|
let mut accounts = Vec::with_capacity(28);
|
||||||
accounts.extend([
|
accounts.extend([
|
||||||
AccountMeta::new(pool, false), // pool_id
|
AccountMeta::new(pool, false), // pool_id
|
||||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||||
@@ -202,10 +205,21 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
accounts.push(AccountMeta::new(wsol_ata, false));
|
accounts.push(AccountMeta::new(wsol_ata, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remainingAccounts: @pump-fun/pump-swap-sdk 要求末尾传 poolV2Pda(baseMint),勿删
|
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts);
|
||||||
let pool_v2 = get_pool_v2_pda(&base_mint)
|
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。
|
||||||
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
|
if protocol_params.coin_creator != Pubkey::default() {
|
||||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||||
|
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||||
|
})?;
|
||||||
|
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
||||||
|
}
|
||||||
|
// Trailing accounts: GlobalConfig.buyback_fee_recipients 中任 pubkey + quote ATA(与 pump-swap-sdk 静态池对齐;轮换时需查链上)。
|
||||||
|
let protocol_extra = get_protocol_extra_fee_recipient_random();
|
||||||
|
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
|
||||||
|
accounts.push(AccountMeta::new(
|
||||||
|
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
|
||||||
// Create instruction data(buy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数)
|
// Create instruction data(buy/buy_exact_quote_in 第三参数 track_volume: OptionBool,仅代币支持返现时传 Some(true);sell 仅两参数)
|
||||||
let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
|
let track_volume = if protocol_params.is_cashback_coin { [1u8, 1u8] } else { [1u8, 0u8] }; // Some(true) / Some(false)
|
||||||
@@ -292,6 +306,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY {
|
||||||
creator = params_coin_creator_vault_authority;
|
creator = params_coin_creator_vault_authority;
|
||||||
}
|
}
|
||||||
|
let cashback_fee_bps = protocol_params.cashback_fee_basis_points;
|
||||||
|
|
||||||
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc {
|
||||||
let result = sell_base_input_internal(
|
let result = sell_base_input_internal(
|
||||||
@@ -300,6 +315,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
&creator,
|
&creator,
|
||||||
|
cashback_fee_bps,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// base_amount_in, min_quote_amount_out
|
// base_amount_in, min_quote_amount_out
|
||||||
@@ -311,6 +327,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
pool_base_token_reserves,
|
pool_base_token_reserves,
|
||||||
pool_quote_token_reserves,
|
pool_quote_token_reserves,
|
||||||
&creator,
|
&creator,
|
||||||
|
cashback_fee_bps,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// max_quote_amount_in, base_amount_out
|
// max_quote_amount_in, base_amount_out
|
||||||
@@ -359,7 +376,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create sell instruction
|
// Create sell instruction
|
||||||
let mut accounts = Vec::with_capacity(23);
|
let mut accounts = Vec::with_capacity(28);
|
||||||
accounts.extend([
|
accounts.extend([
|
||||||
AccountMeta::new(pool, false), // pool_id
|
AccountMeta::new(pool, false), // pool_id
|
||||||
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
AccountMeta::new(params.payer.pubkey(), true), // user (signer)
|
||||||
@@ -403,10 +420,18 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
|||||||
accounts.push(AccountMeta::new(accumulator, false));
|
accounts.push(AccountMeta::new(accumulator, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// remainingAccounts: @pump-fun/pump-swap-sdk sell 要求末尾传 poolV2Pda(baseMint),勿删
|
if protocol_params.coin_creator != Pubkey::default() {
|
||||||
let pool_v2 = get_pool_v2_pda(&base_mint)
|
let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| {
|
||||||
.ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?;
|
anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint)
|
||||||
accounts.push(AccountMeta::new_readonly(pool_v2, false));
|
})?;
|
||||||
|
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
|
// Create instruction data
|
||||||
let mut data = [0u8; 24];
|
let mut data = [0u8; 24];
|
||||||
|
|||||||
@@ -136,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_6: Pubkey = pubkey!("FWsW1xNtWscwNmKv6wVsU1iTzRN6wmmk3MjxRP5tT7hz"); // Pump.fun AMM: Protocol Fee 6
|
||||||
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
pub const PUMPFUN_AMM_FEE_7: Pubkey = pubkey!("G5UZAVbAf46s7cKWoyKu8kYTip9DGTpbLZ2qa9Aq69dP");
|
||||||
// Pump.fun AMM: Protocol Fee 7
|
// 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
|
/// Constants related to program accounts and authorities
|
||||||
@@ -258,6 +271,14 @@ pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 从静态池随机。
|
/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
|
pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
|
||||||
|
|||||||
@@ -91,6 +91,20 @@ pub mod accounts {
|
|||||||
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
|
/// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS)
|
||||||
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0];
|
||||||
|
|
||||||
|
/// Buyback trailing fee recipients (`GlobalConfig.buyback_fee_recipients` on Pump AMM).
|
||||||
|
/// Must match one of these for the pubkey passed after optional `pool-v2` (`@pump-fun/pump-swap-sdk` `getBuybackFeeRecipient`).
|
||||||
|
/// Static mirror of pump-public-docs; if protocol rotates configs, decode global_config from RPC.
|
||||||
|
pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [
|
||||||
|
pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"),
|
||||||
|
pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"),
|
||||||
|
pubkey!("GXPFM2caqTtQYC2cJ5yJRi9VDkpsYZXzYdwYpGnLmtDL"),
|
||||||
|
pubkey!("3BpXnfJaUTiwXnJNe7Ej1rcbzqTTQUvLShZaWazebsVR"),
|
||||||
|
pubkey!("5cjcW9wExnJJiqgLjq7DEG75Pm6JBgE1hNv4B2vHXUW6"),
|
||||||
|
pubkey!("EHAAiTxcdDwQ3U4bU6YcMsQGaekdzLS3B5SmYo46kJtL"),
|
||||||
|
pubkey!("5eHhjP8JaYkz83CWwvGU2uMUXefd3AazWGx4gpcuEEYD"),
|
||||||
|
pubkey!("A7hAgCzFw14fejgCp387JUJRMNyz4j89JKnhtKU8piqW"),
|
||||||
|
];
|
||||||
|
|
||||||
// META
|
// META
|
||||||
|
|
||||||
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
pub const GLOBAL_ACCOUNT_META: solana_sdk::instruction::AccountMeta =
|
||||||
@@ -171,6 +185,14 @@ pub fn get_mayhem_fee_recipient_random() -> (Pubkey, AccountMeta) {
|
|||||||
(recipient, meta)
|
(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.
|
/// Pool v2 PDA (seeds: ["pool-v2", base_mint]). Required at end of buy/sell/buy_exact_quote_in accounts.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
pub fn get_pool_v2_pda(base_mint: &Pubkey) -> Option<Pubkey> {
|
||||||
|
|||||||
+7
-1165
File diff suppressed because it is too large
Load Diff
+13
-5
@@ -27,6 +27,8 @@ pub enum AstralaneBackend {
|
|||||||
Http {
|
Http {
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
auth_token: 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,
|
http_client: Client,
|
||||||
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
ping_handle: Arc<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
|
||||||
stop_ping: Arc<AtomicBool>,
|
stop_ping: Arc<AtomicBool>,
|
||||||
@@ -77,8 +79,8 @@ impl SwqosClientTrait for AstralaneClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AstralaneClient {
|
impl AstralaneClient {
|
||||||
/// 使用 HTTP(irisb)提交。
|
/// HTTP 提交:`/iris`(Plain)或 `/irisb`(Binary),由 `endpoint` URL 路径区分;`mev_http` 为 true 时附加 `mev-protect=true`。
|
||||||
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
|
pub fn new(rpc_url: String, endpoint: String, auth_token: String, mev_http: bool) -> Self {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let http_client = default_http_client_builder().build().unwrap();
|
let http_client = default_http_client_builder().build().unwrap();
|
||||||
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
|
||||||
@@ -89,6 +91,7 @@ impl AstralaneClient {
|
|||||||
backend: AstralaneBackend::Http {
|
backend: AstralaneBackend::Http {
|
||||||
endpoint,
|
endpoint,
|
||||||
auth_token,
|
auth_token,
|
||||||
|
mev_http,
|
||||||
http_client,
|
http_client,
|
||||||
ping_handle,
|
ping_handle,
|
||||||
stop_ping,
|
stop_ping,
|
||||||
@@ -119,6 +122,7 @@ impl AstralaneClient {
|
|||||||
http_client,
|
http_client,
|
||||||
ping_handle,
|
ping_handle,
|
||||||
stop_ping,
|
stop_ping,
|
||||||
|
..
|
||||||
} => {
|
} => {
|
||||||
let endpoint = endpoint.clone();
|
let endpoint = endpoint.clone();
|
||||||
let auth_token = auth_token.clone();
|
let auth_token = auth_token.clone();
|
||||||
@@ -182,10 +186,14 @@ impl AstralaneClient {
|
|||||||
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
|
||||||
|
|
||||||
match &self.backend {
|
match &self.backend {
|
||||||
AstralaneBackend::Http { endpoint, auth_token, http_client, .. } => {
|
AstralaneBackend::Http { endpoint, auth_token, mev_http, http_client, .. } => {
|
||||||
let response = http_client
|
let mut req = http_client
|
||||||
.post(endpoint)
|
.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")
|
.header("Content-Type", "application/octet-stream")
|
||||||
.body(body_bytes)
|
.body(body_bytes)
|
||||||
.send()
|
.send()
|
||||||
|
|||||||
+69
-40
@@ -28,8 +28,9 @@ use anyhow::Result;
|
|||||||
use crate::{
|
use crate::{
|
||||||
common::SolanaRpcClient,
|
common::SolanaRpcClient,
|
||||||
constants::swqos::{
|
constants::swqos::{
|
||||||
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC,
|
SWQOS_ENDPOINTS_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV, SWQOS_ENDPOINTS_BLOCKRAZOR,
|
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||||
|
SWQOS_ENDPOINTS_BLOCKRAZOR,
|
||||||
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
|
SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK,
|
||||||
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
|
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||||
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
|
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS,
|
||||||
@@ -63,7 +64,7 @@ pub const SWQOS_BLACKLIST: &[SwqosType] = &[
|
|||||||
|
|
||||||
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
|
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
|
||||||
/// BlockRazor 支持 gRPC 和 HTTP。
|
/// BlockRazor 支持 gRPC 和 HTTP。
|
||||||
/// Astralane 和 Node1 支持 QUIC。
|
/// Node1 支持 QUIC。
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
pub enum SwqosTransport {
|
pub enum SwqosTransport {
|
||||||
#[default]
|
#[default]
|
||||||
@@ -72,6 +73,19 @@ pub enum SwqosTransport {
|
|||||||
Quic,
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum TradeType {
|
pub enum TradeType {
|
||||||
Create,
|
Create,
|
||||||
@@ -196,15 +210,23 @@ pub trait SwqosClientTrait {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 地理区域,用于默认 SWQOS 端点下标(见 `constants::swqos`)。
|
||||||
|
///
|
||||||
|
/// 各服务商常量表在**缺独立 PoP**时,于**已公布的端点集合内**按地理距离选最近项;[`SwqosRegion::Default`] 不表示地球上的位置,表中为全局/枢纽回退,不适用地理就近。
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum SwqosRegion {
|
pub enum SwqosRegion {
|
||||||
NewYork,
|
NewYork,
|
||||||
Frankfurt,
|
Frankfurt,
|
||||||
Amsterdam,
|
Amsterdam,
|
||||||
|
/// Ireland (EU); Jito publishes `dublin.mainnet.block-engine.jito.wtf`.
|
||||||
|
Dublin,
|
||||||
SLC,
|
SLC,
|
||||||
Tokyo,
|
Tokyo,
|
||||||
|
/// Southeast Asia (Singapore); not interchangeable with [`SwqosRegion::Tokyo`].
|
||||||
|
Singapore,
|
||||||
London,
|
London,
|
||||||
LosAngeles,
|
LosAngeles,
|
||||||
|
/// 非地理区域:未指定区域时的回退,对应表中全局 URL 或枢纽,**不按地理距离选取**。
|
||||||
Default,
|
Default,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,8 +249,8 @@ pub enum SwqosConfig {
|
|||||||
FlashBlock(String, SwqosRegion, Option<String>),
|
FlashBlock(String, SwqosRegion, Option<String>),
|
||||||
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
|
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
|
||||||
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||||
/// Astralane(api_token, region, custom_url, transport). transport=None 表示 Http。
|
/// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`](`/irisb`)。
|
||||||
Astralane(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
Astralane(String, SwqosRegion, Option<String>, Option<AstralaneTransport>),
|
||||||
/// Stellium(api_token, region, custom_url)
|
/// Stellium(api_token, region, custom_url)
|
||||||
Stellium(String, SwqosRegion, Option<String>),
|
Stellium(String, SwqosRegion, Option<String>),
|
||||||
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
|
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
|
||||||
@@ -285,7 +307,7 @@ impl SwqosConfig {
|
|||||||
SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(),
|
SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(),
|
||||||
SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[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::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::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(),
|
||||||
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
|
SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key
|
||||||
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
|
||||||
@@ -300,7 +322,7 @@ impl SwqosConfig {
|
|||||||
region: SwqosRegion,
|
region: SwqosRegion,
|
||||||
url: Option<String>,
|
url: Option<String>,
|
||||||
transport: Option<SwqosTransport>,
|
transport: Option<SwqosTransport>,
|
||||||
mev_protection: bool,
|
_mev_protection: bool,
|
||||||
) -> String {
|
) -> String {
|
||||||
if let Some(custom_url) = url {
|
if let Some(custom_url) = url {
|
||||||
return custom_url;
|
return custom_url;
|
||||||
@@ -324,19 +346,6 @@ impl SwqosConfig {
|
|||||||
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
|
SWQOS_ENDPOINTS_NODE1[region as usize].to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SwqosType::Astralane => {
|
|
||||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
|
||||||
if use_quic {
|
|
||||||
// MEV protection: port 9000; standard: port 7000
|
|
||||||
if mev_protection {
|
|
||||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
|
||||||
} else {
|
|
||||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Self::get_endpoint(swqos_type, region, None),
|
_ => Self::get_endpoint(swqos_type, region, None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,26 +423,46 @@ impl SwqosConfig {
|
|||||||
Ok(Arc::new(blockrazor_client))
|
Ok(Arc::new(blockrazor_client))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SwqosConfig::Astralane(auth_token, region, url, transport) => {
|
SwqosConfig::Astralane(auth_token, region, url, mode) => {
|
||||||
let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
|
let mode = mode.unwrap_or_default();
|
||||||
if use_quic {
|
match mode {
|
||||||
let quic_endpoint = url.unwrap_or_else(|| {
|
AstralaneTransport::Quic => {
|
||||||
// MEV protection: port 9000; standard: port 7000
|
let quic_endpoint = url.unwrap_or_else(|| {
|
||||||
if mev_protection {
|
if mev_protection {
|
||||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string()
|
||||||
} else {
|
} else {
|
||||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let astralane_client =
|
let astralane_client =
|
||||||
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Arc::new(astralane_client))
|
Ok(Arc::new(astralane_client))
|
||||||
} else {
|
}
|
||||||
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
|
AstralaneTransport::Plain => {
|
||||||
let astralane_client =
|
let endpoint = url.unwrap_or_else(|| {
|
||||||
AstralaneClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
|
SWQOS_ENDPOINTS_ASTRALANE_PLAIN[region as usize].to_string()
|
||||||
Ok(Arc::new(astralane_client))
|
});
|
||||||
|
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) => {
|
SwqosConfig::Stellium(auth_token, region, url) => {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use solana_transaction_status::UiTransactionEncoding;
|
|||||||
|
|
||||||
use crate::swqos::SwqosClientTrait;
|
use crate::swqos::SwqosClientTrait;
|
||||||
use crate::{
|
use crate::{
|
||||||
common::SolanaRpcClient,
|
common::{sdk_log, SolanaRpcClient},
|
||||||
swqos::{common::poll_transaction_confirmation, SwqosType, TradeType},
|
swqos::{common::poll_transaction_confirmation, SwqosType, TradeType},
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@@ -25,6 +25,7 @@ impl SwqosClientTrait for SolRpcClient {
|
|||||||
transaction: &VersionedTransaction,
|
transaction: &VersionedTransaction,
|
||||||
wait_confirmation: bool,
|
wait_confirmation: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
let submit_start = Instant::now();
|
||||||
let signature = self
|
let signature = self
|
||||||
.rpc_client
|
.rpc_client
|
||||||
.send_transaction_with_config(
|
.send_transaction_with_config(
|
||||||
@@ -39,6 +40,8 @@ impl SwqosClientTrait for SolRpcClient {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
sdk_log::log_swqos_submitted("Default", trade_type, submit_start.elapsed());
|
||||||
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
|
|||||||
+21
-14
@@ -6,7 +6,10 @@ use quinn::{
|
|||||||
TransportConfig,
|
TransportConfig,
|
||||||
};
|
};
|
||||||
use rand::seq::IndexedRandom as _;
|
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_client::rpc_client::SerializableTransaction;
|
||||||
|
use solana_sdk::signer::Signer;
|
||||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::{
|
use std::{
|
||||||
@@ -69,18 +72,17 @@ impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate dummy self-signed certificate using rcgen
|
/// TLS 客户端证书:ECDSA P-256 + CN=钱包公钥(与 Speedlanding / Astralane QUIC 策略一致)。
|
||||||
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
|
fn generate_client_tls_credentials(keypair: &Keypair) -> Result<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
|
||||||
// Generate a new key pair for the certificate
|
let tls_key = RcgenKeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
|
||||||
let key_pair = rcgen::KeyPair::generate()?;
|
let mut cert_params = CertificateParams::new(vec![])?;
|
||||||
|
cert_params.distinguished_name.push(
|
||||||
let params = rcgen::CertificateParams::default();
|
rcgen::DnType::CommonName,
|
||||||
let cert = params.self_signed(&key_pair)?;
|
rcgen::DnValue::Utf8String(keypair.pubkey().to_string()),
|
||||||
|
);
|
||||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
let cert = cert_params.self_signed(&tls_key)?;
|
||||||
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
let cert_der = CertificateDer::from(cert.der().to_vec());
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
|
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(tls_key.serialize_der()));
|
||||||
|
|
||||||
Ok((cert_der, key_der))
|
Ok((cert_der, key_der))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +103,13 @@ pub struct SoyasClient {
|
|||||||
impl SoyasClient {
|
impl SoyasClient {
|
||||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let keypair = Keypair::from_base58_string(&api_key);
|
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
|
||||||
let (cert, key) = generate_self_signed_cert(&keypair)?;
|
anyhow::anyhow!(
|
||||||
|
"Soyas api_token 无法解析为 Solana keypair base58(QUIC mTLS 用): {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let (cert, key) = generate_client_tls_credentials(&keypair)?;
|
||||||
let mut crypto = rustls::ClientConfig::builder()
|
let mut crypto = rustls::ClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||||
|
|||||||
+61
-112
@@ -6,7 +6,9 @@ use quinn::{
|
|||||||
TransportConfig,
|
TransportConfig,
|
||||||
};
|
};
|
||||||
use rand::seq::IndexedRandom as _;
|
use rand::seq::IndexedRandom as _;
|
||||||
|
use solana_sdk::signer::Signer;
|
||||||
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
|
||||||
|
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use std::{
|
use std::{
|
||||||
net::{SocketAddr, ToSocketAddrs as _},
|
net::{SocketAddr, ToSocketAddrs as _},
|
||||||
@@ -25,69 +27,9 @@ use crate::{
|
|||||||
swqos::{SwqosType, TradeType},
|
swqos::{SwqosType, TradeType},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip server verification implementation
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct SkipServerVerification;
|
|
||||||
|
|
||||||
impl SkipServerVerification {
|
|
||||||
fn new() -> Arc<Self> {
|
|
||||||
Arc::new(Self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rustls::client::danger::ServerCertVerifier for SkipServerVerification {
|
|
||||||
fn verify_server_cert(
|
|
||||||
&self,
|
|
||||||
_end_entity: &rustls::pki_types::CertificateDer<'_>,
|
|
||||||
_intermediates: &[rustls::pki_types::CertificateDer<'_>],
|
|
||||||
_server_name: &rustls::pki_types::ServerName<'_>,
|
|
||||||
_ocsp_response: &[u8],
|
|
||||||
_now: rustls::pki_types::UnixTime,
|
|
||||||
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
|
|
||||||
Ok(rustls::client::danger::ServerCertVerified::assertion())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_tls12_signature(
|
|
||||||
&self,
|
|
||||||
_message: &[u8],
|
|
||||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
|
||||||
_dss: &rustls::DigitallySignedStruct,
|
|
||||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
||||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_tls13_signature(
|
|
||||||
&self,
|
|
||||||
_message: &[u8],
|
|
||||||
_cert: &rustls::pki_types::CertificateDer<'_>,
|
|
||||||
_dss: &rustls::DigitallySignedStruct,
|
|
||||||
) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
|
|
||||||
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
|
||||||
vec![rustls::SignatureScheme::ECDSA_NISTP256_SHA256]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate dummy self-signed certificate using rcgen
|
|
||||||
fn generate_self_signed_cert(_keypair: &Keypair) -> Result<(rustls::pki_types::CertificateDer<'static>, rustls::pki_types::PrivateKeyDer<'static>)> {
|
|
||||||
// Generate a new key pair for the certificate
|
|
||||||
let key_pair = rcgen::KeyPair::generate()?;
|
|
||||||
|
|
||||||
let params = rcgen::CertificateParams::default();
|
|
||||||
let cert = params.self_signed(&key_pair)?;
|
|
||||||
|
|
||||||
let cert_der = rustls::pki_types::CertificateDer::from(cert.der().to_vec());
|
|
||||||
let key_der = rustls::pki_types::PrivateKeyDer::try_from(key_pair.serialize_der())
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to create private key: {:?}", e))?;
|
|
||||||
|
|
||||||
Ok((cert_der, key_der))
|
|
||||||
}
|
|
||||||
|
|
||||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||||
/// Fallback SNI when endpoint is IP or cannot extract host (keeps legacy behavior).
|
/// QUIC TLS SNI:与 Speedlanding 官方客户端一致,固定为 `speed-landing`(勿用 PoP 主机名,否则易握手失败)。
|
||||||
const SPEED_SERVER_FALLBACK: &str = "speed-landing";
|
const SPEED_SERVER: &str = "speed-landing";
|
||||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
@@ -98,34 +40,21 @@ pub struct SpeedlandingClient {
|
|||||||
endpoint: Endpoint,
|
endpoint: Endpoint,
|
||||||
client_config: ClientConfig,
|
client_config: ClientConfig,
|
||||||
addr: SocketAddr,
|
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>,
|
connection: ArcSwap<Connection>,
|
||||||
reconnect: Mutex<()>,
|
reconnect: Mutex<()>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpeedlandingClient {
|
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> {
|
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||||
let rpc_client = SolanaRpcClient::new(rpc_url);
|
let rpc_client = SolanaRpcClient::new(rpc_url);
|
||||||
let server_name = Self::server_name_from_endpoint(&endpoint_string);
|
// Speedlanding QUIC:与官方一致使用 `solana_tls_utils::new_dummy_x509_certificate`(Ed25519 dummy cert)+ SNI `speed-landing`。
|
||||||
let keypair = Keypair::from_base58_string(&api_key);
|
let keypair = Keypair::try_from_base58_string(api_key.trim()).map_err(|e| {
|
||||||
let (cert, key) = generate_self_signed_cert(&keypair)?;
|
anyhow::anyhow!(
|
||||||
|
"Speedlanding api_token 无法解析为 Solana keypair base58(用于 mTLS);请确认粘贴的是机器人提供的密钥而非其它字符串: {}",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let (cert, key) = new_dummy_x509_certificate(&keypair);
|
||||||
let mut crypto = rustls::ClientConfig::builder()
|
let mut crypto = rustls::ClientConfig::builder()
|
||||||
.dangerous()
|
.dangerous()
|
||||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||||
@@ -148,18 +77,23 @@ impl SpeedlandingClient {
|
|||||||
.to_socket_addrs()?
|
.to_socket_addrs()?
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
|
.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)
|
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||||
.await
|
.await
|
||||||
.context("Speedlanding QUIC connect timeout")?
|
.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 {
|
Ok(Self {
|
||||||
rpc_client: Arc::new(rpc_client),
|
rpc_client: Arc::new(rpc_client),
|
||||||
endpoint,
|
endpoint,
|
||||||
client_config,
|
client_config,
|
||||||
addr,
|
addr,
|
||||||
server_name,
|
|
||||||
connection: ArcSwap::from_pointee(connection),
|
connection: ArcSwap::from_pointee(connection),
|
||||||
reconnect: Mutex::new(()),
|
reconnect: Mutex::new(()),
|
||||||
})
|
})
|
||||||
@@ -181,12 +115,17 @@ impl SpeedlandingClient {
|
|||||||
let connecting = self.endpoint.connect_with(
|
let connecting = self.endpoint.connect_with(
|
||||||
self.client_config.clone(),
|
self.client_config.clone(),
|
||||||
self.addr,
|
self.addr,
|
||||||
self.server_name.as_str(),
|
SPEED_SERVER,
|
||||||
)?;
|
)?;
|
||||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||||
.await
|
.await
|
||||||
.context("Speedlanding QUIC reconnect timeout")?
|
.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));
|
self.connection.store(Arc::new(connection));
|
||||||
return Ok(self.connection.load_full());
|
return Ok(self.connection.load_full());
|
||||||
}
|
}
|
||||||
@@ -219,51 +158,61 @@ impl SwqosClientTrait for SpeedlandingClient {
|
|||||||
Ok(Err(_)) | Err(_) => true,
|
Ok(Err(_)) | Err(_) => true,
|
||||||
};
|
};
|
||||||
if need_retry {
|
if need_retry {
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
eprintln!(
|
||||||
eprintln!(" [Speedlanding] {} submission failed after {:?}, reconnecting", trade_type, start_time.elapsed());
|
" [Speedlanding] {} QUIC 首次发送失败 {:?},正在重试",
|
||||||
}
|
trade_type,
|
||||||
|
start_time.elapsed()
|
||||||
|
);
|
||||||
let connection = self.ensure_connected().await?;
|
let connection = self.ensure_connected().await?;
|
||||||
send_result =
|
send_result =
|
||||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
|
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
|
||||||
}
|
}
|
||||||
match send_result.context("Speedlanding QUIC send timeout") {
|
match send_result.context("Speedlanding QUIC send timeout") {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
// 提交结果与「详细耗时/SDK 开关」无关,便于确认当前通道确实在执行
|
||||||
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
|
crate::common::sdk_log::log_swqos_submitted("Speedlanding", trade_type, start_time.elapsed());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
crate::common::sdk_log::log_swqos_submission_failed(
|
||||||
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), &e);
|
"Speedlanding",
|
||||||
}
|
trade_type,
|
||||||
|
start_time.elapsed(),
|
||||||
|
&e,
|
||||||
|
);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
crate::common::sdk_log::log_swqos_submission_failed(
|
||||||
crate::common::sdk_log::log_swqos_submission_failed("Speedlanding", trade_type, start_time.elapsed(), "timeout");
|
"Speedlanding",
|
||||||
}
|
trade_type,
|
||||||
|
start_time.elapsed(),
|
||||||
|
"timeout",
|
||||||
|
);
|
||||||
return Err(e.into());
|
return Err(e.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
|
||||||
Ok(_) => (),
|
Ok(_) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if crate::common::sdk_log::sdk_log_enabled() {
|
println!(" signature: {:?}", signature);
|
||||||
println!(" signature: {:?}", signature);
|
crate::common::sdk_log::log_swqos_submission_failed(
|
||||||
println!(
|
"Speedlanding",
|
||||||
" [{:width$}] {} confirmation failed: {:?}",
|
trade_type,
|
||||||
"Speedlanding",
|
start_time.elapsed(),
|
||||||
trade_type,
|
&e,
|
||||||
start_time.elapsed(),
|
);
|
||||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return Err(e);
|
return Err(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
if wait_confirmation {
|
||||||
println!(" signature: {:?}", signature);
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ use std::collections::HashMap;
|
|||||||
use std::hash::BuildHasherDefault;
|
use std::hash::BuildHasherDefault;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
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 tokio::sync::Notify;
|
||||||
|
|
||||||
use fnv::FnvHasher;
|
use fnv::FnvHasher;
|
||||||
@@ -402,14 +406,31 @@ impl ResultCollector {
|
|||||||
timeout_secs: u64,
|
timeout_secs: u64,
|
||||||
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
) -> Option<(bool, Vec<Signature>, Option<anyhow::Error>, Vec<(SwqosType, i64)>)> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let timeout = std::time::Duration::from_secs(timeout_secs);
|
let primary = Duration::from_secs(timeout_secs);
|
||||||
let poll_interval = std::time::Duration::from_millis(2);
|
let poll_interval = Duration::from_millis(2);
|
||||||
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
while self.completed_count.load(Ordering::Acquire) < self.total_tasks {
|
||||||
if start.elapsed() > timeout {
|
if start.elapsed() > primary {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
tokio::time::sleep(poll_interval).await;
|
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()
|
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.
|
// 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 {
|
let shared = Arc::new(SwqosSharedContext {
|
||||||
payer,
|
payer,
|
||||||
instructions,
|
instructions,
|
||||||
@@ -562,13 +587,20 @@ pub async fn execute_parallel(
|
|||||||
// All jobs enqueued (no spawn on hot path)
|
// All jobs enqueued (no spawn on hot path)
|
||||||
|
|
||||||
if !wait_transaction_confirmed {
|
if !wait_transaction_confirmed {
|
||||||
const SUBMIT_TIMEOUT_SECS: u64 = 2;//无需确认的交易,一般2秒合适了 一般2秒内发送全都返回 没返回的也不等了,没返回的就是太慢的swqos
|
// submit_timeout_secs 为「等齐各 SWQOS HTTP 应答」的上限;收齐后会立刻进入短 settle,不会睡满整段秒数。
|
||||||
let ret = collector.wait_for_all_submitted(SUBMIT_TIMEOUT_SECS).await.unwrap_or((
|
// `wait_for_all_submitted` 仅在未收齐时追加长 grace,避免过早 drain 丢晚到签名。
|
||||||
false,
|
let ret = collector
|
||||||
vec![],
|
.wait_for_all_submitted(submit_timeout_secs)
|
||||||
Some(anyhow!("No SWQOS result within {}s", SUBMIT_TIMEOUT_SECS)),
|
.await
|
||||||
vec![],
|
.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;
|
let (success, signatures, last_error, submit_timings) = ret;
|
||||||
return Ok((success, signatures, last_error, submit_timings));
|
return Ok((success, signatures, last_error, submit_timings));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,867 +0,0 @@
|
|||||||
use crate::common::bonding_curve::BondingCurveAccount;
|
|
||||||
use crate::common::nonce_cache::DurableNonceInfo;
|
|
||||||
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
|
||||||
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
|
||||||
use crate::constants::TOKEN_PROGRAM;
|
|
||||||
use core_affinity::CoreId;
|
|
||||||
use crate::instruction::utils::pumpfun::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)]
|
|
||||||
pub struct SenderConcurrencyConfig {
|
|
||||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
|
||||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
|
||||||
pub max_sender_concurrency: usize,
|
|
||||||
}
|
|
||||||
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
|
||||||
use crate::swqos::{SwqosClient, TradeType};
|
|
||||||
use crate::trading::common::get_multi_token_balances;
|
|
||||||
use crate::trading::MiddlewareManager;
|
|
||||||
use solana_hash::Hash;
|
|
||||||
use solana_message::AddressLookupTableAccount;
|
|
||||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum DexParamEnum {
|
|
||||||
PumpFun(PumpFunParams),
|
|
||||||
PumpSwap(PumpSwapParams),
|
|
||||||
Bonk(BonkParams),
|
|
||||||
RaydiumCpmm(RaydiumCpmmParams),
|
|
||||||
RaydiumAmmV4(RaydiumAmmV4Params),
|
|
||||||
MeteoraDammV2(MeteoraDammV2Params),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DexParamEnum {
|
|
||||||
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
|
||||||
#[inline]
|
|
||||||
pub fn as_any(&self) -> &dyn std::any::Any {
|
|
||||||
match self {
|
|
||||||
DexParamEnum::PumpFun(p) => p,
|
|
||||||
DexParamEnum::PumpSwap(p) => p,
|
|
||||||
DexParamEnum::Bonk(p) => p,
|
|
||||||
DexParamEnum::RaydiumCpmm(p) => p,
|
|
||||||
DexParamEnum::RaydiumAmmV4(p) => p,
|
|
||||||
DexParamEnum::MeteoraDammV2(p) => p,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Swap parameters
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SwapParams {
|
|
||||||
pub rpc: Option<Arc<SolanaRpcClient>>,
|
|
||||||
pub payer: Arc<Keypair>,
|
|
||||||
pub trade_type: TradeType,
|
|
||||||
pub input_mint: Pubkey,
|
|
||||||
pub input_token_program: Option<Pubkey>,
|
|
||||||
pub output_mint: Pubkey,
|
|
||||||
pub output_token_program: Option<Pubkey>,
|
|
||||||
pub input_amount: Option<u64>,
|
|
||||||
pub slippage_basis_points: Option<u64>,
|
|
||||||
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
|
||||||
pub recent_blockhash: Option<Hash>,
|
|
||||||
pub wait_tx_confirmed: bool,
|
|
||||||
pub protocol_params: DexParamEnum,
|
|
||||||
pub open_seed_optimize: bool,
|
|
||||||
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
|
|
||||||
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
|
||||||
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
|
||||||
pub durable_nonce: Option<DurableNonceInfo>,
|
|
||||||
pub with_tip: bool,
|
|
||||||
pub create_input_mint_ata: bool,
|
|
||||||
pub close_input_mint_ata: bool,
|
|
||||||
pub create_output_mint_ata: bool,
|
|
||||||
pub close_output_mint_ata: bool,
|
|
||||||
pub fixed_output_amount: Option<u64>,
|
|
||||||
pub gas_fee_strategy: GasFeeStrategy,
|
|
||||||
pub simulate: bool,
|
|
||||||
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
|
||||||
pub log_enabled: bool,
|
|
||||||
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
|
|
||||||
pub use_dedicated_sender_threads: bool,
|
|
||||||
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
|
|
||||||
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
|
||||||
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
|
|
||||||
pub max_sender_concurrency: usize,
|
|
||||||
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
|
|
||||||
pub effective_core_ids: Arc<Vec<CoreId>>,
|
|
||||||
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
|
||||||
pub check_min_tip: bool,
|
|
||||||
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
|
||||||
pub grpc_recv_us: Option<i64>,
|
|
||||||
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
|
||||||
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
|
||||||
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
|
||||||
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
|
||||||
pub use_exact_sol_amount: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SwapParams {
|
|
||||||
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
|
|
||||||
#[inline]
|
|
||||||
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
|
|
||||||
SenderConcurrencyConfig {
|
|
||||||
sender_thread_cores: self.sender_thread_cores.clone(),
|
|
||||||
effective_core_ids: self.effective_core_ids.clone(),
|
|
||||||
max_sender_concurrency: self.max_sender_concurrency,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Debug for SwapParams {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "SwapParams: ...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PumpFun protocol specific parameters
|
|
||||||
/// Configuration parameters specific to PumpFun trading protocol.
|
|
||||||
///
|
|
||||||
/// **Creator 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,
|
|
||||||
/// 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 {
|
|
||||||
pub fn immediate_sell(
|
|
||||||
creator_vault: Pubkey,
|
|
||||||
token_program: Pubkey,
|
|
||||||
close_token_account_when_sell: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
|
||||||
associated_bonding_curve: Pubkey::default(),
|
|
||||||
creator_vault: creator_vault,
|
|
||||||
token_program: token_program,
|
|
||||||
close_token_account_when_sell: Some(close_token_account_when_sell),
|
|
||||||
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,
|
|
||||||
max_sol_cost: u64,
|
|
||||||
creator: Pubkey,
|
|
||||||
bonding_curve: Pubkey,
|
|
||||||
associated_bonding_curve: Pubkey,
|
|
||||||
creator_vault: Pubkey,
|
|
||||||
close_token_account_when_sell: Option<bool>,
|
|
||||||
fee_recipient: Pubkey,
|
|
||||||
token_program: Pubkey,
|
|
||||||
is_cashback_coin: bool,
|
|
||||||
mayhem_mode: Option<bool>,
|
|
||||||
) -> Self {
|
|
||||||
let is_mayhem_mode =
|
|
||||||
mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient));
|
|
||||||
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
|
||||||
bonding_curve,
|
|
||||||
&mint,
|
|
||||||
token_amount,
|
|
||||||
max_sol_cost,
|
|
||||||
creator,
|
|
||||||
is_mayhem_mode,
|
|
||||||
is_cashback_coin,
|
|
||||||
);
|
|
||||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
|
|
||||||
&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_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,
|
|
||||||
mint: Pubkey,
|
|
||||||
creator: Pubkey,
|
|
||||||
creator_vault: Pubkey,
|
|
||||||
virtual_token_reserves: u64,
|
|
||||||
virtual_sol_reserves: u64,
|
|
||||||
real_token_reserves: u64,
|
|
||||||
real_sol_reserves: u64,
|
|
||||||
close_token_account_when_sell: Option<bool>,
|
|
||||||
fee_recipient: Pubkey,
|
|
||||||
token_program: Pubkey,
|
|
||||||
is_cashback_coin: bool,
|
|
||||||
mayhem_mode: Option<bool>,
|
|
||||||
) -> Self {
|
|
||||||
let is_mayhem_mode = match mayhem_mode {
|
|
||||||
Some(v) => v,
|
|
||||||
None => is_mayhem_fee_recipient(&fee_recipient),
|
|
||||||
};
|
|
||||||
let bonding_curve = BondingCurveAccount::from_trade(
|
|
||||||
bonding_curve,
|
|
||||||
mint,
|
|
||||||
creator,
|
|
||||||
virtual_token_reserves,
|
|
||||||
virtual_sol_reserves,
|
|
||||||
real_token_reserves,
|
|
||||||
real_sol_reserves,
|
|
||||||
is_mayhem_mode,
|
|
||||||
is_cashback_coin,
|
|
||||||
);
|
|
||||||
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
|
|
||||||
&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_resolved,
|
|
||||||
close_token_account_when_sell: close_token_account_when_sell,
|
|
||||||
token_program: token_program,
|
|
||||||
fee_recipient,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_mint_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
mint: &Pubkey,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let account =
|
|
||||||
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
|
||||||
let mint_account = rpc.get_account(&mint).await?;
|
|
||||||
let bonding_curve = BondingCurveAccount {
|
|
||||||
discriminator: 0,
|
|
||||||
account: account.1,
|
|
||||||
virtual_token_reserves: account.0.virtual_token_reserves,
|
|
||||||
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
|
||||||
real_token_reserves: account.0.real_token_reserves,
|
|
||||||
real_sol_reserves: account.0.real_sol_reserves,
|
|
||||||
token_total_supply: account.0.token_total_supply,
|
|
||||||
complete: account.0.complete,
|
|
||||||
creator: account.0.creator,
|
|
||||||
is_mayhem_mode: account.0.is_mayhem_mode,
|
|
||||||
is_cashback_coin: account.0.is_cashback_coin,
|
|
||||||
};
|
|
||||||
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
|
||||||
&bonding_curve.account,
|
|
||||||
mint,
|
|
||||||
&mint_account.owner,
|
|
||||||
);
|
|
||||||
let creator_vault =
|
|
||||||
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator);
|
|
||||||
Ok(Self {
|
|
||||||
bonding_curve: Arc::new(bonding_curve),
|
|
||||||
associated_bonding_curve: associated_bonding_curve,
|
|
||||||
creator_vault: creator_vault.unwrap(),
|
|
||||||
close_token_account_when_sell: None,
|
|
||||||
token_program: mint_account.owner,
|
|
||||||
fee_recipient: Pubkey::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// PumpSwap Protocol Specific Parameters
|
|
||||||
///
|
|
||||||
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
|
||||||
/// token configuration, and transaction amounts.
|
|
||||||
///
|
|
||||||
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
|
||||||
/// retrieve the relevant information from RPC, which will increase transaction time.
|
|
||||||
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PumpSwapParams {
|
|
||||||
/// Liquidity pool address
|
|
||||||
pub pool: Pubkey,
|
|
||||||
/// Base token mint address
|
|
||||||
/// The mint account address of the base token in the trading pair
|
|
||||||
pub base_mint: Pubkey,
|
|
||||||
/// Quote token mint address
|
|
||||||
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
|
||||||
pub quote_mint: Pubkey,
|
|
||||||
/// Pool base token account
|
|
||||||
pub pool_base_token_account: Pubkey,
|
|
||||||
/// Pool quote token account
|
|
||||||
pub pool_quote_token_account: Pubkey,
|
|
||||||
/// Base token reserves in the pool
|
|
||||||
pub pool_base_token_reserves: u64,
|
|
||||||
/// Quote token reserves in the pool
|
|
||||||
pub pool_quote_token_reserves: u64,
|
|
||||||
/// Coin creator vault ATA
|
|
||||||
pub coin_creator_vault_ata: Pubkey,
|
|
||||||
/// Coin creator vault authority
|
|
||||||
pub coin_creator_vault_authority: Pubkey,
|
|
||||||
/// Token program ID
|
|
||||||
pub base_token_program: Pubkey,
|
|
||||||
/// Quote token program ID
|
|
||||||
pub quote_token_program: Pubkey,
|
|
||||||
/// Whether the pool is in mayhem mode
|
|
||||||
pub is_mayhem_mode: bool,
|
|
||||||
/// Whether the pool's coin has cashback enabled
|
|
||||||
pub is_cashback_coin: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PumpSwapParams {
|
|
||||||
pub fn new(
|
|
||||||
pool: Pubkey,
|
|
||||||
base_mint: Pubkey,
|
|
||||||
quote_mint: Pubkey,
|
|
||||||
pool_base_token_account: Pubkey,
|
|
||||||
pool_quote_token_account: Pubkey,
|
|
||||||
pool_base_token_reserves: u64,
|
|
||||||
pool_quote_token_reserves: u64,
|
|
||||||
coin_creator_vault_ata: Pubkey,
|
|
||||||
coin_creator_vault_authority: Pubkey,
|
|
||||||
base_token_program: Pubkey,
|
|
||||||
quote_token_program: Pubkey,
|
|
||||||
fee_recipient: Pubkey,
|
|
||||||
is_cashback_coin: bool,
|
|
||||||
) -> Self {
|
|
||||||
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
|
||||||
Self {
|
|
||||||
pool,
|
|
||||||
base_mint,
|
|
||||||
quote_mint,
|
|
||||||
pool_base_token_account,
|
|
||||||
pool_quote_token_account,
|
|
||||||
pool_base_token_reserves,
|
|
||||||
pool_quote_token_reserves,
|
|
||||||
coin_creator_vault_ata,
|
|
||||||
coin_creator_vault_authority,
|
|
||||||
base_token_program,
|
|
||||||
quote_token_program,
|
|
||||||
is_mayhem_mode,
|
|
||||||
is_cashback_coin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
|
||||||
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
|
||||||
/// lookups and associated latency. Token program IDs should be sourced from
|
|
||||||
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
|
||||||
/// differences.
|
|
||||||
///
|
|
||||||
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
|
||||||
/// from the event so that buy/sell instructions include the correct remaining
|
|
||||||
/// accounts for cashback.
|
|
||||||
pub fn from_trade(
|
|
||||||
pool: Pubkey,
|
|
||||||
base_mint: Pubkey,
|
|
||||||
quote_mint: Pubkey,
|
|
||||||
pool_base_token_account: Pubkey,
|
|
||||||
pool_quote_token_account: Pubkey,
|
|
||||||
pool_base_token_reserves: u64,
|
|
||||||
pool_quote_token_reserves: u64,
|
|
||||||
coin_creator_vault_ata: Pubkey,
|
|
||||||
coin_creator_vault_authority: Pubkey,
|
|
||||||
base_token_program: Pubkey,
|
|
||||||
quote_token_program: Pubkey,
|
|
||||||
fee_recipient: Pubkey,
|
|
||||||
is_cashback_coin: bool,
|
|
||||||
) -> Self {
|
|
||||||
Self::new(
|
|
||||||
pool,
|
|
||||||
base_mint,
|
|
||||||
quote_mint,
|
|
||||||
pool_base_token_account,
|
|
||||||
pool_quote_token_account,
|
|
||||||
pool_base_token_reserves,
|
|
||||||
pool_quote_token_reserves,
|
|
||||||
coin_creator_vault_ata,
|
|
||||||
coin_creator_vault_authority,
|
|
||||||
base_token_program,
|
|
||||||
quote_token_program,
|
|
||||||
fee_recipient,
|
|
||||||
is_cashback_coin,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_mint_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
mint: &Pubkey,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
if let Ok((pool_address, _)) =
|
|
||||||
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
|
||||||
{
|
|
||||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
|
||||||
} else if let Ok((pool_address, _)) =
|
|
||||||
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
|
||||||
{
|
|
||||||
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
|
||||||
} else {
|
|
||||||
return Err(anyhow::anyhow!("No pool found for mint"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_pool_address_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
pool_address: &Pubkey,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
|
||||||
Self::from_pool_data(rpc, pool_address, &pool_data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build params from an already-decoded Pool, only fetching token balances.
|
|
||||||
///
|
|
||||||
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
|
|
||||||
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
|
|
||||||
/// decoded Pool).
|
|
||||||
pub async fn from_pool_data(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
pool_address: &Pubkey,
|
|
||||||
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
|
||||||
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
|
||||||
let creator = pool_data.coin_creator;
|
|
||||||
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
|
||||||
creator,
|
|
||||||
pool_data.quote_mint,
|
|
||||||
);
|
|
||||||
let coin_creator_vault_authority =
|
|
||||||
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
|
||||||
|
|
||||||
let base_token_program_ata = get_associated_token_address_with_program_id(
|
|
||||||
pool_address,
|
|
||||||
&pool_data.base_mint,
|
|
||||||
&crate::constants::TOKEN_PROGRAM,
|
|
||||||
);
|
|
||||||
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
|
||||||
pool_address,
|
|
||||||
&pool_data.quote_mint,
|
|
||||||
&crate::constants::TOKEN_PROGRAM,
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
pool: *pool_address,
|
|
||||||
base_mint: pool_data.base_mint,
|
|
||||||
quote_mint: pool_data.quote_mint,
|
|
||||||
pool_base_token_account: pool_data.pool_base_token_account,
|
|
||||||
pool_quote_token_account: pool_data.pool_quote_token_account,
|
|
||||||
pool_base_token_reserves,
|
|
||||||
pool_quote_token_reserves,
|
|
||||||
coin_creator_vault_ata,
|
|
||||||
coin_creator_vault_authority,
|
|
||||||
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
|
||||||
crate::constants::TOKEN_PROGRAM
|
|
||||||
} else {
|
|
||||||
crate::constants::TOKEN_PROGRAM_2022
|
|
||||||
},
|
|
||||||
is_cashback_coin: pool_data.is_cashback_coin,
|
|
||||||
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
|
||||||
crate::constants::TOKEN_PROGRAM
|
|
||||||
} else {
|
|
||||||
crate::constants::TOKEN_PROGRAM_2022
|
|
||||||
},
|
|
||||||
is_mayhem_mode: pool_data.is_mayhem_mode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Bonk protocol specific parameters
|
|
||||||
/// Configuration parameters specific to Bonk trading protocol
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct BonkParams {
|
|
||||||
pub virtual_base: u128,
|
|
||||||
pub virtual_quote: u128,
|
|
||||||
pub real_base: u128,
|
|
||||||
pub real_quote: u128,
|
|
||||||
pub pool_state: Pubkey,
|
|
||||||
pub base_vault: Pubkey,
|
|
||||||
pub quote_vault: Pubkey,
|
|
||||||
/// Token program ID
|
|
||||||
pub mint_token_program: Pubkey,
|
|
||||||
pub platform_config: Pubkey,
|
|
||||||
pub platform_associated_account: Pubkey,
|
|
||||||
pub creator_associated_account: Pubkey,
|
|
||||||
pub global_config: Pubkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BonkParams {
|
|
||||||
pub fn immediate_sell(
|
|
||||||
mint_token_program: Pubkey,
|
|
||||||
platform_config: Pubkey,
|
|
||||||
platform_associated_account: Pubkey,
|
|
||||||
creator_associated_account: Pubkey,
|
|
||||||
global_config: Pubkey,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
mint_token_program,
|
|
||||||
platform_config,
|
|
||||||
platform_associated_account,
|
|
||||||
creator_associated_account,
|
|
||||||
global_config,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn from_trade(
|
|
||||||
virtual_base: u64,
|
|
||||||
virtual_quote: u64,
|
|
||||||
real_base_after: u64,
|
|
||||||
real_quote_after: u64,
|
|
||||||
pool_state: Pubkey,
|
|
||||||
base_vault: Pubkey,
|
|
||||||
quote_vault: Pubkey,
|
|
||||||
base_token_program: Pubkey,
|
|
||||||
platform_config: Pubkey,
|
|
||||||
platform_associated_account: Pubkey,
|
|
||||||
creator_associated_account: Pubkey,
|
|
||||||
global_config: Pubkey,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
virtual_base: virtual_base as u128,
|
|
||||||
virtual_quote: virtual_quote as u128,
|
|
||||||
real_base: real_base_after as u128,
|
|
||||||
real_quote: real_quote_after as u128,
|
|
||||||
pool_state: pool_state,
|
|
||||||
base_vault: base_vault,
|
|
||||||
quote_vault: quote_vault,
|
|
||||||
mint_token_program: base_token_program,
|
|
||||||
platform_config: platform_config,
|
|
||||||
platform_associated_account: platform_associated_account,
|
|
||||||
creator_associated_account: creator_associated_account,
|
|
||||||
global_config: global_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_dev_trade(
|
|
||||||
is_exact_in: bool,
|
|
||||||
amount_in: u64,
|
|
||||||
amount_out: u64,
|
|
||||||
pool_state: Pubkey,
|
|
||||||
base_vault: Pubkey,
|
|
||||||
quote_vault: Pubkey,
|
|
||||||
base_token_program: Pubkey,
|
|
||||||
platform_config: Pubkey,
|
|
||||||
platform_associated_account: Pubkey,
|
|
||||||
creator_associated_account: Pubkey,
|
|
||||||
global_config: Pubkey,
|
|
||||||
) -> Self {
|
|
||||||
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
|
||||||
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
|
||||||
let _amount_in = if is_exact_in {
|
|
||||||
amount_in
|
|
||||||
} else {
|
|
||||||
crate::instruction::utils::bonk::get_amount_in(
|
|
||||||
amount_out,
|
|
||||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
|
||||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
|
||||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
|
||||||
DEFAULT_VIRTUAL_BASE,
|
|
||||||
DEFAULT_VIRTUAL_QUOTE,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
|
||||||
amount_in,
|
|
||||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
|
||||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
|
||||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
|
||||||
) as u128;
|
|
||||||
let _amount_out = if is_exact_in {
|
|
||||||
crate::instruction::utils::bonk::get_amount_out(
|
|
||||||
amount_in,
|
|
||||||
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
|
||||||
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
|
||||||
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
|
||||||
DEFAULT_VIRTUAL_BASE,
|
|
||||||
DEFAULT_VIRTUAL_QUOTE,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
) as u128
|
|
||||||
} else {
|
|
||||||
amount_out as u128
|
|
||||||
};
|
|
||||||
let real_base = _amount_out;
|
|
||||||
Self {
|
|
||||||
virtual_base: DEFAULT_VIRTUAL_BASE,
|
|
||||||
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
|
||||||
real_base: real_base,
|
|
||||||
real_quote: real_quote,
|
|
||||||
pool_state: pool_state,
|
|
||||||
base_vault: base_vault,
|
|
||||||
quote_vault: quote_vault,
|
|
||||||
mint_token_program: base_token_program,
|
|
||||||
platform_config: platform_config,
|
|
||||||
platform_associated_account: platform_associated_account,
|
|
||||||
creator_associated_account: creator_associated_account,
|
|
||||||
global_config: global_config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_mint_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
mint: &Pubkey,
|
|
||||||
usd1_pool: bool,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
|
||||||
mint,
|
|
||||||
if usd1_pool {
|
|
||||||
&crate::constants::USD1_TOKEN_ACCOUNT
|
|
||||||
} else {
|
|
||||||
&crate::constants::WSOL_TOKEN_ACCOUNT
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let pool_data =
|
|
||||||
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
|
||||||
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
|
||||||
let platform_associated_account =
|
|
||||||
crate::instruction::utils::bonk::get_platform_associated_account(
|
|
||||||
&pool_data.platform_config,
|
|
||||||
);
|
|
||||||
let creator_associated_account =
|
|
||||||
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
|
||||||
let platform_associated_account = platform_associated_account.unwrap();
|
|
||||||
let creator_associated_account = creator_associated_account.unwrap();
|
|
||||||
Ok(Self {
|
|
||||||
virtual_base: pool_data.virtual_base as u128,
|
|
||||||
virtual_quote: pool_data.virtual_quote as u128,
|
|
||||||
real_base: pool_data.real_base as u128,
|
|
||||||
real_quote: pool_data.real_quote as u128,
|
|
||||||
pool_state: pool_address,
|
|
||||||
base_vault: pool_data.base_vault,
|
|
||||||
quote_vault: pool_data.quote_vault,
|
|
||||||
mint_token_program: token_account.owner,
|
|
||||||
platform_config: pool_data.platform_config,
|
|
||||||
platform_associated_account,
|
|
||||||
creator_associated_account,
|
|
||||||
global_config: pool_data.global_config,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RaydiumCpmm protocol specific parameters
|
|
||||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RaydiumCpmmParams {
|
|
||||||
/// Pool address
|
|
||||||
pub pool_state: Pubkey,
|
|
||||||
/// Amm config address
|
|
||||||
pub amm_config: Pubkey,
|
|
||||||
/// Base token mint address
|
|
||||||
pub base_mint: Pubkey,
|
|
||||||
/// Quote token mint address
|
|
||||||
pub quote_mint: Pubkey,
|
|
||||||
/// Base token reserve amount in the pool
|
|
||||||
pub base_reserve: u64,
|
|
||||||
/// Quote token reserve amount in the pool
|
|
||||||
pub quote_reserve: u64,
|
|
||||||
/// Base token vault address
|
|
||||||
pub base_vault: Pubkey,
|
|
||||||
/// Quote token vault address
|
|
||||||
pub quote_vault: Pubkey,
|
|
||||||
/// Base token program ID
|
|
||||||
pub base_token_program: Pubkey,
|
|
||||||
/// Quote token program ID
|
|
||||||
pub quote_token_program: Pubkey,
|
|
||||||
/// Observation state account
|
|
||||||
pub observation_state: Pubkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RaydiumCpmmParams {
|
|
||||||
pub fn from_trade(
|
|
||||||
pool_state: Pubkey,
|
|
||||||
amm_config: Pubkey,
|
|
||||||
input_token_mint: Pubkey,
|
|
||||||
output_token_mint: Pubkey,
|
|
||||||
input_vault: Pubkey,
|
|
||||||
output_vault: Pubkey,
|
|
||||||
input_token_program: Pubkey,
|
|
||||||
output_token_program: Pubkey,
|
|
||||||
observation_state: Pubkey,
|
|
||||||
base_reserve: u64,
|
|
||||||
quote_reserve: u64,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
pool_state: pool_state,
|
|
||||||
amm_config: amm_config,
|
|
||||||
base_mint: input_token_mint,
|
|
||||||
quote_mint: output_token_mint,
|
|
||||||
base_reserve: base_reserve,
|
|
||||||
quote_reserve: quote_reserve,
|
|
||||||
base_vault: input_vault,
|
|
||||||
quote_vault: output_vault,
|
|
||||||
base_token_program: input_token_program,
|
|
||||||
quote_token_program: output_token_program,
|
|
||||||
observation_state: observation_state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_pool_address_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
pool_address: &Pubkey,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let pool =
|
|
||||||
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
|
||||||
let (token0_balance, token1_balance) =
|
|
||||||
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
|
||||||
rpc,
|
|
||||||
pool_address,
|
|
||||||
&pool.token0_mint,
|
|
||||||
&pool.token1_mint,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
Ok(Self {
|
|
||||||
pool_state: *pool_address,
|
|
||||||
amm_config: pool.amm_config,
|
|
||||||
base_mint: pool.token0_mint,
|
|
||||||
quote_mint: pool.token1_mint,
|
|
||||||
base_reserve: token0_balance,
|
|
||||||
quote_reserve: token1_balance,
|
|
||||||
base_vault: pool.token0_vault,
|
|
||||||
quote_vault: pool.token1_vault,
|
|
||||||
base_token_program: pool.token0_program,
|
|
||||||
quote_token_program: pool.token1_program,
|
|
||||||
observation_state: pool.observation_key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RaydiumCpmm protocol specific parameters
|
|
||||||
/// Configuration parameters specific to Raydium CPMM trading protocol
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct RaydiumAmmV4Params {
|
|
||||||
/// AMM pool address
|
|
||||||
pub amm: Pubkey,
|
|
||||||
/// Base token (coin) mint address
|
|
||||||
pub coin_mint: Pubkey,
|
|
||||||
/// Quote token (pc) mint address
|
|
||||||
pub pc_mint: Pubkey,
|
|
||||||
/// Pool's coin token account address
|
|
||||||
pub token_coin: Pubkey,
|
|
||||||
/// Pool's pc token account address
|
|
||||||
pub token_pc: Pubkey,
|
|
||||||
/// Current coin reserve amount in the pool
|
|
||||||
pub coin_reserve: u64,
|
|
||||||
/// Current pc reserve amount in the pool
|
|
||||||
pub pc_reserve: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RaydiumAmmV4Params {
|
|
||||||
pub fn new(
|
|
||||||
amm: Pubkey,
|
|
||||||
coin_mint: Pubkey,
|
|
||||||
pc_mint: Pubkey,
|
|
||||||
token_coin: Pubkey,
|
|
||||||
token_pc: Pubkey,
|
|
||||||
coin_reserve: u64,
|
|
||||||
pc_reserve: u64,
|
|
||||||
) -> Self {
|
|
||||||
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
|
||||||
}
|
|
||||||
pub async fn from_amm_address_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
amm: Pubkey,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
|
||||||
let (coin_reserve, pc_reserve) =
|
|
||||||
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
|
||||||
Ok(Self {
|
|
||||||
amm,
|
|
||||||
coin_mint: amm_info.coin_mint,
|
|
||||||
pc_mint: amm_info.pc_mint,
|
|
||||||
token_coin: amm_info.token_coin,
|
|
||||||
token_pc: amm_info.token_pc,
|
|
||||||
coin_reserve,
|
|
||||||
pc_reserve,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MeteoraDammV2 protocol specific parameters
|
|
||||||
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct MeteoraDammV2Params {
|
|
||||||
pub pool: Pubkey,
|
|
||||||
pub token_a_vault: Pubkey,
|
|
||||||
pub token_b_vault: Pubkey,
|
|
||||||
pub token_a_mint: Pubkey,
|
|
||||||
pub token_b_mint: Pubkey,
|
|
||||||
pub token_a_program: Pubkey,
|
|
||||||
pub token_b_program: Pubkey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MeteoraDammV2Params {
|
|
||||||
pub fn new(
|
|
||||||
pool: Pubkey,
|
|
||||||
token_a_vault: Pubkey,
|
|
||||||
token_b_vault: Pubkey,
|
|
||||||
token_a_mint: Pubkey,
|
|
||||||
token_b_mint: Pubkey,
|
|
||||||
token_a_program: Pubkey,
|
|
||||||
token_b_program: Pubkey,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
pool,
|
|
||||||
token_a_vault,
|
|
||||||
token_b_vault,
|
|
||||||
token_a_mint,
|
|
||||||
token_b_mint,
|
|
||||||
token_a_program,
|
|
||||||
token_b_program,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn from_pool_address_by_rpc(
|
|
||||||
rpc: &SolanaRpcClient,
|
|
||||||
pool_address: &Pubkey,
|
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let pool_data =
|
|
||||||
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
|
||||||
Ok(Self {
|
|
||||||
pool: *pool_address,
|
|
||||||
token_a_vault: pool_data.token_a_vault,
|
|
||||||
token_b_vault: pool_data.token_b_vault,
|
|
||||||
token_a_mint: pool_data.token_a_mint,
|
|
||||||
token_b_mint: pool_data.token_b_mint,
|
|
||||||
token_a_program: TOKEN_PROGRAM,
|
|
||||||
token_b_program: TOKEN_PROGRAM,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
use crate::common::SolanaRpcClient;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// Bonk protocol specific parameters
|
||||||
|
/// Configuration parameters specific to Bonk trading protocol
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct BonkParams {
|
||||||
|
pub virtual_base: u128,
|
||||||
|
pub virtual_quote: u128,
|
||||||
|
pub real_base: u128,
|
||||||
|
pub real_quote: u128,
|
||||||
|
pub pool_state: Pubkey,
|
||||||
|
pub base_vault: Pubkey,
|
||||||
|
pub quote_vault: Pubkey,
|
||||||
|
/// Token program ID
|
||||||
|
pub mint_token_program: Pubkey,
|
||||||
|
pub platform_config: Pubkey,
|
||||||
|
pub platform_associated_account: Pubkey,
|
||||||
|
pub creator_associated_account: Pubkey,
|
||||||
|
pub global_config: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BonkParams {
|
||||||
|
pub fn immediate_sell(
|
||||||
|
mint_token_program: Pubkey,
|
||||||
|
platform_config: Pubkey,
|
||||||
|
platform_associated_account: Pubkey,
|
||||||
|
creator_associated_account: Pubkey,
|
||||||
|
global_config: Pubkey,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
mint_token_program,
|
||||||
|
platform_config,
|
||||||
|
platform_associated_account,
|
||||||
|
creator_associated_account,
|
||||||
|
global_config,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_trade(
|
||||||
|
virtual_base: u64,
|
||||||
|
virtual_quote: u64,
|
||||||
|
real_base_after: u64,
|
||||||
|
real_quote_after: u64,
|
||||||
|
pool_state: Pubkey,
|
||||||
|
base_vault: Pubkey,
|
||||||
|
quote_vault: Pubkey,
|
||||||
|
base_token_program: Pubkey,
|
||||||
|
platform_config: Pubkey,
|
||||||
|
platform_associated_account: Pubkey,
|
||||||
|
creator_associated_account: Pubkey,
|
||||||
|
global_config: Pubkey,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
virtual_base: virtual_base as u128,
|
||||||
|
virtual_quote: virtual_quote as u128,
|
||||||
|
real_base: real_base_after as u128,
|
||||||
|
real_quote: real_quote_after as u128,
|
||||||
|
pool_state: pool_state,
|
||||||
|
base_vault: base_vault,
|
||||||
|
quote_vault: quote_vault,
|
||||||
|
mint_token_program: base_token_program,
|
||||||
|
platform_config: platform_config,
|
||||||
|
platform_associated_account: platform_associated_account,
|
||||||
|
creator_associated_account: creator_associated_account,
|
||||||
|
global_config: global_config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_dev_trade(
|
||||||
|
is_exact_in: bool,
|
||||||
|
amount_in: u64,
|
||||||
|
amount_out: u64,
|
||||||
|
pool_state: Pubkey,
|
||||||
|
base_vault: Pubkey,
|
||||||
|
quote_vault: Pubkey,
|
||||||
|
base_token_program: Pubkey,
|
||||||
|
platform_config: Pubkey,
|
||||||
|
platform_associated_account: Pubkey,
|
||||||
|
creator_associated_account: Pubkey,
|
||||||
|
global_config: Pubkey,
|
||||||
|
) -> Self {
|
||||||
|
const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382;
|
||||||
|
const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951;
|
||||||
|
let _amount_in = if is_exact_in {
|
||||||
|
amount_in
|
||||||
|
} else {
|
||||||
|
crate::instruction::utils::bonk::get_amount_in(
|
||||||
|
amount_out,
|
||||||
|
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||||
|
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||||
|
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||||
|
DEFAULT_VIRTUAL_BASE,
|
||||||
|
DEFAULT_VIRTUAL_QUOTE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let real_quote = crate::instruction::utils::bonk::get_amount_in_net(
|
||||||
|
amount_in,
|
||||||
|
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||||
|
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||||
|
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||||
|
) as u128;
|
||||||
|
let _amount_out = if is_exact_in {
|
||||||
|
crate::instruction::utils::bonk::get_amount_out(
|
||||||
|
amount_in,
|
||||||
|
crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE,
|
||||||
|
crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE,
|
||||||
|
crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE,
|
||||||
|
DEFAULT_VIRTUAL_BASE,
|
||||||
|
DEFAULT_VIRTUAL_QUOTE,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
) as u128
|
||||||
|
} else {
|
||||||
|
amount_out as u128
|
||||||
|
};
|
||||||
|
let real_base = _amount_out;
|
||||||
|
Self {
|
||||||
|
virtual_base: DEFAULT_VIRTUAL_BASE,
|
||||||
|
virtual_quote: DEFAULT_VIRTUAL_QUOTE,
|
||||||
|
real_base: real_base,
|
||||||
|
real_quote: real_quote,
|
||||||
|
pool_state: pool_state,
|
||||||
|
base_vault: base_vault,
|
||||||
|
quote_vault: quote_vault,
|
||||||
|
mint_token_program: base_token_program,
|
||||||
|
platform_config: platform_config,
|
||||||
|
platform_associated_account: platform_associated_account,
|
||||||
|
creator_associated_account: creator_associated_account,
|
||||||
|
global_config: global_config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_mint_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
mint: &Pubkey,
|
||||||
|
usd1_pool: bool,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let pool_address = crate::instruction::utils::bonk::get_pool_pda(
|
||||||
|
mint,
|
||||||
|
if usd1_pool {
|
||||||
|
&crate::constants::USD1_TOKEN_ACCOUNT
|
||||||
|
} else {
|
||||||
|
&crate::constants::WSOL_TOKEN_ACCOUNT
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let pool_data =
|
||||||
|
crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?;
|
||||||
|
let token_account = rpc.get_account(&pool_data.base_mint).await?;
|
||||||
|
let platform_associated_account =
|
||||||
|
crate::instruction::utils::bonk::get_platform_associated_account(
|
||||||
|
&pool_data.platform_config,
|
||||||
|
);
|
||||||
|
let creator_associated_account =
|
||||||
|
crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator);
|
||||||
|
let platform_associated_account = platform_associated_account.unwrap();
|
||||||
|
let creator_associated_account = creator_associated_account.unwrap();
|
||||||
|
Ok(Self {
|
||||||
|
virtual_base: pool_data.virtual_base as u128,
|
||||||
|
virtual_quote: pool_data.virtual_quote as u128,
|
||||||
|
real_base: pool_data.real_base as u128,
|
||||||
|
real_quote: pool_data.real_quote as u128,
|
||||||
|
pool_state: pool_address,
|
||||||
|
base_vault: pool_data.base_vault,
|
||||||
|
quote_vault: pool_data.quote_vault,
|
||||||
|
mint_token_program: token_account.owner,
|
||||||
|
platform_config: pool_data.platform_config,
|
||||||
|
platform_associated_account,
|
||||||
|
creator_associated_account,
|
||||||
|
global_config: pool_data.global_config,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
use crate::common::nonce_cache::DurableNonceInfo;
|
||||||
|
use crate::common::{GasFeeStrategy, SolanaRpcClient};
|
||||||
|
use crate::swqos::{SwqosClient, TradeType};
|
||||||
|
use crate::trading::MiddlewareManager;
|
||||||
|
use core_affinity::CoreId;
|
||||||
|
use solana_hash::Hash;
|
||||||
|
use solana_message::AddressLookupTableAccount;
|
||||||
|
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use super::bonk::BonkParams;
|
||||||
|
use super::meteora_damm_v2::MeteoraDammV2Params;
|
||||||
|
use super::pumpfun::PumpFunParams;
|
||||||
|
use super::pumpswap::PumpSwapParams;
|
||||||
|
use super::raydium_amm_v4::RaydiumAmmV4Params;
|
||||||
|
use super::raydium_cpmm::RaydiumCpmmParams;
|
||||||
|
|
||||||
|
/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SenderConcurrencyConfig {
|
||||||
|
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||||
|
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||||
|
pub max_sender_concurrency: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// DEX 参数枚举 - 零开销抽象替代 Box<dyn ProtocolParams>
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum DexParamEnum {
|
||||||
|
PumpFun(PumpFunParams),
|
||||||
|
PumpSwap(PumpSwapParams),
|
||||||
|
Bonk(BonkParams),
|
||||||
|
RaydiumCpmm(RaydiumCpmmParams),
|
||||||
|
RaydiumAmmV4(RaydiumAmmV4Params),
|
||||||
|
MeteoraDammV2(MeteoraDammV2Params),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DexParamEnum {
|
||||||
|
/// 获取内部参数的 Any 引用,用于向后兼容的类型检查
|
||||||
|
#[inline]
|
||||||
|
pub fn as_any(&self) -> &dyn std::any::Any {
|
||||||
|
match self {
|
||||||
|
DexParamEnum::PumpFun(p) => p,
|
||||||
|
DexParamEnum::PumpSwap(p) => p,
|
||||||
|
DexParamEnum::Bonk(p) => p,
|
||||||
|
DexParamEnum::RaydiumCpmm(p) => p,
|
||||||
|
DexParamEnum::RaydiumAmmV4(p) => p,
|
||||||
|
DexParamEnum::MeteoraDammV2(p) => p,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap parameters
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SwapParams {
|
||||||
|
pub rpc: Option<Arc<SolanaRpcClient>>,
|
||||||
|
pub payer: Arc<Keypair>,
|
||||||
|
pub trade_type: TradeType,
|
||||||
|
pub input_mint: Pubkey,
|
||||||
|
pub input_token_program: Option<Pubkey>,
|
||||||
|
pub output_mint: Pubkey,
|
||||||
|
pub output_token_program: Option<Pubkey>,
|
||||||
|
pub input_amount: Option<u64>,
|
||||||
|
pub slippage_basis_points: Option<u64>,
|
||||||
|
pub address_lookup_table_account: Option<AddressLookupTableAccount>,
|
||||||
|
pub recent_blockhash: Option<Hash>,
|
||||||
|
pub wait_tx_confirmed: bool,
|
||||||
|
pub protocol_params: DexParamEnum,
|
||||||
|
pub open_seed_optimize: bool,
|
||||||
|
/// Arc<Vec<..>> so cloning from infrastructure is a single Arc clone.
|
||||||
|
pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
|
||||||
|
pub middleware_manager: Option<Arc<MiddlewareManager>>,
|
||||||
|
pub durable_nonce: Option<DurableNonceInfo>,
|
||||||
|
pub with_tip: bool,
|
||||||
|
pub create_input_mint_ata: bool,
|
||||||
|
pub close_input_mint_ata: bool,
|
||||||
|
pub create_output_mint_ata: bool,
|
||||||
|
pub close_output_mint_ata: bool,
|
||||||
|
pub fixed_output_amount: Option<u64>,
|
||||||
|
pub gas_fee_strategy: GasFeeStrategy,
|
||||||
|
pub simulate: bool,
|
||||||
|
/// Whether to output SDK logs (from TradeConfig.log_enabled).
|
||||||
|
pub log_enabled: bool,
|
||||||
|
/// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()).
|
||||||
|
pub use_dedicated_sender_threads: bool,
|
||||||
|
/// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path.
|
||||||
|
pub sender_thread_cores: Option<Arc<Vec<usize>>>,
|
||||||
|
/// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path.
|
||||||
|
pub max_sender_concurrency: usize,
|
||||||
|
/// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only.
|
||||||
|
pub effective_core_ids: Arc<Vec<CoreId>>,
|
||||||
|
/// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency.
|
||||||
|
pub check_min_tip: bool,
|
||||||
|
/// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled.
|
||||||
|
pub grpc_recv_us: Option<i64>,
|
||||||
|
/// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap).
|
||||||
|
/// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
|
||||||
|
/// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
|
||||||
|
/// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
|
||||||
|
pub use_exact_sol_amount: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SwapParams {
|
||||||
|
/// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only.
|
||||||
|
#[inline]
|
||||||
|
pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig {
|
||||||
|
SenderConcurrencyConfig {
|
||||||
|
sender_thread_cores: self.sender_thread_cores.clone(),
|
||||||
|
effective_core_ids: self.effective_core_ids.clone(),
|
||||||
|
max_sender_concurrency: self.max_sender_concurrency,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SwapParams {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "SwapParams: ...")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
use crate::common::SolanaRpcClient;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// MeteoraDammV2 protocol specific parameters
|
||||||
|
/// Configuration parameters specific to Meteora Damm V2 trading protocol
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct MeteoraDammV2Params {
|
||||||
|
pub pool: Pubkey,
|
||||||
|
pub token_a_vault: Pubkey,
|
||||||
|
pub token_b_vault: Pubkey,
|
||||||
|
pub token_a_mint: Pubkey,
|
||||||
|
pub token_b_mint: Pubkey,
|
||||||
|
pub token_a_program: Pubkey,
|
||||||
|
pub token_b_program: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeteoraDammV2Params {
|
||||||
|
pub fn new(
|
||||||
|
pool: Pubkey,
|
||||||
|
token_a_vault: Pubkey,
|
||||||
|
token_b_vault: Pubkey,
|
||||||
|
token_a_mint: Pubkey,
|
||||||
|
token_b_mint: Pubkey,
|
||||||
|
token_a_program: Pubkey,
|
||||||
|
token_b_program: Pubkey,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
pool,
|
||||||
|
token_a_vault,
|
||||||
|
token_b_vault,
|
||||||
|
token_a_mint,
|
||||||
|
token_b_mint,
|
||||||
|
token_a_program,
|
||||||
|
token_b_program,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_pool_address_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
pool_address: &Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let pool_data =
|
||||||
|
crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?;
|
||||||
|
let mint_accounts = rpc
|
||||||
|
.get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint])
|
||||||
|
.await?;
|
||||||
|
let token_a_program = mint_accounts
|
||||||
|
.get(0)
|
||||||
|
.and_then(|a| a.as_ref())
|
||||||
|
.map(|a| a.owner)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?;
|
||||||
|
let token_b_program = mint_accounts
|
||||||
|
.get(1)
|
||||||
|
.and_then(|a| a.as_ref())
|
||||||
|
.map(|a| a.owner)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?;
|
||||||
|
Ok(Self {
|
||||||
|
pool: *pool_address,
|
||||||
|
token_a_vault: pool_data.token_a_vault,
|
||||||
|
token_b_vault: pool_data.token_b_vault,
|
||||||
|
token_a_mint: pool_data.token_a_mint,
|
||||||
|
token_b_mint: pool_data.token_b_mint,
|
||||||
|
token_a_program,
|
||||||
|
token_b_program,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
//! DEX protocol parameter types and [`SwapParams`].
|
||||||
|
|
||||||
|
mod bonk;
|
||||||
|
mod dex_swap;
|
||||||
|
mod meteora_damm_v2;
|
||||||
|
mod pumpfun;
|
||||||
|
mod pumpswap;
|
||||||
|
mod raydium_amm_v4;
|
||||||
|
mod raydium_cpmm;
|
||||||
|
|
||||||
|
pub use bonk::BonkParams;
|
||||||
|
pub use dex_swap::{DexParamEnum, SenderConcurrencyConfig, SwapParams};
|
||||||
|
pub use meteora_damm_v2::MeteoraDammV2Params;
|
||||||
|
pub use pumpfun::PumpFunParams;
|
||||||
|
pub use pumpswap::PumpSwapParams;
|
||||||
|
pub use raydium_amm_v4::RaydiumAmmV4Params;
|
||||||
|
pub use raydium_cpmm::RaydiumCpmmParams;
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
use crate::common::bonding_curve::BondingCurveAccount;
|
||||||
|
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||||
|
use crate::common::SolanaRpcClient;
|
||||||
|
use crate::instruction::utils::pumpfun::reconcile_mayhem_mode_for_trade;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
||||||
|
/// PumpFun protocol specific parameters
|
||||||
|
/// Configuration parameters specific to PumpFun trading protocol.
|
||||||
|
///
|
||||||
|
/// **Creator vault**: Pump buy/sell pass `creator_vault` = `PDA(["creator-vault", authority])`.
|
||||||
|
/// Usually `authority` is [`BondingCurveAccount::creator`]; with **Creator Rewards Sharing** it is
|
||||||
|
/// `fee_sharing_config_pda(mint)` (see [`fetch_fee_sharing_creator_vault_if_active`](crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active)).
|
||||||
|
/// Keep `bonding_curve.creator` in sync with chain; ix building uses [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing).
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PumpFunParams {
|
||||||
|
pub bonding_curve: Arc<BondingCurveAccount>,
|
||||||
|
pub associated_bonding_curve: Pubkey,
|
||||||
|
/// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing): ix vault when it matches `PDA(creator)`, fee-sharing vault, or RPC hint.
|
||||||
|
pub creator_vault: Pubkey,
|
||||||
|
/// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc).
|
||||||
|
pub fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||||
|
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 {
|
||||||
|
pub fn immediate_sell(
|
||||||
|
creator_vault: Pubkey,
|
||||||
|
token_program: Pubkey,
|
||||||
|
close_token_account_when_sell: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }),
|
||||||
|
associated_bonding_curve: Pubkey::default(),
|
||||||
|
creator_vault: creator_vault,
|
||||||
|
fee_sharing_creator_vault_if_active: None,
|
||||||
|
token_program: token_program,
|
||||||
|
close_token_account_when_sell: Some(close_token_account_when_sell),
|
||||||
|
fee_recipient: Pubkey::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||||
|
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||||
|
/// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`).
|
||||||
|
/// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts).
|
||||||
|
pub fn from_dev_trade(
|
||||||
|
mint: Pubkey,
|
||||||
|
token_amount: u64,
|
||||||
|
max_sol_cost: u64,
|
||||||
|
creator: Pubkey,
|
||||||
|
bonding_curve: Pubkey,
|
||||||
|
associated_bonding_curve: Pubkey,
|
||||||
|
creator_vault: Pubkey,
|
||||||
|
close_token_account_when_sell: Option<bool>,
|
||||||
|
fee_recipient: Pubkey,
|
||||||
|
token_program: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
|
mayhem_mode: Option<bool>,
|
||||||
|
) -> Self {
|
||||||
|
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
|
||||||
|
let bonding_curve_account = BondingCurveAccount::from_dev_trade(
|
||||||
|
bonding_curve,
|
||||||
|
&mint,
|
||||||
|
token_amount,
|
||||||
|
max_sol_cost,
|
||||||
|
creator,
|
||||||
|
is_mayhem_mode,
|
||||||
|
is_cashback_coin,
|
||||||
|
);
|
||||||
|
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||||
|
&bonding_curve_account.creator,
|
||||||
|
creator_vault,
|
||||||
|
&mint,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.or_else(|| {
|
||||||
|
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
Self {
|
||||||
|
bonding_curve: Arc::new(bonding_curve_account),
|
||||||
|
associated_bonding_curve: associated_bonding_curve,
|
||||||
|
creator_vault: creator_vault_resolved,
|
||||||
|
fee_sharing_creator_vault_if_active: None,
|
||||||
|
close_token_account_when_sell: close_token_account_when_sell,
|
||||||
|
token_program: token_program,
|
||||||
|
fee_recipient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event
|
||||||
|
/// so that sell instructions include the correct remaining accounts for cashback.
|
||||||
|
///
|
||||||
|
/// `mayhem_mode`:
|
||||||
|
/// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**(Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。
|
||||||
|
/// - **`None`**:用 `fee_recipient` 是否落在 Mayhem 静态列表推断。
|
||||||
|
pub fn from_trade(
|
||||||
|
bonding_curve: Pubkey,
|
||||||
|
associated_bonding_curve: Pubkey,
|
||||||
|
mint: Pubkey,
|
||||||
|
creator: Pubkey,
|
||||||
|
creator_vault: Pubkey,
|
||||||
|
virtual_token_reserves: u64,
|
||||||
|
virtual_sol_reserves: u64,
|
||||||
|
real_token_reserves: u64,
|
||||||
|
real_sol_reserves: u64,
|
||||||
|
close_token_account_when_sell: Option<bool>,
|
||||||
|
fee_recipient: Pubkey,
|
||||||
|
token_program: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
|
mayhem_mode: Option<bool>,
|
||||||
|
) -> Self {
|
||||||
|
let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient);
|
||||||
|
let bonding_curve = BondingCurveAccount::from_trade(
|
||||||
|
bonding_curve,
|
||||||
|
mint,
|
||||||
|
creator,
|
||||||
|
virtual_token_reserves,
|
||||||
|
virtual_sol_reserves,
|
||||||
|
real_token_reserves,
|
||||||
|
real_sol_reserves,
|
||||||
|
is_mayhem_mode,
|
||||||
|
is_cashback_coin,
|
||||||
|
);
|
||||||
|
let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||||
|
&bonding_curve.creator,
|
||||||
|
creator_vault,
|
||||||
|
&mint,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.or_else(|| {
|
||||||
|
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
Self {
|
||||||
|
bonding_curve: Arc::new(bonding_curve),
|
||||||
|
associated_bonding_curve: associated_bonding_curve,
|
||||||
|
creator_vault: creator_vault_resolved,
|
||||||
|
fee_sharing_creator_vault_if_active: None,
|
||||||
|
close_token_account_when_sell: close_token_account_when_sell,
|
||||||
|
token_program: token_program,
|
||||||
|
fee_recipient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_mint_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
mint: &Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let account =
|
||||||
|
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?;
|
||||||
|
let mint_account = rpc.get_account(&mint).await?;
|
||||||
|
let bonding_curve = BondingCurveAccount {
|
||||||
|
discriminator: 0,
|
||||||
|
account: account.1,
|
||||||
|
virtual_token_reserves: account.0.virtual_token_reserves,
|
||||||
|
virtual_sol_reserves: account.0.virtual_sol_reserves,
|
||||||
|
real_token_reserves: account.0.real_token_reserves,
|
||||||
|
real_sol_reserves: account.0.real_sol_reserves,
|
||||||
|
token_total_supply: account.0.token_total_supply,
|
||||||
|
complete: account.0.complete,
|
||||||
|
creator: account.0.creator,
|
||||||
|
is_mayhem_mode: account.0.is_mayhem_mode,
|
||||||
|
is_cashback_coin: account.0.is_cashback_coin,
|
||||||
|
};
|
||||||
|
let associated_bonding_curve = get_associated_token_address_with_program_id(
|
||||||
|
&bonding_curve.account,
|
||||||
|
mint,
|
||||||
|
&mint_account.owner,
|
||||||
|
);
|
||||||
|
let fee_sharing_creator_vault_if_active =
|
||||||
|
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
|
||||||
|
.await?;
|
||||||
|
let creator_vault = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||||
|
&bonding_curve.creator,
|
||||||
|
Pubkey::default(),
|
||||||
|
mint,
|
||||||
|
fee_sharing_creator_vault_if_active,
|
||||||
|
)
|
||||||
|
.or_else(|| crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator))
|
||||||
|
.unwrap_or_default();
|
||||||
|
Ok(Self {
|
||||||
|
bonding_curve: Arc::new(bonding_curve),
|
||||||
|
associated_bonding_curve: associated_bonding_curve,
|
||||||
|
creator_vault,
|
||||||
|
fee_sharing_creator_vault_if_active,
|
||||||
|
close_token_account_when_sell: None,
|
||||||
|
token_program: mint_account.owner,
|
||||||
|
fee_recipient: Pubkey::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `getAccount` on pump-fees `SharingConfig` + re-resolves [`Self::creator_vault`]. Call before sell
|
||||||
|
/// when params come from gRPC/cache so migrated fee-sharing mints do not hit Anchor 2006.
|
||||||
|
pub async fn refresh_fee_sharing_creator_vault_from_rpc(
|
||||||
|
mut self,
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
mint: &Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
self.fee_sharing_creator_vault_if_active =
|
||||||
|
crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint)
|
||||||
|
.await?;
|
||||||
|
let c = self.bonding_curve.creator;
|
||||||
|
if let Some(v) =
|
||||||
|
crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing(
|
||||||
|
&c,
|
||||||
|
self.creator_vault,
|
||||||
|
mint,
|
||||||
|
self.fee_sharing_creator_vault_if_active,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.creator_vault = v;
|
||||||
|
}
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Override fee-sharing vault hint (e.g. from an off-chain indexer). `None` clears the hint.
|
||||||
|
#[inline]
|
||||||
|
pub fn with_fee_sharing_creator_vault_if_active(
|
||||||
|
mut self,
|
||||||
|
fee_sharing_creator_vault_if_active: Option<Pubkey>,
|
||||||
|
) -> Self {
|
||||||
|
self.fee_sharing_creator_vault_if_active = fee_sharing_creator_vault_if_active;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
|
||||||
|
use crate::common::SolanaRpcClient;
|
||||||
|
use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// PumpSwap Protocol Specific Parameters
|
||||||
|
///
|
||||||
|
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
|
||||||
|
/// token configuration, and transaction amounts.
|
||||||
|
///
|
||||||
|
/// **Performance Note**: If these parameters are not provided, the system will attempt to
|
||||||
|
/// retrieve the relevant information from RPC, which will increase transaction time.
|
||||||
|
/// For optimal performance, it is recommended to provide all necessary parameters in advance.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PumpSwapParams {
|
||||||
|
/// Liquidity pool address
|
||||||
|
pub pool: Pubkey,
|
||||||
|
/// Base token mint address
|
||||||
|
/// The mint account address of the base token in the trading pair
|
||||||
|
pub base_mint: Pubkey,
|
||||||
|
/// Quote token mint address
|
||||||
|
/// The mint account address of the quote token in the trading pair, usually SOL or USDC
|
||||||
|
pub quote_mint: Pubkey,
|
||||||
|
/// Pool base token account
|
||||||
|
pub pool_base_token_account: Pubkey,
|
||||||
|
/// Pool quote token account
|
||||||
|
pub pool_quote_token_account: Pubkey,
|
||||||
|
/// Base token reserves in the pool
|
||||||
|
pub pool_base_token_reserves: u64,
|
||||||
|
/// Quote token reserves in the pool
|
||||||
|
pub pool_quote_token_reserves: u64,
|
||||||
|
/// Coin creator vault ATA
|
||||||
|
pub coin_creator_vault_ata: Pubkey,
|
||||||
|
/// Coin creator vault authority
|
||||||
|
pub coin_creator_vault_authority: Pubkey,
|
||||||
|
/// Token program ID
|
||||||
|
pub base_token_program: Pubkey,
|
||||||
|
/// Quote token program ID
|
||||||
|
pub quote_token_program: Pubkey,
|
||||||
|
/// Whether the pool is in mayhem mode
|
||||||
|
pub is_mayhem_mode: bool,
|
||||||
|
/// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap
|
||||||
|
/// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()`
|
||||||
|
/// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053).
|
||||||
|
pub coin_creator: Pubkey,
|
||||||
|
/// Whether the pool's coin has cashback enabled
|
||||||
|
pub is_cashback_coin: bool,
|
||||||
|
/// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell
|
||||||
|
/// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS)
|
||||||
|
/// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket.
|
||||||
|
/// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps).
|
||||||
|
pub cashback_fee_basis_points: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PumpSwapParams {
|
||||||
|
pub fn new(
|
||||||
|
pool: Pubkey,
|
||||||
|
base_mint: Pubkey,
|
||||||
|
quote_mint: Pubkey,
|
||||||
|
pool_base_token_account: Pubkey,
|
||||||
|
pool_quote_token_account: Pubkey,
|
||||||
|
pool_base_token_reserves: u64,
|
||||||
|
pool_quote_token_reserves: u64,
|
||||||
|
coin_creator_vault_ata: Pubkey,
|
||||||
|
coin_creator_vault_authority: Pubkey,
|
||||||
|
base_token_program: Pubkey,
|
||||||
|
quote_token_program: Pubkey,
|
||||||
|
fee_recipient: Pubkey,
|
||||||
|
coin_creator: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
|
) -> Self {
|
||||||
|
let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP;
|
||||||
|
Self {
|
||||||
|
pool,
|
||||||
|
base_mint,
|
||||||
|
quote_mint,
|
||||||
|
pool_base_token_account,
|
||||||
|
pool_quote_token_account,
|
||||||
|
pool_base_token_reserves,
|
||||||
|
pool_quote_token_reserves,
|
||||||
|
coin_creator_vault_ata,
|
||||||
|
coin_creator_vault_authority,
|
||||||
|
base_token_program,
|
||||||
|
quote_token_program,
|
||||||
|
is_mayhem_mode,
|
||||||
|
coin_creator,
|
||||||
|
is_cashback_coin,
|
||||||
|
cashback_fee_basis_points,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fast-path constructor for building PumpSwap parameters directly from decoded
|
||||||
|
/// trade/event data and the accompanying instruction accounts, avoiding RPC
|
||||||
|
/// lookups and associated latency. Token program IDs should be sourced from
|
||||||
|
/// the instruction accounts themselves to respect Token Program vs Token-2022
|
||||||
|
/// differences.
|
||||||
|
///
|
||||||
|
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin`
|
||||||
|
/// from the event so that buy/sell instructions include the correct remaining
|
||||||
|
/// accounts for cashback.
|
||||||
|
pub fn from_trade(
|
||||||
|
pool: Pubkey,
|
||||||
|
base_mint: Pubkey,
|
||||||
|
quote_mint: Pubkey,
|
||||||
|
pool_base_token_account: Pubkey,
|
||||||
|
pool_quote_token_account: Pubkey,
|
||||||
|
pool_base_token_reserves: u64,
|
||||||
|
pool_quote_token_reserves: u64,
|
||||||
|
coin_creator_vault_ata: Pubkey,
|
||||||
|
coin_creator_vault_authority: Pubkey,
|
||||||
|
base_token_program: Pubkey,
|
||||||
|
quote_token_program: Pubkey,
|
||||||
|
fee_recipient: Pubkey,
|
||||||
|
coin_creator: Pubkey,
|
||||||
|
is_cashback_coin: bool,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self::new(
|
||||||
|
pool,
|
||||||
|
base_mint,
|
||||||
|
quote_mint,
|
||||||
|
pool_base_token_account,
|
||||||
|
pool_quote_token_account,
|
||||||
|
pool_base_token_reserves,
|
||||||
|
pool_quote_token_reserves,
|
||||||
|
coin_creator_vault_ata,
|
||||||
|
coin_creator_vault_authority,
|
||||||
|
base_token_program,
|
||||||
|
quote_token_program,
|
||||||
|
fee_recipient,
|
||||||
|
coin_creator,
|
||||||
|
is_cashback_coin,
|
||||||
|
cashback_fee_basis_points,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_mint_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
mint: &Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
if let Ok((pool_address, _)) =
|
||||||
|
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
|
||||||
|
{
|
||||||
|
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||||
|
} else if let Ok((pool_address, _)) =
|
||||||
|
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
|
||||||
|
{
|
||||||
|
Self::from_pool_address_by_rpc(rpc, &pool_address).await
|
||||||
|
} else {
|
||||||
|
return Err(anyhow::anyhow!("No pool found for mint"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_pool_address_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
pool_address: &Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?;
|
||||||
|
Self::from_pool_data(rpc, pool_address, &pool_data).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build params from an already-decoded Pool, only fetching token balances.
|
||||||
|
///
|
||||||
|
/// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data
|
||||||
|
/// is already available (e.g. from `pumpswap::find_by_mint` which returns the
|
||||||
|
/// decoded Pool).
|
||||||
|
pub async fn from_pool_data(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
pool_address: &Pubkey,
|
||||||
|
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let (pool_base_token_reserves, pool_quote_token_reserves) =
|
||||||
|
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
|
||||||
|
let creator = pool_data.coin_creator;
|
||||||
|
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
|
||||||
|
creator,
|
||||||
|
pool_data.quote_mint,
|
||||||
|
);
|
||||||
|
let coin_creator_vault_authority =
|
||||||
|
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
|
||||||
|
|
||||||
|
let base_token_program_ata = get_associated_token_address_with_program_id(
|
||||||
|
pool_address,
|
||||||
|
&pool_data.base_mint,
|
||||||
|
&crate::constants::TOKEN_PROGRAM,
|
||||||
|
);
|
||||||
|
let quote_token_program_ata = get_associated_token_address_with_program_id(
|
||||||
|
pool_address,
|
||||||
|
&pool_data.quote_mint,
|
||||||
|
&crate::constants::TOKEN_PROGRAM,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
pool: *pool_address,
|
||||||
|
base_mint: pool_data.base_mint,
|
||||||
|
quote_mint: pool_data.quote_mint,
|
||||||
|
pool_base_token_account: pool_data.pool_base_token_account,
|
||||||
|
pool_quote_token_account: pool_data.pool_quote_token_account,
|
||||||
|
pool_base_token_reserves,
|
||||||
|
pool_quote_token_reserves,
|
||||||
|
coin_creator_vault_ata,
|
||||||
|
coin_creator_vault_authority,
|
||||||
|
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
|
||||||
|
crate::constants::TOKEN_PROGRAM
|
||||||
|
} else {
|
||||||
|
crate::constants::TOKEN_PROGRAM_2022
|
||||||
|
},
|
||||||
|
is_cashback_coin: pool_data.is_cashback_coin,
|
||||||
|
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
|
||||||
|
crate::constants::TOKEN_PROGRAM
|
||||||
|
} else {
|
||||||
|
crate::constants::TOKEN_PROGRAM_2022
|
||||||
|
},
|
||||||
|
is_mayhem_mode: pool_data.is_mayhem_mode,
|
||||||
|
coin_creator: pool_data.coin_creator,
|
||||||
|
cashback_fee_basis_points: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use crate::common::SolanaRpcClient;
|
||||||
|
use crate::trading::common::get_multi_token_balances;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// RaydiumCpmm protocol specific parameters
|
||||||
|
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RaydiumAmmV4Params {
|
||||||
|
/// AMM pool address
|
||||||
|
pub amm: Pubkey,
|
||||||
|
/// Base token (coin) mint address
|
||||||
|
pub coin_mint: Pubkey,
|
||||||
|
/// Quote token (pc) mint address
|
||||||
|
pub pc_mint: Pubkey,
|
||||||
|
/// Pool's coin token account address
|
||||||
|
pub token_coin: Pubkey,
|
||||||
|
/// Pool's pc token account address
|
||||||
|
pub token_pc: Pubkey,
|
||||||
|
/// Current coin reserve amount in the pool
|
||||||
|
pub coin_reserve: u64,
|
||||||
|
/// Current pc reserve amount in the pool
|
||||||
|
pub pc_reserve: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaydiumAmmV4Params {
|
||||||
|
pub fn new(
|
||||||
|
amm: Pubkey,
|
||||||
|
coin_mint: Pubkey,
|
||||||
|
pc_mint: Pubkey,
|
||||||
|
token_coin: Pubkey,
|
||||||
|
token_pc: Pubkey,
|
||||||
|
coin_reserve: u64,
|
||||||
|
pc_reserve: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve }
|
||||||
|
}
|
||||||
|
pub async fn from_amm_address_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
amm: Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?;
|
||||||
|
let (coin_reserve, pc_reserve) =
|
||||||
|
get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?;
|
||||||
|
Ok(Self {
|
||||||
|
amm,
|
||||||
|
coin_mint: amm_info.coin_mint,
|
||||||
|
pc_mint: amm_info.pc_mint,
|
||||||
|
token_coin: amm_info.token_coin,
|
||||||
|
token_pc: amm_info.token_pc,
|
||||||
|
coin_reserve,
|
||||||
|
pc_reserve,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
use crate::common::SolanaRpcClient;
|
||||||
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// RaydiumCpmm protocol specific parameters
|
||||||
|
/// Configuration parameters specific to Raydium CPMM trading protocol
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RaydiumCpmmParams {
|
||||||
|
/// Pool address
|
||||||
|
pub pool_state: Pubkey,
|
||||||
|
/// Amm config address
|
||||||
|
pub amm_config: Pubkey,
|
||||||
|
/// Base token mint address
|
||||||
|
pub base_mint: Pubkey,
|
||||||
|
/// Quote token mint address
|
||||||
|
pub quote_mint: Pubkey,
|
||||||
|
/// Base token reserve amount in the pool
|
||||||
|
pub base_reserve: u64,
|
||||||
|
/// Quote token reserve amount in the pool
|
||||||
|
pub quote_reserve: u64,
|
||||||
|
/// Base token vault address
|
||||||
|
pub base_vault: Pubkey,
|
||||||
|
/// Quote token vault address
|
||||||
|
pub quote_vault: Pubkey,
|
||||||
|
/// Base token program ID
|
||||||
|
pub base_token_program: Pubkey,
|
||||||
|
/// Quote token program ID
|
||||||
|
pub quote_token_program: Pubkey,
|
||||||
|
/// Observation state account
|
||||||
|
pub observation_state: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RaydiumCpmmParams {
|
||||||
|
pub fn from_trade(
|
||||||
|
pool_state: Pubkey,
|
||||||
|
amm_config: Pubkey,
|
||||||
|
input_token_mint: Pubkey,
|
||||||
|
output_token_mint: Pubkey,
|
||||||
|
input_vault: Pubkey,
|
||||||
|
output_vault: Pubkey,
|
||||||
|
input_token_program: Pubkey,
|
||||||
|
output_token_program: Pubkey,
|
||||||
|
observation_state: Pubkey,
|
||||||
|
base_reserve: u64,
|
||||||
|
quote_reserve: u64,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
pool_state: pool_state,
|
||||||
|
amm_config: amm_config,
|
||||||
|
base_mint: input_token_mint,
|
||||||
|
quote_mint: output_token_mint,
|
||||||
|
base_reserve: base_reserve,
|
||||||
|
quote_reserve: quote_reserve,
|
||||||
|
base_vault: input_vault,
|
||||||
|
quote_vault: output_vault,
|
||||||
|
base_token_program: input_token_program,
|
||||||
|
quote_token_program: output_token_program,
|
||||||
|
observation_state: observation_state,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn from_pool_address_by_rpc(
|
||||||
|
rpc: &SolanaRpcClient,
|
||||||
|
pool_address: &Pubkey,
|
||||||
|
) -> Result<Self, anyhow::Error> {
|
||||||
|
let pool =
|
||||||
|
crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?;
|
||||||
|
let (token0_balance, token1_balance) =
|
||||||
|
crate::instruction::utils::raydium_cpmm::get_pool_token_balances(
|
||||||
|
rpc,
|
||||||
|
pool_address,
|
||||||
|
&pool.token0_mint,
|
||||||
|
&pool.token1_mint,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(Self {
|
||||||
|
pool_state: *pool_address,
|
||||||
|
amm_config: pool.amm_config,
|
||||||
|
base_mint: pool.token0_mint,
|
||||||
|
quote_mint: pool.token1_mint,
|
||||||
|
base_reserve: token0_balance,
|
||||||
|
quote_reserve: token1_balance,
|
||||||
|
base_vault: pool.token0_vault,
|
||||||
|
quote_vault: pool.token1_vault,
|
||||||
|
base_token_program: pool.token0_program,
|
||||||
|
quote_token_program: pool.token1_program,
|
||||||
|
observation_state: pool.observation_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,9 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
|
|||||||
/// * basis_points = 500 -> 5% slippage
|
/// * basis_points = 500 -> 5% slippage
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64 {
|
||||||
|
if amount == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
if amount <= basis_points / 10000 {
|
if amount <= basis_points / 10000 {
|
||||||
1
|
1
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+31
-12
@@ -6,6 +6,21 @@ use crate::instruction::utils::pumpswap::accounts::{
|
|||||||
};
|
};
|
||||||
use solana_sdk::pubkey::Pubkey;
|
use solana_sdk::pubkey::Pubkey;
|
||||||
|
|
||||||
|
/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional
|
||||||
|
/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field).
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn creator_side_fee_basis_points(
|
||||||
|
coin_creator: &Pubkey,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
|
) -> u64 {
|
||||||
|
let creator_bps = if *coin_creator == Pubkey::default() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
COIN_CREATOR_FEE_BASIS_POINTS
|
||||||
|
};
|
||||||
|
creator_bps.saturating_add(cashback_fee_basis_points)
|
||||||
|
}
|
||||||
|
|
||||||
/// Result for buying base tokens with base amount input
|
/// Result for buying base tokens with base amount input
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct BuyBaseInputResult {
|
pub struct BuyBaseInputResult {
|
||||||
@@ -58,6 +73,7 @@ pub struct SellQuoteInputResult {
|
|||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Quote token reserves in the pool
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `BuyBaseInputResult` containing quote amounts and slippage calculations
|
/// * `BuyBaseInputResult` containing quote amounts and slippage calculations
|
||||||
@@ -67,6 +83,7 @@ pub fn buy_base_input_internal(
|
|||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<BuyBaseInputResult, String> {
|
) -> Result<BuyBaseInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
@@ -89,11 +106,9 @@ pub fn buy_base_input_internal(
|
|||||||
let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
||||||
let protocol_fee =
|
let protocol_fee =
|
||||||
compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
||||||
let coin_creator_fee = if *coin_creator == Pubkey::default() {
|
let creator_bps =
|
||||||
0
|
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
|
||||||
} else {
|
let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64;
|
||||||
compute_fee(quote_amount_in as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
|
|
||||||
};
|
|
||||||
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
|
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
|
||||||
|
|
||||||
// Calculate max quote with slippage
|
// Calculate max quote with slippage
|
||||||
@@ -114,6 +129,7 @@ pub fn buy_base_input_internal(
|
|||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Quote token reserves in the pool
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `BuyQuoteInputResult` containing base amount and slippage calculations
|
/// * `BuyQuoteInputResult` containing base amount and slippage calculations
|
||||||
@@ -123,6 +139,7 @@ pub fn buy_quote_input_internal(
|
|||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<BuyQuoteInputResult, String> {
|
) -> Result<BuyQuoteInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
@@ -131,7 +148,7 @@ pub fn buy_quote_input_internal(
|
|||||||
// Calculate total fee basis points
|
// Calculate total fee basis points
|
||||||
let total_fee_bps = LP_FEE_BASIS_POINTS
|
let total_fee_bps = LP_FEE_BASIS_POINTS
|
||||||
+ PROTOCOL_FEE_BASIS_POINTS
|
+ PROTOCOL_FEE_BASIS_POINTS
|
||||||
+ if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
|
+ creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points);
|
||||||
let denominator = 10_000 + total_fee_bps;
|
let denominator = 10_000 + total_fee_bps;
|
||||||
|
|
||||||
// Calculate effective quote amount after fees
|
// Calculate effective quote amount after fees
|
||||||
@@ -165,6 +182,7 @@ pub fn buy_quote_input_internal(
|
|||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Quote token reserves in the pool
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `SellBaseInputResult` containing quote amounts and slippage calculations
|
/// * `SellBaseInputResult` containing quote amounts and slippage calculations
|
||||||
@@ -174,6 +192,7 @@ pub fn sell_base_input_internal(
|
|||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<SellBaseInputResult, String> {
|
) -> Result<SellBaseInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
@@ -187,11 +206,9 @@ pub fn sell_base_input_internal(
|
|||||||
let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64;
|
||||||
let protocol_fee =
|
let protocol_fee =
|
||||||
compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64;
|
||||||
let coin_creator_fee = if *coin_creator == Pubkey::default() {
|
let creator_bps =
|
||||||
0
|
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128;
|
||||||
} else {
|
let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64;
|
||||||
compute_fee(quote_amount_out as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate final quote after fees
|
// Calculate final quote after fees
|
||||||
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
|
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
|
||||||
@@ -234,6 +251,7 @@ fn calculate_quote_amount_out(
|
|||||||
/// * `base_reserve` - Base token reserves in the pool
|
/// * `base_reserve` - Base token reserves in the pool
|
||||||
/// * `quote_reserve` - Quote token reserves in the pool
|
/// * `quote_reserve` - Quote token reserves in the pool
|
||||||
/// * `coin_creator` - Token creator address
|
/// * `coin_creator` - Token creator address
|
||||||
|
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
/// * `SellQuoteInputResult` containing base amount and slippage calculations
|
/// * `SellQuoteInputResult` containing base amount and slippage calculations
|
||||||
@@ -243,6 +261,7 @@ pub fn sell_quote_input_internal(
|
|||||||
base_reserve: u64,
|
base_reserve: u64,
|
||||||
quote_reserve: u64,
|
quote_reserve: u64,
|
||||||
coin_creator: &Pubkey,
|
coin_creator: &Pubkey,
|
||||||
|
cashback_fee_basis_points: u64,
|
||||||
) -> Result<SellQuoteInputResult, String> {
|
) -> Result<SellQuoteInputResult, String> {
|
||||||
if base_reserve == 0 || quote_reserve == 0 {
|
if base_reserve == 0 || quote_reserve == 0 {
|
||||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||||
@@ -256,7 +275,7 @@ pub fn sell_quote_input_internal(
|
|||||||
quote,
|
quote,
|
||||||
LP_FEE_BASIS_POINTS,
|
LP_FEE_BASIS_POINTS,
|
||||||
PROTOCOL_FEE_BASIS_POINTS,
|
PROTOCOL_FEE_BASIS_POINTS,
|
||||||
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS },
|
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate base amount needed using inverse constant product formula
|
// Calculate base amount needed using inverse constant product formula
|
||||||
|
|||||||
Reference in New Issue
Block a user