diff --git a/README.md b/README.md index d9415fb..6cea4fc 100644 --- a/README.md +++ b/README.md @@ -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 = 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 = vec![ - **Plain**: `Some(AstralaneTransport::Plain)` — `/iris`. - **QUIC**: `Some(AstralaneTransport::Quic)` — regional `host:7000` / `:9000` (MEV); same API key. +#### Glaive (Binary HTTP / QUIC) + +Glaive supports binary HTTP and persistent QUIC. The SDK defaults to QUIC because Glaive documents it as the lowest-latency submission path. API keys must be UUID v4 strings. Every transaction must tip at least `0.0001 SOL`; the SDK selects one of Glaive's six official tip accounts. + +```rust +use sol_trade_sdk::{ + swqos::{SwqosConfig, SwqosRegion}, + SwqosTransport, +}; + +let glaive_quic = SwqosConfig::Glaive( + "your_glaive_uuid_v4_api_key".to_string(), + SwqosRegion::Frankfurt, + None, // fra.glaive.trade:4000 + None, // QUIC by default +); + +let glaive_http = SwqosConfig::Glaive( + "your_glaive_uuid_v4_api_key".to_string(), + SwqosRegion::Frankfurt, + None, // http://fra.glaive.trade/binary?api-key=... + Some(SwqosTransport::Http), +); +``` + +- **QUIC** (default): `None` or `Some(SwqosTransport::Quic)`. Uses UDP port `4000`, ALPN `solana-tpu`, SNI `glaive-intake`, one persistent authenticated connection, and one unidirectional stream per transaction. +- **Binary HTTP**: `Some(SwqosTransport::Http)`. Sends raw transaction bytes to `/binary?api-key=...` and keeps the pooled connection warm through `/health`. +- `Some(SwqosTransport::Grpc)` is rejected because Glaive does not expose a gRPC submission protocol. +- **MEV protection**: `.mev_protection(true)` sets QUIC auth flag bit 0 or appends `mev-protect=true` to binary HTTP. +- **Tip configuration**: set the Glaive lane's gas-fee strategy tip to at least `0.0001 SOL`. `.check_min_tip(true)` filters lower values locally; it does not raise the configured tip. +- **Regions**: Amsterdam, Frankfurt, London, and New York are native Glaive PoPs. Other `SwqosRegion` values map to the nearest published endpoint. +- **Mainnet only**: Glaive does not currently publish a testnet endpoint. +- Built-in HTTP origins follow Glaive's documented `http://` endpoints. Prefer the default QUIC mode, or provide a custom HTTPS endpoint if Glaive assigns one. + +See the [official Glaive documentation](https://glaive.trade/docs) 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) diff --git a/README_CN.md b/README_CN.md index 5f54526..dc9c038 100755 --- a/README_CN.md +++ b/README_CN.md @@ -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 = 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 = vec![ - **Plain**:`Some(AstralaneTransport::Plain)` — `/iris`。 - **QUIC**:`Some(AstralaneTransport::Quic)` — 按区域的 `host:7000` / `:9000`(MEV);同一 API key。 +#### Glaive(Binary HTTP / QUIC) + +Glaive 支持 binary HTTP 和持久 QUIC。Glaive 官方将 QUIC 定义为最低延迟路径,因此 SDK 默认使用 QUIC。API key 必须是 UUID v4;每笔交易至少需要 `0.0001 SOL` tip,SDK 会从 Glaive 官方公布的 6 个 tip 账户中选择一个。 + +```rust +use sol_trade_sdk::{ + swqos::{SwqosConfig, SwqosRegion}, + SwqosTransport, +}; + +let glaive_quic = SwqosConfig::Glaive( + "your_glaive_uuid_v4_api_key".to_string(), + SwqosRegion::Frankfurt, + None, // fra.glaive.trade:4000 + None, // 默认 QUIC +); + +let glaive_http = SwqosConfig::Glaive( + "your_glaive_uuid_v4_api_key".to_string(), + SwqosRegion::Frankfurt, + None, // http://fra.glaive.trade/binary?api-key=... + Some(SwqosTransport::Http), +); +``` + +- **QUIC(默认)**:`None` 或 `Some(SwqosTransport::Quic)`。使用 UDP `4000`、ALPN `solana-tpu`、SNI `glaive-intake`;维持一条已鉴权连接,每笔交易使用一个单向流。 +- **Binary HTTP**:`Some(SwqosTransport::Http)`。把原始交易字节提交到 `/binary?api-key=...`,并通过 `/health` 保持连接池热连接。 +- `Some(SwqosTransport::Grpc)` 会直接返回错误,因为 Glaive 没有提供 gRPC 提交协议。 +- **MEV 保护**:`.mev_protection(true)` 会设置 QUIC 鉴权帧的 flag bit 0,或为 binary HTTP 追加 `mev-protect=true`。 +- **Tip 配置**:Glaive 通道的 gas-fee strategy tip 至少应为 `0.0001 SOL`。`.check_min_tip(true)` 会在本地过滤低于该值的配置,但不会自动提高用户设置的 tip。 +- **区域**:Glaive 原生 PoP 包括 Amsterdam、Frankfurt、London 和 New York;其他 `SwqosRegion` 会映射到最近的已公布端点。 +- **仅主网**:Glaive 当前没有 testnet 端点。 +- 内置 HTTP 地址遵循 Glaive 官方文档中的 `http://` 端点。优先使用默认 QUIC;如果 Glaive 为你分配了 HTTPS 地址,也可以通过自定义 URL 使用。 + +凭证、限流和协议详情请参考 [Glaive 官方文档](https://glaive.trade/docs)。 + --- ### 🔧 中间件系统说明 @@ -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) diff --git a/examples/trading_client/README.md b/examples/trading_client/README.md index 0da9ed2..8f3a312 100644 --- a/examples/trading_client/README.md +++ b/examples/trading_client/README.md @@ -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 ``` diff --git a/examples/trading_client/README_CN.md b/examples/trading_client/README_CN.md index fa34363..1030720 100644 --- a/examples/trading_client/README_CN.md +++ b/examples/trading_client/README_CN.md @@ -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 ``` diff --git a/examples/trading_client/src/main.rs b/examples/trading_client/src/main.rs index 4b82428..ac69e0c 100644 --- a/examples/trading_client/src/main.rs +++ b/examples/trading_client/src/main.rs @@ -55,6 +55,20 @@ async fn create_trading_client_simple() -> AnyResult { ), // 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) diff --git a/src/common/types.rs b/src/common/types.rs index 6282096..7c9cbba 100755 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -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 { diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index c667d30..720d284 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -198,6 +198,16 @@ pub const LUNARLANDER_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"), ]; +/// Glaive tip accounts from . +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); + } } diff --git a/src/lib.rs b/src/lib.rs index 3e2485b..7577838 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -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, diff --git a/src/swqos/glaive.rs b/src/swqos/glaive.rs new file mode 100644 index 0000000..70e16a4 --- /dev/null +++ b/src/swqos/glaive.rs @@ -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, + ping_handle: Mutex>>, + }, + Quic(Arc), +} + +pub struct GlaiveClient { + rpc_client: Arc, + backend: GlaiveBackend, +} + +impl GlaiveClient { + pub fn new_http( + rpc_url: String, + endpoint: String, + api_key: String, + mev_protection: bool, + ) -> Result { + 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 { + 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, + 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 { + 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::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 { + 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 { + 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")); + } +} diff --git a/src/swqos/glaive_quic.rs b/src/swqos/glaive_quic.rs new file mode 100644 index 0000000..d1f3763 --- /dev/null +++ b/src/swqos/glaive_quic.rs @@ -0,0 +1,241 @@ +//! Glaive's persistent QUIC submission transport. +//! +//! Protocol reference: + +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, + reconnect: Mutex<()>, +} + +impl GlaiveQuicClient { + pub async fn connect( + endpoint_addr: &str, + api_key_uuid: &str, + mev_protection: bool, + ) -> Result { + 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) -> Result> { + 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 { + 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 { + 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 { + 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()); + } +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 5ef4788..240c155 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -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, Option), + /// 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: + Glaive(String, SwqosRegion, Option, Option), } 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")); + } }