From 77232a2b700dfd1543236365712a9ad0b5f3ab30 Mon Sep 17 00:00:00 2001 From: Sam Kerr Date: Mon, 9 Mar 2026 13:33:39 -0400 Subject: [PATCH 1/4] Add Hello Moon Lunar Lander support --- README.md | 4 +- README_CN.md | 4 +- src/constants/swqos.rs | 28 ++++++ src/swqos/lunarlander.rs | 209 +++++++++++++++++++++++++++++++++++++++ src/swqos/mod.rs | 21 ++++ 5 files changed, 264 insertions(+), 2 deletions(-) create mode 100644 src/swqos/lunarlander.rs diff --git a/README.md b/README.md index da9ead9..a6df576 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ 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 and other services +7. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane, LunarLander and other services 8. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail 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 @@ -122,6 +122,7 @@ let swqos_configs: Vec = vec![ SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None), // Astralane: HTTP (4th param None) or QUIC (Some(SwqosTransport::Quic)); same API key SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP + SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Astralane( "your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, @@ -351,6 +352,7 @@ You can apply for a key through the official website: [Community Website](https: - **BlockRazor**: High-speed transaction execution with API key authentication - **Node1**: High-speed transaction execution with API key authentication - **Astralane**: Blockchain network acceleration (supports HTTP and QUIC; see [Astralane QUIC](#astralane-quic-low-latency) above) +- **LunarLander**: HelloMoon transaction landing service (minimum tip: 0.001 SOL) ## 📁 Project Structure diff --git a/README_CN.md b/README_CN.md index e1039d2..9a782b0 100755 --- a/README_CN.md +++ b/README_CN.md @@ -69,7 +69,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 等服务 +7. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane、LunarLander 等服务 8. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败 9. **统一交易接口**: 使用统一的交易协议枚举进行交易操作 10. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除 @@ -122,6 +122,7 @@ let swqos_configs: Vec = vec![ SwqosConfig::Bloxroute("your api_token".to_string(), SwqosRegion::Frankfurt, None), // Astralane:第4个参数 None 为 HTTP,Some(SwqosTransport::Quic) 为 QUIC;同一 API key SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // HTTP + SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Astralane( "your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, @@ -350,6 +351,7 @@ SDK 不会在每次卖出时通过 RPC 拉取 creator_vault(以避免延迟) - **BlockRazor**: 高速交易执行,支持 API 密钥认证 - **Node1**: 高速交易执行,支持 API 密钥认证 - **Astralane**: 区块链网络加速(支持 HTTP 与 QUIC,见上方 [Astralane QUIC](#astralane-quic低延迟)) +- **LunarLander**: HelloMoon 交易着陆服务(最低小费:0.001 SOL) ## 📁 项目结构 diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index 3499355..db95821 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -155,6 +155,20 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"), ]; +// LunarLander (HelloMoon) tip accounts +pub const LUNARLANDER_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"), + pubkey!("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"), + pubkey!("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"), + pubkey!("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"), + pubkey!("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"), + pubkey!("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"), + pubkey!("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"), + pubkey!("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"), + pubkey!("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"), + pubkey!("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"), +]; + // NewYork, // Frankfurt, // Amsterdam, @@ -314,6 +328,19 @@ pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [ "fra.speedlanding.trade:17778", ]; +/// HelloMoon Lunar Lander: JSON-RPC sendTransaction, auth via ?api-key= query param. +/// Region order: NewYork(NYC), Frankfurt, Amsterdam, SLC(Ashburn), Tokyo, London(→Frankfurt), LosAngeles(→NYC), Default(geolocated). +pub const SWQOS_ENDPOINTS_LUNARLANDER: [&str; 8] = [ + "http://nyc.lunar-lander.hellomoon.io/send", // NewYork + "http://fra.lunar-lander.hellomoon.io/send", // Frankfurt + "http://ams.lunar-lander.hellomoon.io/send", // Amsterdam + "http://ash.lunar-lander.hellomoon.io/send", // SLC (closest: Ashburn) + "http://tyo.lunar-lander.hellomoon.io/send", // Tokyo + "http://fra.lunar-lander.hellomoon.io/send", // London (fallback to Frankfurt) + "http://nyc.lunar-lander.hellomoon.io/send", // LosAngeles (fallback to NYC) + "http://lunar-lander.hellomoon.io/send", // Default (geolocated) +]; + /// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only). /// Region order: NewYork(EWR), Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles(SG), Default(Global). pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [ @@ -343,5 +370,6 @@ pub const SWQOS_MIN_TIP_SOYAS: f64 = 0.001; // Soyas requires minimum 0.001 SOL pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires minimum 0.001 SOL tip /// Helius Sender: 0.0002 SOL when not swqos_only; use SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY when swqos_only=true. pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002; +pub const SWQOS_MIN_TIP_LUNARLANDER: f64 = 0.001; // LunarLander 最低小费 0.001 SOL /// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed). pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005; diff --git a/src/swqos/lunarlander.rs b/src/swqos/lunarlander.rs new file mode 100644 index 0000000..8533816 --- /dev/null +++ b/src/swqos/lunarlander.rs @@ -0,0 +1,209 @@ +//! HelloMoon Lunar Lander SWQOS client. +//! +//! High-performance transaction landing service with keep-alive ping. +//! Auth via `?api-key=` query parameter. Minimum tip: 0.001 SOL. + +use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode}; +use rand::seq::IndexedRandom; +use reqwest::Client; +use serde_json::json; +use std::{sync::Arc, time::Instant}; +use std::sync::atomic::{AtomicBool, Ordering}; + +use std::time::Duration; +use solana_transaction_status::UiTransactionEncoding; + +use anyhow::Result; +use solana_sdk::transaction::VersionedTransaction; +use crate::swqos::{SwqosType, TradeType}; +use crate::swqos::SwqosClientTrait; + +use crate::{common::SolanaRpcClient, constants::swqos::LUNARLANDER_TIP_ACCOUNTS}; + + +#[derive(Clone)] +pub struct LunarLanderClient { + pub endpoint: String, + pub api_key: String, + pub rpc_client: Arc, + pub http_client: Client, + stop_ping: Arc, +} + +#[async_trait::async_trait] +impl SwqosClientTrait for LunarLanderClient { + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { + self.send_transaction(trade_type, transaction, wait_confirmation).await + } + + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec, wait_confirmation: bool) -> Result<()> { + self.send_transactions(trade_type, transactions, wait_confirmation).await + } + + fn get_tip_account(&self) -> Result { + let tip_account = *LUNARLANDER_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| LUNARLANDER_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::LunarLander + } +} + +impl LunarLanderClient { + /// Derive the ping URL from the send endpoint by replacing the last path segment. + fn ping_url(endpoint: &str, api_key: &str) -> String { + // Find the last '/' that is part of the path (after "://") + let scheme_end = endpoint.find("://").map(|i| i + 3).unwrap_or(0); + let base = endpoint[scheme_end..].rfind('/') + .map(|i| &endpoint[..scheme_end + i]) + .unwrap_or(endpoint); + format!("{}/ping?api-key={}", base, api_key) + } + + /// Derive the send URL with the API key query parameter. + fn send_url(endpoint: &str, api_key: &str) -> String { + let separator = if endpoint.contains('?') { '&' } else { '?' }; + format!("{}{}api-key={}", endpoint, separator, api_key) + } + + pub fn new(rpc_url: String, endpoint: String, api_key: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + let http_client = default_http_client_builder().build().unwrap(); + + let stop_ping = Arc::new(AtomicBool::new(false)); + + let client = Self { + rpc_client: Arc::new(rpc_client), + endpoint: endpoint.clone(), + api_key: api_key.clone(), + http_client: http_client.clone(), + stop_ping: stop_ping.clone(), + }; + + // Start ping task + let client_clone = client.clone(); + tokio::spawn(async move { + client_clone.start_ping_task().await; + }); + + client + } + + /// Start periodic ping task to keep connections active. + /// GET /ping every 30s on the same TCP connection (recommended 30-45s by HelloMoon). + async fn start_ping_task(&self) { + let ping_url = Self::ping_url(&self.endpoint, &self.api_key); + let http_client = self.http_client.clone(); + let stop_ping = self.stop_ping.clone(); + + tokio::spawn(async move { + // Immediate first ping to warm connection and reduce first-submit cold start latency + if let Ok(resp) = http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await { + let status = resp.status(); + let _ = resp.bytes().await; + if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() { + eprintln!(" [lunarlander] ping failed with status: {}", status); + } + } + let mut interval = tokio::time::interval(Duration::from_secs(30)); + loop { + interval.tick().await; + if stop_ping.load(Ordering::Relaxed) { + break; + } + match http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await { + Ok(response) => { + let status = response.status(); + let _ = response.bytes().await; + if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() { + eprintln!(" [lunarlander] ping failed with status: {}", status); + } + } + Err(e) => { + if crate::common::sdk_log::sdk_log_enabled() { + eprintln!(" [lunarlander] ping request error: {:?}", e); + } + } + } + } + }); + } + + pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> { + let start_time = Instant::now(); + let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?; + + let request_body = serde_json::to_string(&json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": [ + content, + { "encoding": "base64" } + ] + }))?; + + let url = Self::send_url(&self.endpoint, &self.api_key); + + let response_text = self.http_client.post(&url) + .body(request_body) + .header("Content-Type", "application/json") + .header("Connection", "keep-alive") + .header("Keep-Alive", "timeout=30, max=1000") + .send() + .await? + .text() + .await?; + + // Parse response + if let Ok(response_json) = serde_json::from_str::(&response_text) { + if crate::common::sdk_log::sdk_log_enabled() { + if response_json.get("result").is_some() { + println!(" [lunarlander] {} submitted: {:?}", trade_type, start_time.elapsed()); + } else if let Some(error) = response_json.get("error") { + eprintln!(" [lunarlander] {} submission failed: {:?}", trade_type, error); + } + } + } else if crate::common::sdk_log::sdk_log_enabled() { + eprintln!(" [lunarlander] {} submission failed: {:?}", trade_type, response_text); + } + + let start_time: Instant = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await { + Ok(_) => (), + Err(e) => { + if crate::common::sdk_log::sdk_log_enabled() { + println!(" signature: {:?}", signature); + println!(" [lunarlander] {} confirmation failed: {:?}", trade_type, start_time.elapsed()); + } + return Err(e); + }, + } + if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() { + println!(" signature: {:?}", signature); + println!(" [lunarlander] {} confirmed: {:?}", trade_type, start_time.elapsed()); + } + + Ok(()) + } + + pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec, wait_confirmation: bool) -> Result<()> { + for transaction in transactions { + self.send_transaction(trade_type, transaction, wait_confirmation).await?; + } + Ok(()) + } + + /// Stop the ping task + pub fn stop_ping_task(&self) { + self.stop_ping.store(true, Ordering::Relaxed); + } +} + +impl Drop for LunarLanderClient { + fn drop(&mut self) { + // Stop ping task when client is dropped + self.stop_ping.store(true, Ordering::Relaxed); + } +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index e151e49..f1b2ea9 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -16,6 +16,7 @@ pub mod lightspeed; pub mod soyas; pub mod speedlanding; pub mod helius; +pub mod lunarlander; use std::sync::Arc; @@ -57,6 +58,8 @@ use crate::{ SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_HELIUS, + SWQOS_ENDPOINTS_LUNARLANDER, + SWQOS_MIN_TIP_LUNARLANDER, }, swqos::{ bloxroute::BloxrouteClient, @@ -74,6 +77,7 @@ use crate::{ soyas::SoyasClient, speedlanding::SpeedlandingClient, helius::HeliusClient, + lunarlander::LunarLanderClient, } }; @@ -134,6 +138,7 @@ pub enum SwqosType { Soyas, Speedlanding, Helius, + LunarLander, Default, } @@ -154,6 +159,7 @@ impl SwqosType { Self::Soyas, Self::Speedlanding, Self::Helius, + Self::LunarLander, Self::Default, ] } @@ -185,6 +191,7 @@ pub trait SwqosClientTrait { SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS, SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING, SwqosType::Helius => SWQOS_MIN_TIP_HELIUS, + SwqosType::LunarLander => SWQOS_MIN_TIP_LUNARLANDER, SwqosType::Default => SWQOS_MIN_TIP_DEFAULT, } } @@ -237,6 +244,9 @@ pub enum SwqosConfig { /// Helius Sender: dual routing to validators and Jito. API key optional (custom TPS only). /// (api_key, region, custom_url, swqos_only). swqos_only: None => false (min tip 0.0002 SOL); Some(true) => SWQOS-only (min tip 0.000005 SOL, much lower). Helius(String, SwqosRegion, Option, Option), + /// LunarLander(api_key, region, custom_url) - HelloMoon Lunar Lander + /// Minimum tip: 0.001 SOL + LunarLander(String, SwqosRegion, Option), } impl SwqosConfig { @@ -257,6 +267,7 @@ impl SwqosConfig { SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas, SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding, SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius, + SwqosConfig::LunarLander(_, _, _) => SwqosType::LunarLander, } } @@ -285,6 +296,7 @@ impl SwqosConfig { SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(), SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(), SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(), + SwqosType::LunarLander => SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string(), SwqosType::Default => "".to_string(), } } @@ -429,6 +441,15 @@ impl SwqosConfig { ); Ok(Arc::new(helius_client)) }, + SwqosConfig::LunarLander(api_key, region, url) => { + let endpoint = SwqosConfig::get_endpoint(SwqosType::LunarLander, region, url); + let lunarlander_client = LunarLanderClient::new( + rpc_url.clone(), + endpoint.to_string(), + api_key + ); + Ok(Arc::new(lunarlander_client)) + }, SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment( endpoint, From 8c6557257e35ecc20cf6fcf1b46bbd678c245f05 Mon Sep 17 00:00:00 2001 From: Sam Kerr Date: Mon, 9 Mar 2026 13:48:19 -0400 Subject: [PATCH 2/4] Add a test for Lunar Lander --- src/swqos/lunarlander.rs | 71 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/swqos/lunarlander.rs b/src/swqos/lunarlander.rs index 8533816..be7ee6e 100644 --- a/src/swqos/lunarlander.rs +++ b/src/swqos/lunarlander.rs @@ -207,3 +207,74 @@ impl Drop for LunarLanderClient { self.stop_ping.store(true, Ordering::Relaxed); } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ping_url_default_endpoint() { + let url = LunarLanderClient::ping_url( + "http://nyc.lunar-lander.hellomoon.io/send", + "test-key", + ); + assert_eq!(url, "http://nyc.lunar-lander.hellomoon.io/ping?api-key=test-key"); + } + + #[test] + fn test_ping_url_geolocated_endpoint() { + let url = LunarLanderClient::ping_url( + "http://lunar-lander.hellomoon.io/send", + "test-key", + ); + assert_eq!(url, "http://lunar-lander.hellomoon.io/ping?api-key=test-key"); + } + + #[test] + fn test_ping_url_custom_with_nested_path() { + let url = LunarLanderClient::ping_url( + "http://proxy.example.com/v2/lunar/send", + "key123", + ); + assert_eq!(url, "http://proxy.example.com/v2/lunar/ping?api-key=key123"); + } + + #[test] + fn test_ping_url_custom_pathless() { + // Custom URL with no path — should append /ping to the host + let url = LunarLanderClient::ping_url( + "http://proxy.example.com", + "key123", + ); + assert_eq!(url, "http://proxy.example.com/ping?api-key=key123"); + } + + #[test] + fn test_send_url_simple() { + let url = LunarLanderClient::send_url( + "http://nyc.lunar-lander.hellomoon.io/send", + "test-key", + ); + assert_eq!(url, "http://nyc.lunar-lander.hellomoon.io/send?api-key=test-key"); + } + + #[test] + fn test_send_url_existing_query_params() { + let url = LunarLanderClient::send_url( + "http://proxy.example.com/send?foo=bar", + "test-key", + ); + assert_eq!(url, "http://proxy.example.com/send?foo=bar&api-key=test-key"); + } + + #[test] + fn test_tip_account_is_valid() { + // Verify the tip accounts array is non-empty and accounts parse as pubkeys + assert!(!LUNARLANDER_TIP_ACCOUNTS.is_empty()); + for account in LUNARLANDER_TIP_ACCOUNTS { + // If these were invalid, the pubkey! macro would fail at compile time, + // but verify they are 32 bytes + assert_eq!(account.to_bytes().len(), 32); + } + } +} From 70d1da0b13d0b27b732a4856c78a2c75221799ce Mon Sep 17 00:00:00 2001 From: Joe Abbey Date: Mon, 30 Mar 2026 13:43:26 -0400 Subject: [PATCH 3/4] feat(swqos): add Lunar Lander (HelloMoon) provider with binary HTTP and QUIC support - HTTP binary mode: POST /send-bin with bincode body, x-api-key header, keepalive ping - QUIC mode: port 16888 via lunar-lander-quic-client crate, fire-and-forget uni streams - Dual transport: SwqosConfig::LunarLander(key, region, url, transport) like Astralane - 10 moon-prefix tip accounts, 0.001 SOL minimum tip - Regional endpoints: NYC, Frankfurt, Amsterdam, Ashburn, Tokyo --- Cargo.toml | 1 + src/constants/swqos.rs | 42 ++++++ src/swqos/lunarlander.rs | 290 +++++++++++++++++++++++++++++++++++++++ src/swqos/mod.rs | 57 ++++++-- 4 files changed, 380 insertions(+), 10 deletions(-) create mode 100644 src/swqos/lunarlander.rs diff --git a/Cargo.toml b/Cargo.toml index 37600e3..6de5fbe 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,7 @@ arc-swap = "1.7" sha2 = "0.10" tonic-prost = "0.14.2" quinn = { version = "0.11", default-features = false, features = ["rustls"] } +lunar-lander-quic-client = "0.1" rcgen = "0.13" uuid = "1.11" diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index 3473c24..047d525 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -369,6 +369,47 @@ pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [ "https://sender.helius-rpc.com/fast", ]; +/// Lunar Lander (HelloMoon) tip accounts. +/// Apply for API key: https://docs.hellomoon.io/reference/lunar-lander +pub const LUNARLANDER_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("moon17L6BgxXRX5uHKudAmqVF96xia9h8ygcmG2sL3F"), + pubkey!("moon26Sek222Md7ZydcAGxoKG832DK36CkLrS3PQY4c"), + pubkey!("moon7fwyajcVstMoBnVy7UBcTx87SBtNoGGAaH2Cb8V"), + pubkey!("moonBtH9HvLHjLqi9ivyrMVKgFUsSfrz9BwQ9khhn1u"), + pubkey!("moonCJg8476LNFLptX1qrK8PdRsA1HD1R6XWyu9MB93"), + pubkey!("moonF2sz7qwAtdETnrgxNbjonnhGGjd6r4W4UC9284s"), + pubkey!("moonKfftMiGSak3cezvhEqvkPSzwrmQxQHXuspC96yj"), + pubkey!("moonQBUKBpkifLcTd78bfxxt4PYLwmJ5admLW6cBBs8"), + pubkey!("moonXwpKwoVkMegt5Bc776cSW793X1irL5hHV1vJ3JA"), + pubkey!("moonZ6u9E2fgk6eWd82621eLPHt9zuJuYECXAYjMY1C"), +]; + +/// Lunar Lander HTTP endpoints. Binary tx via POST /send-bin with x-api-key header. +/// Region order: NewYork, Frankfurt, Amsterdam, SLC, Tokyo, London, LosAngeles, Default. +pub const SWQOS_ENDPOINTS_LUNARLANDER: [&str; 8] = [ + "http://nyc-1.prod.lunar-lander.hellomoon.io", + "http://fra-1.prod.lunar-lander.hellomoon.io", + "http://ams-1.prod.lunar-lander.hellomoon.io", + "http://ash-2.prod.lunar-lander.hellomoon.io", // SLC → Ashburn + "http://tyo-1.prod.lunar-lander.hellomoon.io", + "http://fra-1.prod.lunar-lander.hellomoon.io", // London → Frankfurt + "http://nyc-1.prod.lunar-lander.hellomoon.io", // LA → NYC + "http://nyc-1.prod.lunar-lander.hellomoon.io", // Default → NYC +]; + +/// Lunar Lander QUIC endpoints (direct, port 16888). Auth via client cert CN = API key. +/// ALPN: b"lunar-lander-tpu". Fire-and-forget unidirectional streams. +pub const SWQOS_ENDPOINTS_LUNARLANDER_QUIC: [&str; 8] = [ + "nyc-1.prod.lunar-lander.hellomoon.io:16888", + "fra-1.prod.lunar-lander.hellomoon.io:16888", + "ams-1.prod.lunar-lander.hellomoon.io:16888", + "ash-2.prod.lunar-lander.hellomoon.io:16888", // SLC → Ashburn + "tyo-1.prod.lunar-lander.hellomoon.io:16888", + "fra-1.prod.lunar-lander.hellomoon.io:16888", // London → Frankfurt + "nyc-1.prod.lunar-lander.hellomoon.io:16888", // LA → NYC + "nyc-1.prod.lunar-lander.hellomoon.io:16888", // Default → NYC +]; + pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费 pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001; pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001; @@ -387,3 +428,4 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002; /// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed). pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005; +pub const SWQOS_MIN_TIP_LUNARLANDER: f64 = 0.001; diff --git a/src/swqos/lunarlander.rs b/src/swqos/lunarlander.rs new file mode 100644 index 0000000..a50508d --- /dev/null +++ b/src/swqos/lunarlander.rs @@ -0,0 +1,290 @@ +use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation}; +use rand::seq::IndexedRandom; +use reqwest::Client; +use std::{sync::Arc, time::Instant}; + +use crate::swqos::SwqosClientTrait; +use crate::swqos::{SwqosType, TradeType}; +use anyhow::Result; +use bincode::serialize as bincode_serialize; +use solana_sdk::transaction::VersionedTransaction; +use std::time::Duration; + +use crate::{common::SolanaRpcClient, constants::swqos::LUNARLANDER_TIP_ACCOUNTS}; + +use std::sync::atomic::{AtomicBool, Ordering}; +use tokio::task::JoinHandle; + +use lunar_lander_quic_client::LunarLanderQuicClient; +use tokio::sync::Mutex; + +#[derive(Clone)] +pub enum LunarLanderBackend { + Http { + endpoint: String, + auth_token: String, + http_client: Client, + ping_handle: Arc>>>, + stop_ping: Arc, + }, + Quic(Arc>), +} + +#[derive(Clone)] +pub struct LunarLanderClient { + pub rpc_client: Arc, + backend: LunarLanderBackend, +} + +#[async_trait::async_trait] +impl SwqosClientTrait for LunarLanderClient { + 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 = *LUNARLANDER_TIP_ACCOUNTS + .choose(&mut rand::rng()) + .or_else(|| LUNARLANDER_TIP_ACCOUNTS.first()) + .unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::LunarLander + } +} + +impl LunarLanderClient { + /// Create an HTTP binary client (POST /send-bin with bincode body). + pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + let http_client = default_http_client_builder().build().unwrap(); + let ping_handle = Arc::new(tokio::sync::Mutex::new(None)); + let stop_ping = Arc::new(AtomicBool::new(false)); + + let client = Self { + rpc_client: Arc::new(rpc_client), + backend: LunarLanderBackend::Http { + endpoint, + auth_token, + http_client, + ping_handle, + stop_ping, + }, + }; + let client_clone = client.clone(); + tokio::spawn(async move { + client_clone.start_ping_task().await; + }); + client + } + + /// Create a QUIC client (port 16888, cert CN = api_key, fire-and-forget unidirectional streams). + pub async fn new_quic(rpc_url: String, quic_endpoint: &str, api_key: String) -> Result { + let rpc_client = SolanaRpcClient::new(rpc_url); + let quic_client = LunarLanderQuicClient::connect(quic_endpoint, &api_key).await?; + Ok(Self { + rpc_client: Arc::new(rpc_client), + backend: LunarLanderBackend::Quic(Arc::new(Mutex::new(quic_client))), + }) + } + + async fn start_ping_task(&self) { + match &self.backend { + LunarLanderBackend::Http { + endpoint, + auth_token, + http_client, + ping_handle, + stop_ping, + } => { + let endpoint = endpoint.clone(); + let auth_token = auth_token.clone(); + let http_client = http_client.clone(); + let ping_handle = ping_handle.clone(); + let stop_ping = stop_ping.clone(); + let handle = tokio::spawn(async move { + // Immediate first ping to warm connection + let _ = Self::send_ping_request(&http_client, &endpoint, &auth_token).await; + let mut interval = tokio::time::interval(Duration::from_secs(30)); + loop { + interval.tick().await; + if stop_ping.load(Ordering::Relaxed) { + break; + } + if let Err(e) = + Self::send_ping_request(&http_client, &endpoint, &auth_token).await + { + if crate::common::sdk_log::sdk_log_enabled() { + eprintln!("LunarLander ping request failed: {}", e); + } + } + } + }); + let mut guard = ping_handle.lock().await; + if let Some(old) = guard.as_ref() { + old.abort(); + } + *guard = Some(handle); + } + LunarLanderBackend::Quic(_) => {} + } + } + + /// GET {endpoint}/ping for HTTP keepalive. + async fn send_ping_request( + http_client: &Client, + endpoint: &str, + auth_token: &str, + ) -> Result<()> { + let url = format!("{}/ping", endpoint); + let response = http_client + .get(&url) + .header("x-api-key", auth_token) + .timeout(Duration::from_millis(1500)) + .send() + .await?; + let _ = response.bytes().await; + Ok(()) + } + + async fn send_transaction_impl( + &self, + trade_type: TradeType, + transaction: &VersionedTransaction, + wait_confirmation: bool, + ) -> Result<()> { + let start_time = Instant::now(); + let signature = transaction.signatures[0]; + let body_bytes = bincode_serialize(transaction) + .map_err(|e| anyhow::anyhow!("LunarLander binary serialize failed: {}", e))?; + + match &self.backend { + LunarLanderBackend::Http { endpoint, auth_token, http_client, .. } => { + let url = format!("{}/send-bin", endpoint); + let response = http_client + .post(&url) + .header("x-api-key", auth_token) + .header("Content-Type", "application/octet-stream") + .body(body_bytes) + .send() + .await?; + let status = response.status(); + let _ = response.bytes().await; + if status.is_success() { + if crate::common::sdk_log::sdk_log_enabled() { + crate::common::sdk_log::log_swqos_submitted( + "LunarLander", + trade_type, + start_time.elapsed(), + ); + } + } else { + if crate::common::sdk_log::sdk_log_enabled() { + crate::common::sdk_log::log_swqos_submission_failed( + "LunarLander", + trade_type, + start_time.elapsed(), + format!("status {}", status), + ); + } + return Err(anyhow::anyhow!("LunarLander sendTransaction failed: {}", status)); + } + } + LunarLanderBackend::Quic(quic) => { + let send_result = { + let client = quic.lock().await; + client.send_transaction(&body_bytes).await + }; + if let Err(e) = send_result { + // Attempt reconnect on failure + let mut client = quic.lock().await; + if let Err(re) = client.reconnect().await { + if crate::common::sdk_log::sdk_log_enabled() { + crate::common::sdk_log::log_swqos_submission_failed( + "LunarLander", + trade_type, + start_time.elapsed(), + format!("QUIC send failed: {}, reconnect failed: {}", e, re), + ); + } + } + return Err(anyhow::anyhow!("LunarLander QUIC send failed: {}", e)); + } + if crate::common::sdk_log::sdk_log_enabled() { + crate::common::sdk_log::log_swqos_submitted( + "LunarLander", + trade_type, + start_time.elapsed(), + ); + } + } + } + + let start_time = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await { + Ok(_) => (), + Err(e) => { + if crate::common::sdk_log::sdk_log_enabled() { + println!(" signature: {:?}", signature); + println!( + " [{:width$}] {} confirmation failed: {:?}", + "LunarLander", + trade_type, + start_time.elapsed(), + width = crate::common::sdk_log::SWQOS_LABEL_WIDTH + ); + } + return Err(e); + } + } + if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() { + println!(" signature: {:?}", signature); + println!( + " [{:width$}] {} confirmed: {:?}", + "LunarLander", + trade_type, + start_time.elapsed(), + width = crate::common::sdk_log::SWQOS_LABEL_WIDTH + ); + } + Ok(()) + } +} + +impl Drop for LunarLanderClient { + fn drop(&mut self) { + match &self.backend { + LunarLanderBackend::Http { stop_ping, ping_handle, .. } => { + stop_ping.store(true, Ordering::Relaxed); + let ping_handle = ping_handle.clone(); + tokio::spawn(async move { + let mut guard = ping_handle.lock().await; + if let Some(handle) = guard.as_ref() { + handle.abort(); + } + *guard = None; + }); + } + LunarLanderBackend::Quic(_) => {} + } + } +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 9aae951..dd4ec8e 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -7,6 +7,7 @@ pub mod flashblock; pub mod helius; pub mod jito; pub mod lightspeed; +pub mod lunarlander; pub mod nextblock; pub mod node1; pub mod node1_quic; @@ -31,12 +32,13 @@ use crate::{ constants::swqos::{ SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_FLASHBLOCK, - SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_NEXTBLOCK, - SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS, - SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL, - SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR, - SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK, - SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED, + SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_LUNARLANDER, + SWQOS_ENDPOINTS_LUNARLANDER_QUIC, SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1, + SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, + SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, + SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, + SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, + SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_LUNARLANDER, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT, @@ -44,10 +46,10 @@ use crate::{ swqos::{ astralane::AstralaneClient, blockrazor::BlockRazorClient, bloxroute::BloxrouteClient, flashblock::FlashBlockClient, helius::HeliusClient, jito::JitoClient, - lightspeed::LightspeedClient, nextblock::NextBlockClient, node1::Node1Client, - node1_quic::Node1QuicClient, solana_rpc::SolRpcClient, soyas::SoyasClient, - speedlanding::SpeedlandingClient, stellium::StelliumClient, temporal::TemporalClient, - zeroslot::ZeroSlotClient, + lightspeed::LightspeedClient, lunarlander::LunarLanderClient, + nextblock::NextBlockClient, node1::Node1Client, node1_quic::Node1QuicClient, + solana_rpc::SolRpcClient, soyas::SoyasClient, speedlanding::SpeedlandingClient, + stellium::StelliumClient, temporal::TemporalClient, zeroslot::ZeroSlotClient, }, }; @@ -111,6 +113,7 @@ pub enum SwqosType { Soyas, Speedlanding, Helius, + LunarLander, Default, } @@ -133,6 +136,7 @@ impl SwqosType { Self::Soyas => "Soyas", Self::Speedlanding => "Speedlanding", Self::Helius => "Helius", + Self::LunarLander => "LunarLander", Self::Default => "Default", } } @@ -153,6 +157,7 @@ impl SwqosType { Self::Soyas, Self::Speedlanding, Self::Helius, + Self::LunarLander, Self::Default, ] } @@ -194,6 +199,7 @@ pub trait SwqosClientTrait { SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS, SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING, SwqosType::Helius => SWQOS_MIN_TIP_HELIUS, + SwqosType::LunarLander => SWQOS_MIN_TIP_LUNARLANDER, SwqosType::Default => SWQOS_MIN_TIP_DEFAULT, } } @@ -246,6 +252,10 @@ pub enum SwqosConfig { /// Helius Sender: dual routing to validators and Jito. API key optional (custom TPS only). /// (api_key, region, custom_url, swqos_only). swqos_only: None => false (min tip 0.0002 SOL); Some(true) => SWQOS-only (min tip 0.000005 SOL, much lower). Helius(String, SwqosRegion, Option, Option), + /// Lunar Lander (HelloMoon): binary tx via HTTP POST /send-bin or QUIC (port 16888). + /// (api_key, region, custom_url, transport). transport=None => HTTP; Some(Quic) => QUIC. + /// Minimum tip: 0.001 SOL. Apply for API key: https://docs.hellomoon.io/reference/lunar-lander + LunarLander(String, SwqosRegion, Option, Option), } impl SwqosConfig { @@ -266,6 +276,7 @@ impl SwqosConfig { SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas, SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding, SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius, + SwqosConfig::LunarLander(_, _, _, _) => SwqosType::LunarLander, } } @@ -294,6 +305,7 @@ impl SwqosConfig { SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(), SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(), SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(), + SwqosType::LunarLander => SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string(), SwqosType::Default => "".to_string(), } } @@ -334,6 +346,14 @@ impl SwqosConfig { SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string() } } + SwqosType::LunarLander => { + let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic); + if use_quic { + SWQOS_ENDPOINTS_LUNARLANDER_QUIC[region as usize].to_string() + } else { + SWQOS_ENDPOINTS_LUNARLANDER[region as usize].to_string() + } + } _ => Self::get_endpoint(swqos_type, region, url), } } @@ -460,6 +480,23 @@ impl SwqosConfig { HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only); Ok(Arc::new(helius_client)) } + SwqosConfig::LunarLander(api_key, region, url, transport) => { + let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic); + if use_quic { + let quic_endpoint = url.unwrap_or_else(|| { + SWQOS_ENDPOINTS_LUNARLANDER_QUIC[region as usize].to_string() + }); + let lunarlander_client = + LunarLanderClient::new_quic(rpc_url.clone(), &quic_endpoint, api_key) + .await?; + Ok(Arc::new(lunarlander_client)) + } else { + let endpoint = SwqosConfig::get_endpoint(SwqosType::LunarLander, region, url); + let lunarlander_client = + LunarLanderClient::new(rpc_url.clone(), endpoint, api_key); + Ok(Arc::new(lunarlander_client)) + } + } SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment); let rpc_client = SolRpcClient::new(Arc::new(rpc)); From 1d224770d829efcf021aa50de96de62dbe379170 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:03:08 +0800 Subject: [PATCH 4/4] fix: align Lunar Lander with official client --- Cargo.toml | 2 +- src/swqos/lunarlander.rs | 74 ++++++++++++++++++++++------------------ src/swqos/mod.rs | 10 ++++-- 3 files changed, 48 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2d9e32..b1f94b1 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,7 +112,7 @@ sha2 = "0.10" tonic-prost = "0.14.2" # 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime found,QUIC(Speedlanding/Soyas)无法初始化 quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] } -lunar-lander-quic-client = "0.1" +lunar-lander-quic-client = "0.4" rcgen = "0.13" uuid = "1.11" diff --git a/src/swqos/lunarlander.rs b/src/swqos/lunarlander.rs index f513d25..a198c2e 100644 --- a/src/swqos/lunarlander.rs +++ b/src/swqos/lunarlander.rs @@ -15,8 +15,7 @@ use crate::{common::SolanaRpcClient, constants::swqos::LUNARLANDER_TIP_ACCOUNTS} use std::sync::atomic::{AtomicBool, Ordering}; use tokio::task::JoinHandle; -use lunar_lander_quic_client::LunarLanderQuicClient; -use tokio::sync::Mutex; +use lunar_lander_quic_client::{ClientOptions, LunarLanderQuicClient}; #[derive(Clone)] pub enum LunarLanderBackend { @@ -27,7 +26,7 @@ pub enum LunarLanderBackend { ping_handle: Arc>>>, stop_ping: Arc, }, - Quic(Arc>), + Quic(Arc), } #[derive(Clone)] @@ -98,12 +97,22 @@ impl LunarLanderClient { } /// Create a QUIC client (port 16888, cert CN = api_key, fire-and-forget unidirectional streams). - pub async fn new_quic(rpc_url: String, quic_endpoint: &str, api_key: String) -> Result { + pub async fn new_quic( + rpc_url: String, + quic_endpoint: &str, + api_key: String, + mev_protection: bool, + ) -> Result { let rpc_client = SolanaRpcClient::new(rpc_url); - let quic_client = LunarLanderQuicClient::connect(quic_endpoint, &api_key).await?; + let quic_client = LunarLanderQuicClient::connect_with_options( + quic_endpoint, + api_key, + quic_client_options(mev_protection), + ) + .await?; Ok(Self { rpc_client: Arc::new(rpc_client), - backend: LunarLanderBackend::Quic(Arc::new(Mutex::new(quic_client))), + backend: LunarLanderBackend::Quic(Arc::new(quic_client)), }) } @@ -155,7 +164,7 @@ impl LunarLanderClient { endpoint: &str, auth_token: &str, ) -> Result<()> { - let url = format!("{}/ping", endpoint); + let url = format!("{}/ping", endpoint.trim_end_matches('/')); let response = http_client .get(&url) .header("x-api-key", auth_token) @@ -173,13 +182,16 @@ impl LunarLanderClient { wait_confirmation: bool, ) -> Result<()> { let start_time = Instant::now(); - let signature = transaction.signatures[0]; + let signature = *transaction + .signatures + .first() + .ok_or_else(|| anyhow::anyhow!("LunarLander transaction has no signature"))?; let body_bytes = bincode_serialize(transaction) .map_err(|e| anyhow::anyhow!("LunarLander binary serialize failed: {}", e))?; match &self.backend { LunarLanderBackend::Http { endpoint, auth_token, http_client, .. } => { - let url = format!("{}/send-bin", endpoint); + let url = format!("{}/send-bin", endpoint.trim_end_matches('/')); let response = http_client .post(&url) .header("x-api-key", auth_token) @@ -210,25 +222,7 @@ impl LunarLanderClient { } } LunarLanderBackend::Quic(quic) => { - let send_result = { - let client = quic.lock().await; - client.send_transaction(&body_bytes).await - }; - if let Err(e) = send_result { - // Attempt reconnect on failure. - let mut client = quic.lock().await; - if let Err(re) = client.reconnect().await { - if crate::common::sdk_log::sdk_log_enabled() { - crate::common::sdk_log::log_swqos_submission_failed( - "LunarLander", - trade_type, - start_time.elapsed(), - format!("QUIC send failed: {}, reconnect failed: {}", e, re), - ); - } - } - return Err(anyhow::anyhow!("LunarLander QUIC send failed: {}", e)); - } + quic.send_transaction(&body_bytes).await?; if crate::common::sdk_log::sdk_log_enabled() { crate::common::sdk_log::log_swqos_submitted( "LunarLander", @@ -275,16 +269,28 @@ impl Drop for LunarLanderClient { match &self.backend { LunarLanderBackend::Http { stop_ping, ping_handle, .. } => { stop_ping.store(true, Ordering::Relaxed); - let ping_handle = ping_handle.clone(); - tokio::spawn(async move { - let mut guard = ping_handle.lock().await; - if let Some(handle) = guard.as_ref() { + if let Ok(mut guard) = ping_handle.try_lock() { + if let Some(handle) = guard.take() { handle.abort(); } - *guard = None; - }); + } } LunarLanderBackend::Quic(_) => {} } } } + +fn quic_client_options(mev_protection: bool) -> ClientOptions { + ClientOptions { mev_protect: mev_protection, ..ClientOptions::default() } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn quic_options_follow_sdk_mev_protection() { + assert!(!quic_client_options(false).mev_protect); + assert!(quic_client_options(true).mev_protect); + } +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 39a417c..ea71863 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -553,9 +553,13 @@ impl SwqosConfig { let quic_endpoint = url.unwrap_or_else(|| { SWQOS_ENDPOINTS_LUNARLANDER_QUIC[region as usize].to_string() }); - let lunarlander_client = - LunarLanderClient::new_quic(rpc_url.clone(), &quic_endpoint, api_key) - .await?; + let lunarlander_client = LunarLanderClient::new_quic( + rpc_url.clone(), + &quic_endpoint, + api_key, + mev_protection, + ) + .await?; Ok(Arc::new(lunarlander_client)) } else { let endpoint = SwqosConfig::get_endpoint(SwqosType::LunarLander, region, url);