Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e92f8f333 | |||
| cf3bef0dd5 | |||
| c76c8d6ac2 |
@@ -55,6 +55,7 @@
|
||||
- [📊 Usage Examples Summary Table](#-usage-examples-summary-table)
|
||||
- [⚙️ SWQoS Service Configuration](#️-swqos-service-configuration)
|
||||
- [Astralane (Binary / Plain / QUIC)](#astralane-binary--plain--quic)
|
||||
- [Glaive (Binary HTTP / QUIC)](#glaive-binary-http--quic)
|
||||
- [🔧 Middleware System](#-middleware-system)
|
||||
- [🔍 Address Lookup Tables](#-address-lookup-tables)
|
||||
- [🔍 Nonce Cache](#-nonce-cache)
|
||||
@@ -86,7 +87,7 @@ This SDK is available in multiple languages:
|
||||
| Area | Coverage |
|
||||
|------|----------|
|
||||
| DEX protocols | PumpFun, PumpSwap, Bonk, Meteora DAMM v2, Raydium AMM v4, Raydium CPMM |
|
||||
| Submit lanes | Default Solana RPC plus Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, SpeedLanding, and other SWQoS providers |
|
||||
| Submit lanes | Default Solana RPC plus Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, Glaive, SpeedLanding, and other SWQoS providers |
|
||||
| Trading workflows | Buy/sell, exact input/output, copy trading, sniper trading, address lookup tables, durable nonce, middleware, shared infrastructure |
|
||||
| Hot-path design | Caller supplies recent blockhash or durable nonce; trade execution avoids RPC reads for blockhash, account, or balance data |
|
||||
|
||||
@@ -104,7 +105,7 @@ This release updates PumpSwap for the July 2026 virtual quote reserve rollout. P
|
||||
4. **Raydium CPMM Trading**: Support for Raydium CPMM (Concentrated Pool Market Maker) trading operations
|
||||
5. **Raydium AMM V4 Trading**: Support for Raydium AMM V4 (Automated Market Maker) trading operations
|
||||
6. **Meteora DAMM V2 Trading**: Support for Meteora DAMM V2 (Dynamic AMM) trading operations
|
||||
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, LunarLander and other services
|
||||
7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, Glaive, LunarLander and other services
|
||||
8. **Concurrent Trading**: Submit through every configured SWQoS provider plus the default RPC lane; the first accepted result can return early while slower routes continue submitting
|
||||
9. **Unified Trading Interface**: Use unified trading protocol enums for trading operations
|
||||
10. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution
|
||||
@@ -169,6 +170,13 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
None,
|
||||
Some(SwqosTransport::Http),
|
||||
),
|
||||
// Glaive: None = QUIC (default, UDP/4000); Some(Http) = binary HTTP
|
||||
SwqosConfig::Glaive(
|
||||
"your_glaive_uuid_v4_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
];
|
||||
// Create TradeConfig instance
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -177,7 +185,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .log_enabled(true) // default: true - SDK timing / SWQOS logs
|
||||
// .check_min_tip(false) // default: false - filter SWQOS below min tip
|
||||
// .swqos_cores_from_end(false) // default: false - bind SWQOS to last N CPU cores
|
||||
// .mev_protection(false) // default: false - MEV (Astralane QUIC :9000 or HTTP mev-protect / BlockRazor)
|
||||
// .mev_protection(false) // default: false - MEV protection for Astralane / BlockRazor / Glaive
|
||||
.build();
|
||||
|
||||
// Create TradingClient
|
||||
@@ -348,6 +356,7 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- If a custom URL is provided (`Some(url)`), it will be used instead of the regional endpoint
|
||||
- If no custom URL is provided (`None`), the system will use the default endpoint for the specified `SwqosRegion`
|
||||
- This allows for maximum flexibility while maintaining backward compatibility
|
||||
- For Glaive, a custom QUIC URL is `host:4000`; a custom HTTP URL is an absolute `http://` or `https://` base URL. The SDK appends `/binary` and authentication parameters for HTTP.
|
||||
|
||||
When using multiple MEV services, you need to use `Durable Nonce`. Fetch the latest nonce value and attach it to the high-level buy/sell params:
|
||||
|
||||
@@ -396,6 +405,42 @@ let swqos_configs: Vec<SwqosConfig> = vec for credentials, rate limits, and protocol details.
|
||||
|
||||
---
|
||||
|
||||
### 🔧 Middleware System
|
||||
@@ -523,6 +568,7 @@ You can apply for a key through the official website: [Community Website](https:
|
||||
- **FlashBlock**: High-speed transaction execution with API key authentication
|
||||
- **BlockRazor**: High-speed transaction execution with API key authentication
|
||||
- **Astralane**: Blockchain network acceleration (Binary/Plain HTTP and QUIC)
|
||||
- **Glaive**: Persistent QUIC and binary HTTP transaction delivery (minimum tip: 0.0001 SOL)
|
||||
- **SpeedLanding**: High-speed transaction execution with API key authentication
|
||||
- **Node1**: High-speed transaction execution with API key authentication
|
||||
- **LunarLander**: HelloMoon transaction landing service (minimum tip: 0.001 SOL)
|
||||
|
||||
+49
-3
@@ -55,6 +55,7 @@
|
||||
- [📊 使用示例汇总表格](#-使用示例汇总表格)
|
||||
- [⚙️ SWQoS 服务配置说明](#️-swqos-服务配置说明)
|
||||
- [Astralane(Binary / Plain / QUIC)](#astralanebinary--plain--quic)
|
||||
- [Glaive(Binary HTTP / QUIC)](#glaivebinary-http--quic)
|
||||
- [🔧 中间件系统说明](#-中间件系统说明)
|
||||
- [🔍 地址查找表](#-地址查找表)
|
||||
- [🔍 Nonce 缓存](#-nonce-缓存)
|
||||
@@ -86,7 +87,7 @@
|
||||
| 方向 | 覆盖范围 |
|
||||
|------|----------|
|
||||
| DEX 协议 | PumpFun、PumpSwap、Bonk、Meteora DAMM v2、Raydium AMM v4、Raydium CPMM |
|
||||
| 提交通道 | 默认 Solana RPC,以及 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、SpeedLanding 等 SWQoS 服务 |
|
||||
| 提交通道 | 默认 Solana RPC,以及 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、Glaive、SpeedLanding 等 SWQoS 服务 |
|
||||
| 交易流程 | 买入/卖出、精确输入/输出、跟单交易、狙击交易、地址查找表、durable nonce、中间件、共享基础设施 |
|
||||
| 热路径设计 | 调用方传入 recent blockhash 或 durable nonce;交易执行阶段不再查询 RPC 获取 blockhash、账户或余额 |
|
||||
|
||||
@@ -104,7 +105,7 @@
|
||||
4. **Raydium CPMM 交易**: 支持 Raydium CPMM (Concentrated Pool Market Maker) 的交易操作
|
||||
5. **Raydium AMM V4 交易**: 支持 Raydium AMM V4 (Automated Market Maker) 的交易操作
|
||||
6. **Meteora DAMM V2 交易**: 支持 Meteora DAMM V2 (Dynamic AMM) 的交易操作
|
||||
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、SpeedLanding、LunarLander 等服务
|
||||
7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、Glaive、SpeedLanding、LunarLander 等服务
|
||||
8. **并发交易**: 所有已配置的 SWQoS 通道和默认 RPC 通道都会发出提交;首个成功只影响返回,较慢通道会继续提交
|
||||
9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作
|
||||
10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除
|
||||
@@ -169,6 +170,13 @@ let swqos_configs: Vec<SwqosConfig> = vec![
|
||||
None,
|
||||
Some(SwqosTransport::Http),
|
||||
),
|
||||
// Glaive:None 为 QUIC(默认,UDP/4000);Some(Http) 为 binary HTTP
|
||||
SwqosConfig::Glaive(
|
||||
"your_glaive_uuid_v4_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
];
|
||||
// 创建 TradeConfig 实例
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
@@ -177,7 +185,7 @@ let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
// .log_enabled(true) // 默认: true - SDK 计时 / SWQOS 日志
|
||||
// .check_min_tip(false) // 默认: false - 过滤低于最低小费的 SWQOS
|
||||
// .swqos_cores_from_end(false) // 默认: false - 将 SWQOS 绑定到末尾 N 个 CPU 核心
|
||||
// .mev_protection(false) // 默认: false - MEV(Astralane QUIC :9000 或 HTTP mev-protect / BlockRazor)
|
||||
// .mev_protection(false) // 默认: false - Astralane / BlockRazor / Glaive 的 MEV 保护
|
||||
.build();
|
||||
|
||||
// 创建 TradingClient
|
||||
@@ -347,6 +355,7 @@ let temporal_config = SwqosConfig::Temporal(
|
||||
- 如果提供了自定义 URL(`Some(url)`),将使用自定义 URL 而不是区域端点
|
||||
- 如果没有提供自定义 URL(`None`),系统将使用指定 `SwqosRegion` 的默认端点
|
||||
- 这提供了最大的灵活性,同时保持向后兼容性
|
||||
- Glaive 自定义 QUIC 地址格式为 `host:4000`;自定义 HTTP 地址必须是完整的 `http://` 或 `https://` 基础 URL,SDK 会自动追加 `/binary` 和鉴权参数。
|
||||
|
||||
当使用多个 MEV 服务时,需要使用 `Durable Nonce`。先获取最新 nonce,再挂到新的 buy/sell 参数上:
|
||||
|
||||
@@ -395,6 +404,42 @@ let swqos_configs: Vec<SwqosConfig> = vec。
|
||||
|
||||
---
|
||||
|
||||
### 🔧 中间件系统说明
|
||||
@@ -520,6 +565,7 @@ PumpSwap 报价必须使用 `effective_quote_reserves = pool_quote_token_account
|
||||
- **FlashBlock**: 高速交易执行,支持 API 密钥认证
|
||||
- **BlockRazor**: 高速交易执行,支持 API 密钥认证
|
||||
- **Astralane**: 区块链网络加速(Binary/Plain HTTP 与 QUIC)
|
||||
- **Glaive**: 持久 QUIC 与 binary HTTP 交易投递(最低 tip:0.0001 SOL)
|
||||
- **SpeedLanding**: 高速交易执行,支持 API 密钥认证
|
||||
- **Node1**: 高速交易执行,支持 API 密钥认证
|
||||
- **LunarLander**: HelloMoon 交易着陆服务(最低小费:0.001 SOL)
|
||||
|
||||
@@ -6,6 +6,8 @@ Shows two client-construction patterns: `TradingClient::new` for one wallet and
|
||||
|
||||
This is a configuration template. Set `PRIVATE_KEY`, RPC, and every enabled provider credential; it initializes network clients but does not submit a trade.
|
||||
|
||||
The Glaive entry uses QUIC by default. Its credential must be a UUID v4 API key from Glaive. Replace the fourth argument with `Some(SwqosTransport::Http)` to use binary HTTP instead.
|
||||
|
||||
```bash
|
||||
cargo run --package trading_client
|
||||
```
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
这是配置模板。运行前设置 `PRIVATE_KEY`、RPC 和所有已启用服务商凭证;程序会初始化网络客户端,但不会提交交易。
|
||||
|
||||
Glaive 配置默认使用 QUIC,其凭证必须是 Glaive 提供的 UUID v4 API key。若要改用 binary HTTP,把第四个参数替换为 `Some(SwqosTransport::Http)`。
|
||||
|
||||
```bash
|
||||
cargo run --package trading_client
|
||||
```
|
||||
|
||||
@@ -55,6 +55,20 @@ async fn create_trading_client_simple() -> AnyResult<TradingClient> {
|
||||
), // QUIC;None / Some(Binary) / Some(Plain) 为 HTTP
|
||||
// Helius Sender: 4th param swqos_only Some(true) => min tip 0.000005 SOL; None => 0.0002 SOL
|
||||
SwqosConfig::Helius("".to_string(), SwqosRegion::Default, None, Some(true)),
|
||||
// Glaive defaults to persistent QUIC (UDP/4000). Use Some(Http) for binary HTTP.
|
||||
SwqosConfig::Glaive(
|
||||
"your_glaive_uuid_v4_api_key".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
// HTTP alternative:
|
||||
// SwqosConfig::Glaive(
|
||||
// "your_glaive_uuid_v4_api_key".to_string(),
|
||||
// SwqosRegion::Frankfurt,
|
||||
// None,
|
||||
// Some(sol_trade_sdk::SwqosTransport::Http),
|
||||
// ),
|
||||
];
|
||||
|
||||
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
|
||||
|
||||
+6
-5
@@ -13,8 +13,8 @@ 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.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC `:9000` or HTTP `mev-protect=true`, BlockRazor) use MEV-protected
|
||||
/// endpoints/modes. Default false.
|
||||
/// (Astralane, BlockRazor, Glaive) use MEV-protected endpoints/modes. Glaive HTTP adds
|
||||
/// `mev-protect=true`; Glaive QUIC sets auth-frame flag bit 0. Default false.
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
|
||||
@@ -100,8 +100,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.
|
||||
pub swqos_cores_from_end: bool,
|
||||
/// Global MEV protection flag. When true, SWQOS providers that support MEV protection
|
||||
/// (Astralane QUIC `:9000` or Plain/Binary HTTP `mev-protect=true`, BlockRazor sandwichMitigation)
|
||||
/// use their MEV-protected endpoints/modes. Default false (no MEV protection, lower latency).
|
||||
/// (Astralane, BlockRazor, Glaive) use their MEV-protected endpoints/modes. Glaive HTTP
|
||||
/// adds `mev-protect=true`; Glaive QUIC sets auth-frame flag bit 0. Default false.
|
||||
pub mev_protection: bool,
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ impl TradeConfig {
|
||||
/// - `.log_enabled(bool)` — SDK timing/SWQOS logs (default: true)
|
||||
/// - `.check_min_tip(bool)` — filter SWQOS below min tip (default: false)
|
||||
/// - `.swqos_cores_from_end(bool)` — bind SWQOS to last N cores (default: false)
|
||||
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor (default: false)
|
||||
/// - `.mev_protection(bool)` — MEV protection for Astralane/BlockRazor/Glaive (default: false)
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust,ignore
|
||||
@@ -209,6 +209,7 @@ impl TradeConfigBuilder {
|
||||
/// Enable global MEV protection. When `true`:
|
||||
/// - **Astralane QUIC** uses port `9000`; **Astralane HTTP** adds `mev-protect=true`
|
||||
/// - **BlockRazor** uses `mode=sandwichMitigation` (skips blacklisted Leader slots)
|
||||
/// - **Glaive HTTP** adds `mev-protect=true`; **Glaive QUIC** sets auth-frame flag bit 0
|
||||
///
|
||||
/// May reduce landing speed. Default: `false`.
|
||||
pub fn mev_protection(mut self, v: bool) -> Self {
|
||||
|
||||
@@ -198,6 +198,16 @@ pub const LUNARLANDER_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"),
|
||||
];
|
||||
|
||||
/// Glaive tip accounts from <https://glaive.trade/docs#tip-accounts>.
|
||||
pub const GLAIVE_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("GLaiv4GMRYQmthatDS98uQT4HoucgxWT8NeJz6oSwxeU"),
|
||||
pubkey!("GLaivL5uPrDpvd1wTtvat38KGqb5WLhEdqQfnmNd3oNr"),
|
||||
pubkey!("GLaivinAWh21NaJMhtExtD5G2gZs1xnvaYVZmwqobWZL"),
|
||||
pubkey!("GLaivJSUL71FcocYa8tks5vpVyYzvaDMHtyrzfQF2ABr"),
|
||||
pubkey!("GLaivRU6eDKrta3p3psFAWPEFLzCjeMHGpPUuQqTjtyv"),
|
||||
pubkey!("GLaivq5dU8qHayz9Qf13LjPfVy3SmUhbmickfGiZdmfh"),
|
||||
];
|
||||
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
@@ -477,6 +487,35 @@ pub const SWQOS_ENDPOINTS_LUNARLANDER_QUIC: [&str; 10] = [
|
||||
"nyc-1.prod.lunar-lander.hellomoon.io:16888", // Default -> NYC
|
||||
];
|
||||
|
||||
/// Glaive binary HTTP origins. The client appends `/binary?api-key=...`.
|
||||
/// Glaive currently publishes Amsterdam, Frankfurt, London, and New York PoPs.
|
||||
pub const SWQOS_ENDPOINTS_GLAIVE: [&str; 10] = [
|
||||
"http://ny.glaive.trade",
|
||||
"http://fra.glaive.trade",
|
||||
"http://ams1.glaive.trade",
|
||||
"http://lon.glaive.trade", // Dublin -> London
|
||||
"http://ny.glaive.trade", // SLC -> only published US PoP
|
||||
"http://ams1.glaive.trade", // Tokyo -> nearest published PoP
|
||||
"http://fra.glaive.trade", // Singapore -> nearest published PoP
|
||||
"http://lon.glaive.trade",
|
||||
"http://ny.glaive.trade", // Los Angeles -> only published US PoP
|
||||
"http://ams1.glaive.trade", // Default -> official documentation example
|
||||
];
|
||||
|
||||
/// Glaive QUIC endpoints. ALPN `solana-tpu`, SNI `glaive-intake`, UDP port 4000.
|
||||
pub const SWQOS_ENDPOINTS_GLAIVE_QUIC: [&str; 10] = [
|
||||
"ny.glaive.trade:4000",
|
||||
"fra.glaive.trade:4000",
|
||||
"ams1.glaive.trade:4000",
|
||||
"lon.glaive.trade:4000", // Dublin -> London
|
||||
"ny.glaive.trade:4000", // SLC -> only published US PoP
|
||||
"ams1.glaive.trade:4000", // Tokyo -> nearest published PoP
|
||||
"fra.glaive.trade:4000", // Singapore -> nearest published PoP
|
||||
"lon.glaive.trade:4000",
|
||||
"ny.glaive.trade:4000", // Los Angeles -> only published US PoP
|
||||
"ams1.glaive.trade:4000", // Default -> official documentation example
|
||||
];
|
||||
|
||||
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
|
||||
pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"http://ewr-sender.helius-rpc.com/fast",
|
||||
@@ -522,6 +561,8 @@ 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_SOLAMI: f64 = 0.0001;
|
||||
pub const SWQOS_MIN_TIP_LUNARLANDER: f64 = 0.001; // LunarLander 最低小费 0.001 SOL
|
||||
/// Glaive requires at least 100,000 lamports (0.0001 SOL).
|
||||
pub const SWQOS_MIN_TIP_GLAIVE: f64 = 0.0001;
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
#[cfg(test)]
|
||||
@@ -549,6 +590,8 @@ mod tests {
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
&SWQOS_ENDPOINTS_LUNARLANDER,
|
||||
&SWQOS_ENDPOINTS_LUNARLANDER_QUIC,
|
||||
&SWQOS_ENDPOINTS_GLAIVE,
|
||||
&SWQOS_ENDPOINTS_GLAIVE_QUIC,
|
||||
&SWQOS_ENDPOINTS_SOLAMI,
|
||||
];
|
||||
|
||||
@@ -593,4 +636,21 @@ mod tests {
|
||||
assert_eq!(plain, binary, "Astralane Plain vs Binary base URL mismatch at index {}", i);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glaive_http_and_quic_hosts_match_per_region() {
|
||||
for i in 0..10 {
|
||||
let http_host =
|
||||
SWQOS_ENDPOINTS_GLAIVE[i].strip_prefix("http://").expect("Glaive HTTP URL");
|
||||
let quic_host =
|
||||
SWQOS_ENDPOINTS_GLAIVE_QUIC[i].strip_suffix(":4000").expect("Glaive QUIC endpoint");
|
||||
assert_eq!(http_host, quic_host, "Glaive HTTP vs QUIC host mismatch at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glaive_tip_policy_matches_official_docs() {
|
||||
assert_eq!(GLAIVE_TIP_ACCOUNTS.len(), 6);
|
||||
assert_eq!(SWQOS_MIN_TIP_GLAIVE, 0.0001);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ pub mod trading;
|
||||
pub mod utils;
|
||||
|
||||
pub use crate::common::nonce_cache::{fetch_nonce_info, DurableNonceInfo};
|
||||
// Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode)
|
||||
// Re-export transport selectors used by SWQoS configs (including Glaive).
|
||||
pub use crate::swqos::{AstralaneTransport, SwqosTransport};
|
||||
pub use client::{
|
||||
find_pool_by_mint, recommended_sender_thread_core_indices, AccountPolicy, BuyAmount,
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
use anyhow::{Context as _, Result};
|
||||
use parking_lot::Mutex;
|
||||
use rand::seq::IndexedRandom as _;
|
||||
use reqwest::{Client, StatusCode, Url};
|
||||
use serde_json::Value;
|
||||
use solana_sdk::{signature::Signature, transaction::VersionedTransaction};
|
||||
use std::{
|
||||
str::FromStr as _,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tokio::task::JoinHandle;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
common::SolanaRpcClient,
|
||||
constants::swqos::GLAIVE_TIP_ACCOUNTS,
|
||||
swqos::{
|
||||
common::{default_http_client_builder, poll_transaction_confirmation},
|
||||
glaive_quic::GlaiveQuicClient,
|
||||
serialization::serialize_transaction_bincode_sync,
|
||||
SwqosClientTrait, SwqosType, TradeType,
|
||||
},
|
||||
};
|
||||
|
||||
const HTTP_KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(20);
|
||||
|
||||
enum GlaiveBackend {
|
||||
Http {
|
||||
submit_url: Url,
|
||||
health_url: Url,
|
||||
http_client: Client,
|
||||
stop_ping: Arc<AtomicBool>,
|
||||
ping_handle: Mutex<Option<JoinHandle<()>>>,
|
||||
},
|
||||
Quic(Arc<GlaiveQuicClient>),
|
||||
}
|
||||
|
||||
pub struct GlaiveClient {
|
||||
rpc_client: Arc<SolanaRpcClient>,
|
||||
backend: GlaiveBackend,
|
||||
}
|
||||
|
||||
impl GlaiveClient {
|
||||
pub fn new_http(
|
||||
rpc_url: String,
|
||||
endpoint: String,
|
||||
api_key: String,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
validate_api_key(&api_key)?;
|
||||
let submit_url = build_binary_url(&endpoint, &api_key, mev_protection)?;
|
||||
let health_url = build_health_url(&endpoint)?;
|
||||
let http_client = default_http_client_builder().build()?;
|
||||
let stop_ping = Arc::new(AtomicBool::new(false));
|
||||
let ping_handle = Mutex::new(None);
|
||||
|
||||
let client = Self {
|
||||
rpc_client: Arc::new(SolanaRpcClient::new(rpc_url)),
|
||||
backend: GlaiveBackend::Http {
|
||||
submit_url,
|
||||
health_url,
|
||||
http_client,
|
||||
stop_ping,
|
||||
ping_handle,
|
||||
},
|
||||
};
|
||||
client.start_http_keep_alive();
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub async fn new_quic(
|
||||
rpc_url: String,
|
||||
endpoint: &str,
|
||||
api_key: String,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
let quic = GlaiveQuicClient::connect(endpoint, &api_key, mev_protection).await?;
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(SolanaRpcClient::new(rpc_url)),
|
||||
backend: GlaiveBackend::Quic(Arc::new(quic)),
|
||||
})
|
||||
}
|
||||
|
||||
fn start_http_keep_alive(&self) {
|
||||
let GlaiveBackend::Http { health_url, http_client, stop_ping, ping_handle, .. } =
|
||||
&self.backend
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let health_url = health_url.clone();
|
||||
let http_client = http_client.clone();
|
||||
let stop_ping = Arc::clone(stop_ping);
|
||||
let Ok(runtime) = tokio::runtime::Handle::try_current() else {
|
||||
return;
|
||||
};
|
||||
let handle = runtime.spawn(async move {
|
||||
let mut interval = tokio::time::interval(HTTP_KEEP_ALIVE_INTERVAL);
|
||||
loop {
|
||||
interval.tick().await;
|
||||
if stop_ping.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
if let Err(error) = send_health_request(&http_client, health_url.clone()).await {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
eprintln!(" [Glaive] HTTP keep-alive failed: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
*ping_handle.lock() = Some(handle);
|
||||
}
|
||||
|
||||
async fn send_transaction_impl(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
let submit_started = Instant::now();
|
||||
let (serialized, signature) = serialize_transaction_bincode_sync(transaction)?;
|
||||
|
||||
let submit_result = match &self.backend {
|
||||
GlaiveBackend::Http { submit_url, http_client, .. } => {
|
||||
let response = http_client
|
||||
.post(submit_url.clone())
|
||||
.header("Content-Type", "application/octet-stream")
|
||||
.body(serialized.to_vec())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
anyhow::anyhow!("Glaive HTTP request failed: {}", error.without_url())
|
||||
})?;
|
||||
let status = response.status();
|
||||
let body = response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("read Glaive response: {error}"))?;
|
||||
parse_binary_response(status, &body, signature)
|
||||
}
|
||||
GlaiveBackend::Quic(quic) => quic.send_transaction(&serialized).await,
|
||||
};
|
||||
|
||||
if let Err(error) = submit_result {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Glaive",
|
||||
trade_type,
|
||||
submit_started.elapsed(),
|
||||
&error,
|
||||
);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted(
|
||||
"Glaive",
|
||||
trade_type,
|
||||
submit_started.elapsed(),
|
||||
);
|
||||
}
|
||||
|
||||
let confirm_started = Instant::now();
|
||||
if let Err(error) =
|
||||
poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await
|
||||
{
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Glaive",
|
||||
trade_type,
|
||||
confirm_started.elapsed(),
|
||||
&error,
|
||||
);
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {signature:?}");
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Glaive",
|
||||
trade_type,
|
||||
confirm_started.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SwqosClientTrait for GlaiveClient {
|
||||
async fn send_transaction(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transaction: &VersionedTransaction,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
|
||||
}
|
||||
|
||||
async fn send_transactions(
|
||||
&self,
|
||||
trade_type: TradeType,
|
||||
transactions: &Vec<VersionedTransaction>,
|
||||
wait_confirmation: bool,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
let tip_account = *GLAIVE_TIP_ACCOUNTS
|
||||
.choose(&mut rand::rng())
|
||||
.or_else(|| GLAIVE_TIP_ACCOUNTS.first())
|
||||
.context("Glaive tip account list is empty")?;
|
||||
Ok(tip_account.to_string())
|
||||
}
|
||||
|
||||
fn get_swqos_type(&self) -> SwqosType {
|
||||
SwqosType::Glaive
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlaiveClient {
|
||||
fn drop(&mut self) {
|
||||
if let GlaiveBackend::Http { stop_ping, ping_handle, .. } = &self.backend {
|
||||
stop_ping.store(true, Ordering::Relaxed);
|
||||
if let Some(handle) = ping_handle.lock().take() {
|
||||
handle.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_api_key(api_key: &str) -> Result<()> {
|
||||
let key = Uuid::parse_str(api_key.trim()).context("Glaive API key must be a valid UUID v4")?;
|
||||
if key.get_version_num() != 4 {
|
||||
anyhow::bail!("Glaive API key must be a UUID v4");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_endpoint(endpoint: &str) -> Result<Url> {
|
||||
Url::parse(endpoint).context("Glaive HTTP endpoint must be an absolute http(s) URL")
|
||||
}
|
||||
|
||||
fn build_binary_url(endpoint: &str, api_key: &str, mev_protection: bool) -> Result<Url> {
|
||||
let mut url = parse_endpoint(endpoint)?;
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
anyhow::bail!("Glaive HTTP endpoint must use http or https");
|
||||
}
|
||||
|
||||
let path = url.path().trim_end_matches('/');
|
||||
if !path.ends_with("/binary") {
|
||||
let path = if path.is_empty() { "/binary".to_string() } else { format!("{path}/binary") };
|
||||
url.set_path(&path);
|
||||
}
|
||||
let existing_query: Vec<(String, String)> = url
|
||||
.query_pairs()
|
||||
.filter(|(key, _)| key != "api-key" && key != "mev-protect")
|
||||
.map(|(key, value)| (key.into_owned(), value.into_owned()))
|
||||
.collect();
|
||||
url.set_query(None);
|
||||
{
|
||||
let mut query = url.query_pairs_mut();
|
||||
query.extend_pairs(existing_query);
|
||||
query.append_pair("api-key", api_key.trim());
|
||||
if mev_protection {
|
||||
query.append_pair("mev-protect", "true");
|
||||
}
|
||||
}
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
fn build_health_url(endpoint: &str) -> Result<Url> {
|
||||
let mut url = parse_endpoint(endpoint)?;
|
||||
if !matches!(url.scheme(), "http" | "https") {
|
||||
anyhow::bail!("Glaive HTTP endpoint must use http or https");
|
||||
}
|
||||
url.set_path("/health");
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
Ok(url)
|
||||
}
|
||||
|
||||
async fn send_health_request(client: &Client, health_url: Url) -> Result<()> {
|
||||
let response =
|
||||
client.get(health_url).timeout(Duration::from_millis(1500)).send().await.map_err(
|
||||
|error| anyhow::anyhow!("Glaive health request failed: {}", error.without_url()),
|
||||
)?;
|
||||
let status = response.status();
|
||||
response
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("read Glaive health response: {}", error.without_url()))?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("Glaive health request returned HTTP {status}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_binary_response(status: StatusCode, body: &[u8], expected: Signature) -> Result<()> {
|
||||
let json: Value = serde_json::from_slice(body).with_context(|| {
|
||||
format!("Glaive returned HTTP {status} with invalid JSON: {}", bounded_body(body))
|
||||
})?;
|
||||
|
||||
if let Some(error) = json.get("error") {
|
||||
let message = error
|
||||
.get("message")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| error.as_str())
|
||||
.unwrap_or("unknown Glaive error");
|
||||
let code = error.get("code").and_then(Value::as_i64);
|
||||
if let Some(code) = code {
|
||||
anyhow::bail!("Glaive rejected transaction: code={code} message={message}");
|
||||
}
|
||||
anyhow::bail!("Glaive rejected transaction: {message}");
|
||||
}
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("Glaive returned HTTP {status}: {}", bounded_body(body));
|
||||
}
|
||||
|
||||
let result = json
|
||||
.get("result")
|
||||
.and_then(Value::as_str)
|
||||
.context("Glaive response missing result signature")?;
|
||||
let returned = Signature::from_str(result).context("Glaive returned an invalid signature")?;
|
||||
if returned != expected {
|
||||
anyhow::bail!("Glaive returned a signature that does not match the submitted transaction");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn bounded_body(body: &[u8]) -> String {
|
||||
String::from_utf8_lossy(body).chars().take(512).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEST_UUID: &str = "00112233-4455-4677-8899-aabbccddeeff";
|
||||
|
||||
#[test]
|
||||
fn binary_url_uses_official_query_names() {
|
||||
let url = build_binary_url("http://fra.glaive.trade", TEST_UUID, true).unwrap();
|
||||
assert_eq!(url.path(), "/binary");
|
||||
let query: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect();
|
||||
assert_eq!(query.get("api-key").map(String::as_str), Some(TEST_UUID));
|
||||
assert_eq!(query.get("mev-protect").map(String::as_str), Some("true"));
|
||||
assert!(!query.contains_key("key"));
|
||||
assert!(!query.contains_key("mev_protect"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_binary_url_is_not_duplicated_and_health_is_rooted() {
|
||||
let url = build_binary_url(
|
||||
"https://custom.example/glaive/binary?existing=1&api-key=stale&mev-protect=true",
|
||||
TEST_UUID,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(url.path(), "/glaive/binary");
|
||||
assert!(url.query_pairs().any(|(key, value)| key == "existing" && value == "1"));
|
||||
assert!(!url.query_pairs().any(|(key, _)| key == "mev-protect"));
|
||||
let keys: Vec<_> = url
|
||||
.query_pairs()
|
||||
.filter(|(key, _)| key == "api-key")
|
||||
.map(|(_, value)| value.into_owned())
|
||||
.collect();
|
||||
assert_eq!(keys, [TEST_UUID]);
|
||||
|
||||
let health = build_health_url("https://custom.example/glaive/binary?existing=1").unwrap();
|
||||
assert_eq!(health.as_str(), "https://custom.example/health");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_response_requires_matching_signature() {
|
||||
let signature = Signature::new_unique();
|
||||
let success = format!(r#"{{"result":"{signature}"}}"#);
|
||||
assert!(parse_binary_response(StatusCode::OK, success.as_bytes(), signature).is_ok());
|
||||
|
||||
let mismatch = format!(r#"{{"result":"{}"}}"#, Signature::new_unique());
|
||||
assert!(parse_binary_response(StatusCode::OK, mismatch.as_bytes(), signature).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_response_surfaces_http_and_rpc_errors() {
|
||||
let signature = Signature::new_unique();
|
||||
let rpc_error = br#"{"error":{"code":-32000,"message":"missing tip"}}"#;
|
||||
let error =
|
||||
parse_binary_response(StatusCode::OK, rpc_error, signature).unwrap_err().to_string();
|
||||
assert!(error.contains("-32000"));
|
||||
assert!(error.contains("missing tip"));
|
||||
|
||||
let http_error = br#"{"error":"rate limit exceeded"}"#;
|
||||
let error = parse_binary_response(StatusCode::TOO_MANY_REQUESTS, http_error, signature)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("rate limit exceeded"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
//! Glaive's persistent QUIC submission transport.
|
||||
//!
|
||||
//! Protocol reference: <https://glaive.trade/docs#send-quic>
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use arc_swap::ArcSwap;
|
||||
use quinn::{
|
||||
crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout,
|
||||
TransportConfig,
|
||||
};
|
||||
use solana_sdk::signature::Keypair;
|
||||
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, ToSocketAddrs as _},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{sync::Mutex, time::timeout};
|
||||
use uuid::Uuid;
|
||||
|
||||
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
|
||||
const GLAIVE_QUIC_SNI: &str = "glaive-intake";
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const AUTH_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(10);
|
||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
const MEV_PROTECT_BIT: u8 = 1 << 0;
|
||||
pub const MAX_TRANSACTION_SIZE: usize = 1232;
|
||||
|
||||
pub struct GlaiveQuicClient {
|
||||
endpoint: Endpoint,
|
||||
client_config: ClientConfig,
|
||||
server_addr: SocketAddr,
|
||||
auth_frame: [u8; 17],
|
||||
connection: ArcSwap<Connection>,
|
||||
reconnect: Mutex<()>,
|
||||
}
|
||||
|
||||
impl GlaiveQuicClient {
|
||||
pub async fn connect(
|
||||
endpoint_addr: &str,
|
||||
api_key_uuid: &str,
|
||||
mev_protection: bool,
|
||||
) -> Result<Self> {
|
||||
let auth_frame = build_auth_frame(api_key_uuid, mev_protection)?;
|
||||
let client_config = build_client_config()?;
|
||||
let server_addr = resolve_endpoint(endpoint_addr)?;
|
||||
|
||||
let mut endpoint = Endpoint::client(local_bind_addr(server_addr))
|
||||
.context("create Glaive QUIC endpoint")?;
|
||||
endpoint.set_default_client_config(client_config.clone());
|
||||
|
||||
let connection = connect_once(&endpoint, &client_config, server_addr).await?;
|
||||
authenticate(&connection, &auth_frame).await?;
|
||||
|
||||
Ok(Self {
|
||||
endpoint,
|
||||
client_config,
|
||||
server_addr,
|
||||
auth_frame,
|
||||
connection: ArcSwap::from_pointee(connection),
|
||||
reconnect: Mutex::new(()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn send_transaction(&self, transaction_bytes: &[u8]) -> Result<()> {
|
||||
validate_transaction_size(transaction_bytes)?;
|
||||
|
||||
let stale = self.connection.load_full();
|
||||
match send_once(&stale, transaction_bytes).await {
|
||||
Ok(()) => Ok(()),
|
||||
Err(first_error) => {
|
||||
let connection = self.reconnect_if_stale(&stale).await.with_context(|| {
|
||||
format!("Glaive QUIC reconnect after send failure: {first_error}")
|
||||
})?;
|
||||
send_once(&connection, transaction_bytes)
|
||||
.await
|
||||
.context("Glaive QUIC send failed after reconnect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn reconnect_if_stale(&self, stale: &Arc<Connection>) -> Result<Arc<Connection>> {
|
||||
let _guard = self.reconnect.lock().await;
|
||||
let current = self.connection.load_full();
|
||||
if !Arc::ptr_eq(¤t, stale) && current.close_reason().is_none() {
|
||||
return Ok(current);
|
||||
}
|
||||
|
||||
let connection =
|
||||
connect_once(&self.endpoint, &self.client_config, self.server_addr).await?;
|
||||
authenticate(&connection, &self.auth_frame).await?;
|
||||
self.connection.store(Arc::new(connection));
|
||||
Ok(self.connection.load_full())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for GlaiveQuicClient {
|
||||
fn drop(&mut self) {
|
||||
self.connection.load_full().close(0u32.into(), b"client closing");
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_once(
|
||||
endpoint: &Endpoint,
|
||||
client_config: &ClientConfig,
|
||||
server_addr: SocketAddr,
|
||||
) -> Result<Connection> {
|
||||
let connecting = endpoint
|
||||
.connect_with(client_config.clone(), server_addr, GLAIVE_QUIC_SNI)
|
||||
.context("start Glaive QUIC handshake")?;
|
||||
timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Glaive QUIC connect timeout")?
|
||||
.context("Glaive QUIC handshake failed")
|
||||
}
|
||||
|
||||
async fn authenticate(connection: &Connection, auth_frame: &[u8; 17]) -> Result<()> {
|
||||
timeout(AUTH_TIMEOUT, async {
|
||||
let mut stream = connection.open_uni().await.context("open Glaive auth stream")?;
|
||||
stream.write_all(auth_frame).await.context("write Glaive auth frame")?;
|
||||
stream.finish().context("finish Glaive auth stream")?;
|
||||
Result::<()>::Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Glaive QUIC auth timeout")??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_once(connection: &Connection, transaction_bytes: &[u8]) -> Result<()> {
|
||||
timeout(SEND_TIMEOUT, async {
|
||||
let mut stream = connection.open_uni().await.context("open Glaive transaction stream")?;
|
||||
stream.write_all(transaction_bytes).await.context("write Glaive transaction")?;
|
||||
stream.finish().context("finish Glaive transaction stream")?;
|
||||
Result::<()>::Ok(())
|
||||
})
|
||||
.await
|
||||
.context("Glaive QUIC send timeout")??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_auth_frame(api_key_uuid: &str, mev_protection: bool) -> Result<[u8; 17]> {
|
||||
let api_key =
|
||||
Uuid::parse_str(api_key_uuid.trim()).context("Glaive API key must be a valid UUID v4")?;
|
||||
if api_key.get_version_num() != 4 {
|
||||
anyhow::bail!("Glaive API key must be a UUID v4");
|
||||
}
|
||||
|
||||
let mut frame = [0u8; 17];
|
||||
frame[..16].copy_from_slice(&api_key.as_u128().to_be_bytes());
|
||||
frame[16] = if mev_protection { MEV_PROTECT_BIT } else { 0 };
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn validate_transaction_size(transaction_bytes: &[u8]) -> Result<()> {
|
||||
if transaction_bytes.len() > MAX_TRANSACTION_SIZE {
|
||||
anyhow::bail!(
|
||||
"Glaive QUIC transaction too large: {} bytes (max {})",
|
||||
transaction_bytes.len(),
|
||||
MAX_TRANSACTION_SIZE
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_client_config() -> Result<ClientConfig> {
|
||||
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||
let keypair = Keypair::new();
|
||||
let (certificate, private_key) = new_dummy_x509_certificate(&keypair);
|
||||
|
||||
let mut crypto = rustls::ClientConfig::builder()
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(SkipServerVerification::new())
|
||||
.with_client_auth_cert(vec![certificate], private_key)
|
||||
.context("configure Glaive QUIC client certificate")?;
|
||||
crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()];
|
||||
|
||||
let quic_crypto = QuicClientConfig::try_from(crypto)
|
||||
.context("convert Glaive rustls config to QUIC config")?;
|
||||
let mut client_config = ClientConfig::new(Arc::new(quic_crypto));
|
||||
let mut transport = TransportConfig::default();
|
||||
transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
|
||||
transport.max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT)?));
|
||||
client_config.transport_config(Arc::new(transport));
|
||||
Ok(client_config)
|
||||
}
|
||||
|
||||
fn resolve_endpoint(endpoint_addr: &str) -> Result<SocketAddr> {
|
||||
let mut candidates: Vec<_> = endpoint_addr
|
||||
.to_socket_addrs()
|
||||
.with_context(|| format!("resolve Glaive QUIC endpoint {endpoint_addr}"))?
|
||||
.collect();
|
||||
candidates.sort_by_key(|addr| if addr.is_ipv4() { 0 } else { 1 });
|
||||
candidates
|
||||
.into_iter()
|
||||
.next()
|
||||
.with_context(|| format!("Glaive QUIC endpoint resolved to no addresses: {endpoint_addr}"))
|
||||
}
|
||||
|
||||
fn local_bind_addr(remote: SocketAddr) -> SocketAddr {
|
||||
match remote.ip() {
|
||||
IpAddr::V4(_) => SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0),
|
||||
IpAddr::V6(_) => SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEST_UUID: &str = "00112233-4455-4677-8899-aabbccddeeff";
|
||||
|
||||
#[test]
|
||||
fn auth_frame_is_big_endian_uuid_plus_flags() {
|
||||
let frame = build_auth_frame(TEST_UUID, true).unwrap();
|
||||
assert_eq!(
|
||||
&frame[..16],
|
||||
&[
|
||||
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x46, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd,
|
||||
0xee, 0xff
|
||||
]
|
||||
);
|
||||
assert_eq!(frame[16], MEV_PROTECT_BIT);
|
||||
|
||||
let unprotected = build_auth_frame(TEST_UUID, false).unwrap();
|
||||
assert_eq!(unprotected[16], 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_frame_rejects_non_v4_or_malformed_keys() {
|
||||
assert!(build_auth_frame("not-a-uuid", false).is_err());
|
||||
assert!(build_auth_frame("00112233-4455-1677-8899-aabbccddeeff", false).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_size_matches_solana_packet_limit() {
|
||||
assert!(validate_transaction_size(&vec![0; MAX_TRANSACTION_SIZE]).is_ok());
|
||||
assert!(validate_transaction_size(&vec![0; MAX_TRANSACTION_SIZE + 1]).is_err());
|
||||
}
|
||||
}
|
||||
+106
-11
@@ -4,6 +4,8 @@ pub mod blockrazor;
|
||||
pub mod bloxroute;
|
||||
pub mod common;
|
||||
pub mod flashblock;
|
||||
pub mod glaive;
|
||||
pub mod glaive_quic;
|
||||
pub mod helius;
|
||||
pub mod jito;
|
||||
pub mod lightspeed;
|
||||
@@ -33,20 +35,21 @@ use crate::{
|
||||
SWQOS_ENDPOINTS_ASTRALANE_BINARY, SWQOS_ENDPOINTS_ASTRALANE_PLAIN,
|
||||
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX,
|
||||
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO,
|
||||
SWQOS_ENDPOINTS_LUNARLANDER, SWQOS_ENDPOINTS_LUNARLANDER_QUIC, SWQOS_ENDPOINTS_NEXTBLOCK,
|
||||
SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOLAMI,
|
||||
SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
|
||||
SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
|
||||
SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
|
||||
SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
|
||||
SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_LUNARLANDER, SWQOS_MIN_TIP_NEXTBLOCK,
|
||||
SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_GLAIVE, SWQOS_ENDPOINTS_GLAIVE_QUIC,
|
||||
SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_LUNARLANDER,
|
||||
SWQOS_ENDPOINTS_LUNARLANDER_QUIC, SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1,
|
||||
SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOLAMI, SWQOS_ENDPOINTS_SOYAS,
|
||||
SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL,
|
||||
SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR,
|
||||
SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK,
|
||||
SWQOS_MIN_TIP_GLAIVE, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED,
|
||||
SWQOS_MIN_TIP_LUNARLANDER, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1,
|
||||
SWQOS_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
},
|
||||
swqos::{
|
||||
astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient,
|
||||
flashblock::FlashBlockClient, helius::HeliusClient, jito::JitoClient,
|
||||
flashblock::FlashBlockClient, glaive::GlaiveClient, helius::HeliusClient, jito::JitoClient,
|
||||
lightspeed::LightspeedClient, lunarlander::LunarLanderClient, nextblock::NextBlockClient,
|
||||
node1::Node1Client, node1_quic::Node1QuicClient, solami::SolamiClient,
|
||||
solana_rpc::SolRpcClient, soyas::SoyasClient, speedlanding::SpeedlandingClient,
|
||||
@@ -66,7 +69,7 @@ pub const SWQOS_BLACKLIST: &[SwqosType] = &[
|
||||
|
||||
/// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。
|
||||
/// BlockRazor 支持 gRPC 和 HTTP。
|
||||
/// Node1 支持 QUIC。
|
||||
/// Node1、Glaive 与 Lunar Lander 支持 QUIC。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub enum SwqosTransport {
|
||||
#[default]
|
||||
@@ -126,6 +129,7 @@ pub enum SwqosType {
|
||||
Helius,
|
||||
Solami,
|
||||
LunarLander,
|
||||
Glaive,
|
||||
Default,
|
||||
}
|
||||
|
||||
@@ -150,6 +154,7 @@ impl SwqosType {
|
||||
Self::Helius => "Helius",
|
||||
Self::Solami => "Solami",
|
||||
Self::LunarLander => "LunarLander",
|
||||
Self::Glaive => "Glaive",
|
||||
Self::Default => "Default",
|
||||
}
|
||||
}
|
||||
@@ -172,6 +177,7 @@ impl SwqosType {
|
||||
Self::Helius,
|
||||
Self::Solami,
|
||||
Self::LunarLander,
|
||||
Self::Glaive,
|
||||
Self::Default,
|
||||
]
|
||||
}
|
||||
@@ -215,6 +221,7 @@ pub trait SwqosClientTrait {
|
||||
SwqosType::Helius => SWQOS_MIN_TIP_HELIUS,
|
||||
SwqosType::Solami => SWQOS_MIN_TIP_SOLAMI,
|
||||
SwqosType::LunarLander => SWQOS_MIN_TIP_LUNARLANDER,
|
||||
SwqosType::Glaive => SWQOS_MIN_TIP_GLAIVE,
|
||||
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
|
||||
}
|
||||
}
|
||||
@@ -281,6 +288,10 @@ pub enum SwqosConfig {
|
||||
/// (api_key, region, custom_url, transport). transport=None => QUIC; Some(Http) => HTTP.
|
||||
/// Minimum tip: 0.001 SOL. Apply for API key: https://docs.hellomoon.io/reference/lunar-lander
|
||||
LunarLander(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
/// Glaive(api_key_uuid, region, custom_url, transport).
|
||||
/// transport=None => QUIC (official lowest-latency path, UDP/4000); Some(Http) => binary HTTP.
|
||||
/// Minimum tip: 0.0001 SOL. API and protocol docs: <https://glaive.trade/docs>
|
||||
Glaive(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
|
||||
}
|
||||
|
||||
impl SwqosConfig {
|
||||
@@ -303,6 +314,7 @@ impl SwqosConfig {
|
||||
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
|
||||
SwqosConfig::Solami(_, _, _) => SwqosType::Solami,
|
||||
SwqosConfig::LunarLander(_, _, _, _) => SwqosType::LunarLander,
|
||||
SwqosConfig::Glaive(_, _, _, _) => SwqosType::Glaive,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +345,7 @@ impl SwqosConfig {
|
||||
SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(),
|
||||
SwqosType::Solami => SWQOS_ENDPOINTS_SOLAMI[region as usize].to_string(),
|
||||
SwqosType::LunarLander => SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string(),
|
||||
SwqosType::Glaive => SWQOS_ENDPOINTS_GLAIVE[region as usize].to_string(),
|
||||
SwqosType::Default => "".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -374,6 +387,14 @@ impl SwqosConfig {
|
||||
SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
SwqosType::Glaive => {
|
||||
let use_quic = transport.unwrap_or(SwqosTransport::Quic) == SwqosTransport::Quic;
|
||||
if use_quic {
|
||||
SWQOS_ENDPOINTS_GLAIVE_QUIC[region as usize].to_string()
|
||||
} else {
|
||||
SWQOS_ENDPOINTS_GLAIVE[region as usize].to_string()
|
||||
}
|
||||
}
|
||||
_ => Self::get_endpoint(swqos_type, region, None),
|
||||
}
|
||||
}
|
||||
@@ -568,6 +589,37 @@ impl SwqosConfig {
|
||||
Ok(Arc::new(lunarlander_client))
|
||||
}
|
||||
}
|
||||
SwqosConfig::Glaive(api_key, region, url, transport) => {
|
||||
match transport.unwrap_or(SwqosTransport::Quic) {
|
||||
SwqosTransport::Quic => {
|
||||
let endpoint = url.unwrap_or_else(|| {
|
||||
SWQOS_ENDPOINTS_GLAIVE_QUIC[region as usize].to_string()
|
||||
});
|
||||
let client = GlaiveClient::new_quic(
|
||||
rpc_url.clone(),
|
||||
&endpoint,
|
||||
api_key,
|
||||
mev_protection,
|
||||
)
|
||||
.await?;
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
SwqosTransport::Http => {
|
||||
let endpoint = url
|
||||
.unwrap_or_else(|| SWQOS_ENDPOINTS_GLAIVE[region as usize].to_string());
|
||||
let client = GlaiveClient::new_http(
|
||||
rpc_url.clone(),
|
||||
endpoint,
|
||||
api_key,
|
||||
mev_protection,
|
||||
)?;
|
||||
Ok(Arc::new(client))
|
||||
}
|
||||
SwqosTransport::Grpc => {
|
||||
anyhow::bail!("Glaive does not support the gRPC transport")
|
||||
}
|
||||
}
|
||||
}
|
||||
SwqosConfig::Default(endpoint) => {
|
||||
let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment);
|
||||
let rpc_client = SolRpcClient::new(Arc::new(rpc));
|
||||
@@ -606,4 +658,47 @@ mod tests {
|
||||
|
||||
assert_eq!(endpoint, SWQOS_ENDPOINTS_LUNARLANDER[SwqosRegion::Frankfurt as usize]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glaive_defaults_to_quic_endpoint() {
|
||||
assert!(SwqosType::values().contains(&SwqosType::Glaive));
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::Glaive,
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
);
|
||||
assert_eq!(endpoint, SWQOS_ENDPOINTS_GLAIVE_QUIC[SwqosRegion::Frankfurt as usize]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glaive_http_transport_uses_binary_http_origin() {
|
||||
let endpoint = SwqosConfig::get_endpoint_with_transport(
|
||||
SwqosType::Glaive,
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Http),
|
||||
false,
|
||||
);
|
||||
assert_eq!(endpoint, SWQOS_ENDPOINTS_GLAIVE[SwqosRegion::Frankfurt as usize]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn glaive_rejects_unsupported_grpc_transport_without_connecting() {
|
||||
let result = SwqosConfig::get_swqos_client(
|
||||
"http://127.0.0.1:8899".to_string(),
|
||||
CommitmentConfig::processed(),
|
||||
SwqosConfig::Glaive(
|
||||
"00112233-4455-4677-8899-aabbccddeeff".to_string(),
|
||||
SwqosRegion::Frankfurt,
|
||||
None,
|
||||
Some(SwqosTransport::Grpc),
|
||||
),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let error = result.err().expect("Glaive gRPC config must fail");
|
||||
assert!(error.to_string().contains("does not support the gRPC transport"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user