From f10d0309ed3aef2b8229a9ffc7987df5b43542de Mon Sep 17 00:00:00 2001 From: speedlanding Date: Sat, 17 Jan 2026 10:48:19 +0800 Subject: [PATCH] feat(swqos): Add Speedlanding --- src/constants/swqos.rs | 23 +++++ src/swqos/mod.rs | 22 +++- src/swqos/speedlanding.rs | 158 +++++++++++++++++++++++++++++ src/trading/core/async_executor.rs | 4 +- 4 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 src/swqos/speedlanding.rs diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index 88b0502..9429c6a 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -130,6 +130,17 @@ pub const SOYAS_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"), ]; +pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("SpEEdz8S1KorkMZqjMUxfxrmWwofmp6ReNP2Nx6CUmq"), + pubkey!("SpeeDy3GJM4wcrQmk1itRFWgidvxX4rwjTLMv78wwjE"), + pubkey!("SPeEdva37vW8vRtqgYjprQs1g3965icfVN5Rt7SMAyh"), + pubkey!("speEdrSEpox5GUfHWcBc7tQjRuSfUin2yvB7qoYvvJh"), + pubkey!("SPeEDmkHkN3A2roSZf6aZyEMsmrGqTHKqwP51y2Y4rV"), + pubkey!("SpeedLdTJXh2RKpXEaP8JCxkWoUVXhtdPQ1EnxBJMxc"), + pubkey!("SpEediGKLbbXndSYTzwmz6Z3NDgHQLDcTDEvGFkSMH9"), + pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"), +]; + // NewYork, // Frankfurt, // Amsterdam, @@ -260,6 +271,17 @@ pub const SWQOS_ENDPOINTS_SOYAS: [&str; 8] = [ "fra.landing.soyas.xyz:9000", ]; +pub const SWQOS_ENDPOINTS_SPEEDLANDING: [&str; 8] = [ + "nyc.speedlanding.trade:17778", + "fra.speedlanding.trade:17778", + "ams.speedlanding.trade:17778", + "nyc.speedlanding.trade:17778", + "tyo.speedlanding.trade:17778", + "fra.speedlanding.trade:17778", + "nyc.speedlanding.trade:17778", + "fra.speedlanding.trade:17778", +]; + 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; @@ -273,3 +295,4 @@ pub const SWQOS_MIN_TIP_ASTRALANE: f64 = 0.00001; pub const SWQOS_MIN_TIP_STELLIUM: f64 = 0.0001; // Stellium requires minimum 0.001 SOL tip pub const SWQOS_MIN_TIP_LIGHTSPEED: f64 = 0.0001; // Lightspeed requires minimum 0.001 SOL tip pub const SWQOS_MIN_TIP_SOYAS: f64 = 0.001; // Soyas requires minimum 0.001 SOL tip +pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires minimum 0.001 SOL tip diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 57d0b13..85ebab8 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -13,6 +13,7 @@ pub mod astralane; pub mod stellium; pub mod lightspeed; pub mod soyas; +pub mod speedlanding; use std::sync::Arc; @@ -35,7 +36,8 @@ use crate::{ SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_STELLIUM, - SWQOS_ENDPOINTS_SOYAS + SWQOS_ENDPOINTS_SOYAS, + SWQOS_ENDPOINTS_SPEEDLANDING }, swqos::{ bloxroute::BloxrouteClient, @@ -50,7 +52,8 @@ use crate::{ astralane::AstralaneClient, stellium::StelliumClient, lightspeed::LightspeedClient, - soyas::SoyasClient + soyas::SoyasClient, + speedlanding::SpeedlandingClient, } }; @@ -99,6 +102,7 @@ pub enum SwqosType { Stellium, Lightspeed, Soyas, + Speedlanding, Default, } @@ -173,6 +177,9 @@ pub enum SwqosConfig { Lightspeed(String, SwqosRegion, Option), /// Soyas(api_token, region, custom_url) Soyas(String, SwqosRegion, Option), + /// To apply for an API key, please contact -> https://t.me/speedlanding_bot?start=0xzero + /// Minimum tip: 0.001 SOL + Speedlanding(String, SwqosRegion, Option), } impl SwqosConfig { @@ -191,6 +198,7 @@ impl SwqosConfig { SwqosConfig::Stellium(_, _, _) => SwqosType::Stellium, SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed, SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas, + SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding, } } @@ -217,6 +225,7 @@ impl SwqosConfig { SwqosType::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(), SwqosType::Lightspeed => "".to_string(), // Lightspeed requires custom URL with api_key SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(), + SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(), SwqosType::Default => "".to_string(), } } @@ -331,6 +340,15 @@ impl SwqosConfig { ).await?; Ok(Arc::new(soyas_client)) }, + SwqosConfig::Speedlanding(auth_token, region, url) => { + let endpoint = SwqosConfig::get_endpoint(SwqosType::Speedlanding, region, url); + let speedlanding_client = SpeedlandingClient::new( + rpc_url.clone(), + endpoint.to_string(), + auth_token + ).await?; + Ok(Arc::new(speedlanding_client)) + }, SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment( endpoint, diff --git a/src/swqos/speedlanding.rs b/src/swqos/speedlanding.rs new file mode 100644 index 0000000..61118a4 --- /dev/null +++ b/src/swqos/speedlanding.rs @@ -0,0 +1,158 @@ +use anyhow::Context as _; +use anyhow::Result; +use arc_swap::ArcSwap; +use quinn::{ + crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout, + TransportConfig, +}; +use rand::seq::IndexedRandom as _; +use solana_rpc_client::rpc_client::SerializableTransaction; +use solana_sdk::{signature::Keypair, transaction::VersionedTransaction}; +use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification}; +use std::time::Instant; +use std::{ + net::{SocketAddr, ToSocketAddrs as _}, + sync::Arc, + time::Duration, +}; +use tokio::sync::Mutex; + +use crate::common::SolanaRpcClient; +use crate::swqos::common::poll_transaction_confirmation; +use crate::swqos::SwqosClientTrait; +use crate::{ + constants::swqos::SPEEDLANDING_TIP_ACCOUNTS, + swqos::{SwqosType, TradeType}, +}; + +const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu"; +const SPEED_SERVER: &str = "speed-landing"; +const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25); +const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); + +pub struct SpeedlandingClient { + pub rpc_client: Arc, + endpoint: Endpoint, + client_config: ClientConfig, + addr: SocketAddr, + connection: ArcSwap, + reconnect: Mutex<()>, +} + +impl SpeedlandingClient { + pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result { + let rpc_client = SolanaRpcClient::new(rpc_url); + let keypair = Keypair::from_base58_string(&api_key); + let (cert, key) = new_dummy_x509_certificate(&keypair); + let mut crypto = rustls::ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(SkipServerVerification::new()) + .with_client_auth_cert(vec![cert], key) + .context("failed to configure client certificate")?; + + crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()]; + + let client_crypto = QuicClientConfig::try_from(crypto) + .context("failed to convert rustls config into quinn crypto config")?; + let mut client_config = ClientConfig::new(Arc::new(client_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)); + + let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?; + endpoint.set_default_client_config(client_config.clone()); + let addr = endpoint_string + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow::anyhow!("Address not resolved"))?; + let connection = endpoint.connect(addr, SPEED_SERVER)?.await?; + + Ok(Self { + rpc_client: Arc::new(rpc_client), + endpoint, + client_config, + addr, + connection: ArcSwap::from_pointee(connection), + reconnect: Mutex::new(()), + }) + } + + async fn reconnect(&self) -> Result<()> { + let _guard = self.reconnect.try_lock()?; + let connection = self + .endpoint + .connect_with(self.client_config.clone(), self.addr, SPEED_SERVER)? + .await?; + self.connection.store(Arc::new(connection)); + Ok(()) + } + + async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> Result<()> { + let mut stream = connection.open_uni().await?; + stream.write_all(payload).await?; + stream.finish()?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl SwqosClientTrait for SpeedlandingClient { + async fn send_transaction( + &self, + trade_type: TradeType, + transaction: &VersionedTransaction, + wait_confirmation: bool, + ) -> Result<()> { + let start_time = Instant::now(); + let signature = transaction.get_signature(); + let serialized_tx = bincode::serialize(transaction)?; + let connection = self.connection.load_full(); + if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() { + eprintln!(" [speedlanding] {} submission failed, reconnecting", trade_type); + self.reconnect().await?; + let connection = self.connection.load_full(); + if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await { + eprintln!(" [speedlanding] {} submission failed: {:?}", trade_type, e); + return Err(e.into()); + } + } + match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await { + Ok(_) => (), + Err(e) => { + println!(" signature: {:?}", signature); + println!(" [speedlanding] {} confirmation failed: {:?}", trade_type, start_time.elapsed()); + return Err(e); + } + } + if wait_confirmation { + println!(" signature: {:?}", signature); + println!(" [speedlanding] {} confirmed: {:?}", trade_type, start_time.elapsed()); + } + Ok(()) + } + + 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(()) + } + + fn get_tip_account(&self) -> Result { + let tip_account = *SPEEDLANDING_TIP_ACCOUNTS + .choose(&mut rand::rng()) + .or_else(|| SPEEDLANDING_TIP_ACCOUNTS.first()) + .unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::Speedlanding + } +} diff --git a/src/trading/core/async_executor.rs b/src/trading/core/async_executor.rs index 6ffa711..08679f4 100644 --- a/src/trading/core/async_executor.rs +++ b/src/trading/core/async_executor.rs @@ -28,7 +28,8 @@ use crate::{ SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_LIGHTSPEED, - SWQOS_MIN_TIP_SOYAS + SWQOS_MIN_TIP_SOYAS, + SWQOS_MIN_TIP_SPEEDLANDING }, }; @@ -257,6 +258,7 @@ pub async fn execute_parallel( SwqosType::Stellium => SWQOS_MIN_TIP_STELLIUM, SwqosType::Lightspeed => SWQOS_MIN_TIP_LIGHTSPEED, SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS, + SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING, SwqosType::Default => SWQOS_MIN_TIP_DEFAULT, }; if config.2.tip < min_tip {