Merge pull request #96 from joe-hellomoon/feat/add-lunarlander-swqos

# Conflicts:
#	Cargo.toml
#	src/constants/swqos.rs
#	src/swqos/lunarlander.rs
#	src/swqos/mod.rs
This commit is contained in:
0xfnzero
2026-07-12 17:55:32 +08:00
6 changed files with 272 additions and 230 deletions
+1
View File
@@ -112,6 +112,7 @@ sha2 = "0.10"
tonic-prost = "0.14.2" tonic-prost = "0.14.2"
# 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime foundQUICSpeedlanding/Soyas)无法初始化 # 须含 runtime-tokio,否则 quinn::Endpoint::client 报 no async runtime foundQUICSpeedlanding/Soyas)无法初始化
quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] } quinn = { version = "0.11", default-features = false, features = ["rustls", "runtime-tokio"] }
lunar-lander-quic-client = "0.1"
rcgen = "0.13" rcgen = "0.13"
uuid = "1.11" uuid = "1.11"
+8 -1
View File
@@ -161,7 +161,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
// Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic // Astralane: 4th param = AstralaneTransport — Binary (default), Plain (/iris), or Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary HTTP /irisb
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None), // Lunar Lander: 4th param None = binary HTTP; Some(SwqosTransport::Quic) = QUIC
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::LunarLander(
"your_hellomoon_api_key".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
),
]; ];
// Create TradeConfig instance // Create TradeConfig instance
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
+8 -1
View File
@@ -161,7 +161,14 @@ let swqos_configs: Vec<SwqosConfig> = vec![
// Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain/iris)或 Quic // Astralane:第4个参数为 AstralaneTransport — Binary(默认)、Plain/iris)或 Quic
SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb SwqosConfig::Astralane("your_astralane_api_key".to_string(), SwqosRegion::Frankfurt, None, None), // Binary /irisb
SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::SpeedLanding("your api_token".to_string(), SwqosRegion::Frankfurt, None),
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None), // Lunar Lander:第4个参数 None 为 binary HTTPSome(SwqosTransport::Quic) 为 QUIC
SwqosConfig::LunarLander("your_hellomoon_api_key".to_string(), SwqosRegion::Frankfurt, None, None),
SwqosConfig::LunarLander(
"your_hellomoon_api_key".to_string(),
SwqosRegion::Frankfurt,
None,
Some(SwqosTransport::Quic),
),
]; ];
// 创建 TradeConfig 实例 // 创建 TradeConfig 实例
let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment) let trade_config = TradeConfig::builder(rpc_url, swqos_configs, commitment)
+27 -12
View File
@@ -447,19 +447,34 @@ pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 10] = [
"fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽 "fra.speedlanding.trade:17778", // Default: 非地理区域;EU 枢纽
]; ];
/// HelloMoon Lunar Lander: JSON-RPC sendTransaction, auth via ?api-key= query param. /// Lunar Lander HTTP endpoints. Binary tx via POST /send-bin with x-api-key header.
/// `SwqosRegion` order; regions without a dedicated published PoP use the nearest available host. /// `SwqosRegion` order; regions without a dedicated published PoP use the nearest available host.
pub const SWQOS_ENDPOINTS_LUNARLANDER: [&str; 10] = [ pub const SWQOS_ENDPOINTS_LUNARLANDER: [&str; 10] = [
"http://nyc.lunar-lander.hellomoon.io/send", // NewYork "http://nyc-1.prod.lunar-lander.hellomoon.io",
"http://fra.lunar-lander.hellomoon.io/send", // Frankfurt "http://fra-1.prod.lunar-lander.hellomoon.io",
"http://ams.lunar-lander.hellomoon.io/send", // Amsterdam "http://ams-1.prod.lunar-lander.hellomoon.io",
"http://ams.lunar-lander.hellomoon.io/send", // Dublin -> Amsterdam "http://ams-1.prod.lunar-lander.hellomoon.io", // Dublin -> Amsterdam
"http://ash.lunar-lander.hellomoon.io/send", // SLC -> Ashburn "http://ash-2.prod.lunar-lander.hellomoon.io", // SLC -> Ashburn
"http://tyo.lunar-lander.hellomoon.io/send", // Tokyo "http://tyo-1.prod.lunar-lander.hellomoon.io",
"http://tyo.lunar-lander.hellomoon.io/send", // Singapore -> Tokyo "http://tyo-1.prod.lunar-lander.hellomoon.io", // Singapore -> Tokyo
"http://fra.lunar-lander.hellomoon.io/send", // London -> Frankfurt "http://fra-1.prod.lunar-lander.hellomoon.io", // London -> Frankfurt
"http://nyc.lunar-lander.hellomoon.io/send", // LosAngeles -> NYC "http://nyc-1.prod.lunar-lander.hellomoon.io", // LosAngeles -> NYC
"http://lunar-lander.hellomoon.io/send", // Default "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; 10] = [
"nyc-1.prod.lunar-lander.hellomoon.io:16888",
"fra-1.prod.lunar-lander.hellomoon.io:16888",
"ams-1.prod.lunar-lander.hellomoon.io:16888",
"ams-1.prod.lunar-lander.hellomoon.io:16888", // Dublin -> Amsterdam
"ash-2.prod.lunar-lander.hellomoon.io:16888", // SLC -> Ashburn
"tyo-1.prod.lunar-lander.hellomoon.io:16888",
"tyo-1.prod.lunar-lander.hellomoon.io:16888", // Singapore -> Tokyo
"fra-1.prod.lunar-lander.hellomoon.io:16888", // London -> Frankfurt
"nyc-1.prod.lunar-lander.hellomoon.io:16888", // LosAngeles -> NYC
"nyc-1.prod.lunar-lander.hellomoon.io:16888", // Default -> NYC
]; ];
/// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only). /// Helius Sender: POST /fast, dual routing to validators and Jito. API key optional (custom TPS only).
@@ -509,7 +524,6 @@ pub const SWQOS_MIN_TIP_SOLAMI: f64 = 0.0001;
pub const SWQOS_MIN_TIP_LUNARLANDER: f64 = 0.001; // LunarLander 最低小费 0.001 SOL 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). /// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005; pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -534,6 +548,7 @@ mod tests {
&SWQOS_ENDPOINTS_SPEEDLANDING, &SWQOS_ENDPOINTS_SPEEDLANDING,
&SWQOS_ENDPOINTS_HELIUS, &SWQOS_ENDPOINTS_HELIUS,
&SWQOS_ENDPOINTS_LUNARLANDER, &SWQOS_ENDPOINTS_LUNARLANDER,
&SWQOS_ENDPOINTS_LUNARLANDER_QUIC,
&SWQOS_ENDPOINTS_SOLAMI, &SWQOS_ENDPOINTS_SOLAMI,
]; ];
+191 -199
View File
@@ -1,34 +1,39 @@
//! HelloMoon Lunar Lander SWQOS client. use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation};
//!
//! 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 rand::seq::IndexedRandom;
use reqwest::Client; use reqwest::Client;
use serde_json::json;
use std::sync::atomic::{AtomicBool, Ordering};
use std::{sync::Arc, time::Instant}; use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait; use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType}; use crate::swqos::{SwqosType, TradeType};
use anyhow::Result; use anyhow::Result;
use bincode::serialize as bincode_serialize;
use solana_sdk::transaction::VersionedTransaction; use solana_sdk::transaction::VersionedTransaction;
use std::time::Duration;
use crate::{common::SolanaRpcClient, constants::swqos::LUNARLANDER_TIP_ACCOUNTS}; 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<tokio::sync::Mutex<Option<JoinHandle<()>>>>,
stop_ping: Arc<AtomicBool>,
},
Quic(Arc<Mutex<LunarLanderQuicClient>>),
}
#[derive(Clone)] #[derive(Clone)]
pub struct LunarLanderClient { pub struct LunarLanderClient {
pub endpoint: String,
pub api_key: String,
pub rpc_client: Arc<SolanaRpcClient>, pub rpc_client: Arc<SolanaRpcClient>,
pub http_client: Client, backend: LunarLanderBackend,
stop_ping: Arc<AtomicBool>,
} }
#[async_trait::async_trait] #[async_trait::async_trait]
@@ -39,7 +44,7 @@ impl SwqosClientTrait for LunarLanderClient {
transaction: &VersionedTransaction, transaction: &VersionedTransaction,
wait_confirmation: bool, wait_confirmation: bool,
) -> Result<()> { ) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
} }
async fn send_transactions( async fn send_transactions(
@@ -48,7 +53,10 @@ impl SwqosClientTrait for LunarLanderClient {
transactions: &Vec<VersionedTransaction>, transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool, wait_confirmation: bool,
) -> Result<()> { ) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await for transaction in transactions {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
} }
fn get_tip_account(&self) -> Result<String> { fn get_tip_account(&self) -> Result<String> {
@@ -65,145 +73,184 @@ impl SwqosClientTrait for LunarLanderClient {
} }
impl LunarLanderClient { impl LunarLanderClient {
/// Derive the ping URL from the send endpoint by replacing the last path segment. /// Create an HTTP binary client (POST /send-bin with bincode body).
fn ping_url(endpoint: &str, api_key: &str) -> String { pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
// 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 rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap(); let http_client = default_http_client_builder().build().unwrap();
let ping_handle = Arc::new(tokio::sync::Mutex::new(None));
let stop_ping = Arc::new(AtomicBool::new(false)); let stop_ping = Arc::new(AtomicBool::new(false));
let client = Self { let client = Self {
rpc_client: Arc::new(rpc_client), rpc_client: Arc::new(rpc_client),
endpoint: endpoint.clone(), backend: LunarLanderBackend::Http {
api_key: api_key.clone(), endpoint,
http_client: http_client.clone(), auth_token,
stop_ping: stop_ping.clone(), http_client,
ping_handle,
stop_ping,
},
}; };
// Start ping task
let client_clone = client.clone(); let client_clone = client.clone();
tokio::spawn(async move { tokio::spawn(async move {
client_clone.start_ping_task().await; client_clone.start_ping_task().await;
}); });
client client
} }
/// Start periodic ping task to keep connections active. /// Create a QUIC client (port 16888, cert CN = api_key, fire-and-forget unidirectional streams).
/// GET /ping every 30s on the same TCP connection (recommended 30-45s by HelloMoon). pub async fn new_quic(rpc_url: String, quic_endpoint: &str, api_key: String) -> Result<Self> {
async fn start_ping_task(&self) { let rpc_client = SolanaRpcClient::new(rpc_url);
let ping_url = Self::ping_url(&self.endpoint, &self.api_key); let quic_client = LunarLanderQuicClient::connect(quic_endpoint, &api_key).await?;
let http_client = self.http_client.clone(); Ok(Self {
let stop_ping = self.stop_ping.clone(); rpc_client: Arc::new(rpc_client),
backend: LunarLanderBackend::Quic(Arc::new(Mutex::new(quic_client))),
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( 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, &self,
trade_type: TradeType, trade_type: TradeType,
transaction: &VersionedTransaction, transaction: &VersionedTransaction,
wait_confirmation: bool, wait_confirmation: bool,
) -> Result<()> { ) -> Result<()> {
let start_time = Instant::now(); let start_time = Instant::now();
let (content, signature) = let signature = transaction.signatures[0];
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?; let body_bytes = bincode_serialize(transaction)
.map_err(|e| anyhow::anyhow!("LunarLander binary serialize failed: {}", e))?;
let request_body = serde_json::to_string(&json!({ match &self.backend {
"jsonrpc": "2.0", LunarLanderBackend::Http { endpoint, auth_token, http_client, .. } => {
"id": 1, let url = format!("{}/send-bin", endpoint);
"method": "sendTransaction", let response = http_client
"params": [ .post(&url)
content, .header("x-api-key", auth_token)
{ "encoding": "base64" } .header("Content-Type", "application/octet-stream")
] .body(body_bytes)
}))?; .send()
.await?;
let url = Self::send_url(&self.endpoint, &self.api_key); let status = response.status();
let _ = response.bytes().await;
let response_text = self if status.is_success() {
.http_client if crate::common::sdk_log::sdk_log_enabled() {
.post(&url) crate::common::sdk_log::log_swqos_submitted(
.body(request_body) "LunarLander",
.header("Content-Type", "application/json") trade_type,
.header("Connection", "keep-alive") start_time.elapsed(),
.header("Keep-Alive", "timeout=30, max=1000") );
.send() }
.await? } else {
.text() if crate::common::sdk_log::sdk_log_enabled() {
.await?; crate::common::sdk_log::log_swqos_submission_failed(
"LunarLander",
// Parse response trade_type,
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) { start_time.elapsed(),
if crate::common::sdk_log::sdk_log_enabled() { format!("status {}", status),
if response_json.get("result").is_some() { );
println!(" [lunarlander] {} submitted: {:?}", trade_type, start_time.elapsed()); }
} else if let Some(error) = response_json.get("error") { return Err(anyhow::anyhow!("LunarLander sendTransaction failed: {}", status));
eprintln!(" [lunarlander] {} submission failed: {:?}", trade_type, error); }
}
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(),
);
} }
} }
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [lunarlander] {} submission failed: {:?}", trade_type, response_text);
} }
let start_time: Instant = Instant::now(); let start_time = Instant::now();
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await { match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() { if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature); println!(" signature: {:?}", signature);
println!( println!(
" [lunarlander] {} confirmation failed: {:?}", " [{:width$}] {} confirmation failed: {:?}",
"LunarLander",
trade_type, trade_type,
start_time.elapsed() start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
); );
} }
return Err(e); return Err(e);
@@ -211,88 +258,33 @@ impl LunarLanderClient {
} }
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() { if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature); println!(" signature: {:?}", signature);
println!(" [lunarlander] {} confirmed: {:?}", trade_type, start_time.elapsed()); println!(
} " [{:width$}] {} confirmed: {:?}",
"LunarLander",
Ok(()) trade_type,
} start_time.elapsed(),
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
pub async fn send_transactions( );
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
} }
Ok(()) Ok(())
} }
/// Stop the ping task
pub fn stop_ping_task(&self) {
self.stop_ping.store(true, Ordering::Relaxed);
}
} }
impl Drop for LunarLanderClient { impl Drop for LunarLanderClient {
fn drop(&mut self) { fn drop(&mut self) {
// Stop ping task when client is dropped match &self.backend {
self.stop_ping.store(true, Ordering::Relaxed); LunarLanderBackend::Http { stop_ping, ping_handle, .. } => {
} stop_ping.store(true, Ordering::Relaxed);
} let ping_handle = ping_handle.clone();
tokio::spawn(async move {
#[cfg(test)] let mut guard = ping_handle.lock().await;
mod tests { if let Some(handle) = guard.as_ref() {
use super::*; handle.abort();
}
#[test] *guard = None;
fn test_ping_url_default_endpoint() { });
let url = }
LunarLanderClient::ping_url("http://nyc.lunar-lander.hellomoon.io/send", "test-key"); LunarLanderBackend::Quic(_) => {}
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);
} }
} }
} }
+37 -17
View File
@@ -34,14 +34,14 @@ use crate::{
SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV,
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX, SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, SWQOS_ENDPOINTS_BLOX,
SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO, SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_HELIUS, SWQOS_ENDPOINTS_JITO,
SWQOS_ENDPOINTS_LUNARLANDER, SWQOS_ENDPOINTS_NEXTBLOCK, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_LUNARLANDER, SWQOS_ENDPOINTS_LUNARLANDER_QUIC, SWQOS_ENDPOINTS_NEXTBLOCK,
SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOLAMI, SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_NODE1_QUIC, SWQOS_ENDPOINTS_SOLAMI,
SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM, SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_SOYAS, SWQOS_ENDPOINTS_SPEEDLANDING, SWQOS_ENDPOINTS_STELLIUM,
SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_MIN_TIP_ASTRALANE,
SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT, SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_BLOXROUTE, SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO, SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_LUNARLANDER, SWQOS_MIN_TIP_NEXTBLOCK, SWQOS_MIN_TIP_NODE1, SWQOS_MIN_TIP_LIGHTSPEED, SWQOS_MIN_TIP_LUNARLANDER, SWQOS_MIN_TIP_NEXTBLOCK,
SWQOS_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING, 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_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
}, },
swqos::{ swqos::{
@@ -277,9 +277,10 @@ pub enum SwqosConfig {
Helius(String, SwqosRegion, Option<String>, Option<bool>), Helius(String, SwqosRegion, Option<String>, Option<bool>),
/// Solami(api_key, region, custom_url) /// Solami(api_key, region, custom_url)
Solami(String, SwqosRegion, Option<String>), Solami(String, SwqosRegion, Option<String>),
/// LunarLander(api_key, region, custom_url) - HelloMoon Lunar Lander /// Lunar Lander (HelloMoon): binary tx via HTTP POST /send-bin or QUIC (port 16888).
/// Minimum tip: 0.001 SOL /// (api_key, region, custom_url, transport). transport=None => HTTP; Some(Quic) => QUIC.
LunarLander(String, SwqosRegion, Option<String>), /// Minimum tip: 0.001 SOL. Apply for API key: https://docs.hellomoon.io/reference/lunar-lander
LunarLander(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
} }
impl SwqosConfig { impl SwqosConfig {
@@ -301,7 +302,7 @@ impl SwqosConfig {
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding, SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius, SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
SwqosConfig::Solami(_, _, _) => SwqosType::Solami, SwqosConfig::Solami(_, _, _) => SwqosType::Solami,
SwqosConfig::LunarLander(_, _, _) => SwqosType::LunarLander, SwqosConfig::LunarLander(_, _, _, _) => SwqosType::LunarLander,
} }
} }
@@ -365,6 +366,14 @@ impl SwqosConfig {
SWQOS_ENDPOINTS_NODE1[region as usize].to_string() SWQOS_ENDPOINTS_NODE1[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, None), _ => Self::get_endpoint(swqos_type, region, None),
} }
} }
@@ -538,11 +547,22 @@ impl SwqosConfig {
SolamiClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?; SolamiClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?;
Ok(Arc::new(solami_client)) Ok(Arc::new(solami_client))
} }
SwqosConfig::LunarLander(api_key, region, url) => { SwqosConfig::LunarLander(api_key, region, url, transport) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::LunarLander, region, url); let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic);
let lunarlander_client = if use_quic {
LunarLanderClient::new(rpc_url.clone(), endpoint.to_string(), api_key); let quic_endpoint = url.unwrap_or_else(|| {
Ok(Arc::new(lunarlander_client)) 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) => { SwqosConfig::Default(endpoint) => {
let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment); let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment);