pub mod astralane; pub mod astralane_quic; pub mod blockrazor; pub mod bloxroute; pub mod common; pub mod flashblock; pub mod helius; pub mod jito; pub mod lightspeed; pub mod nextblock; pub mod node1; pub mod node1_quic; pub mod serialization; pub mod solana_rpc; pub mod soyas; pub mod speedlanding; pub mod stellium; pub mod temporal; pub mod zeroslot; use std::sync::Arc; use solana_commitment_config::CommitmentConfig; use solana_sdk::transaction::VersionedTransaction; use anyhow::Result; use crate::{ common::SolanaRpcClient, constants::swqos::{ 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_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_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, }, 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, }, }; // Tip 账户:`SwqosClient::get_tip_account()` 在各实现里多为静态常量;同一批多路提交时, // 在 `trading::core::async_executor::execute_parallel` 内用局部 `tip_cache`(按 client 指针)去重解析。 /// SWQOS provider blacklist configuration /// Providers added here will be disabled even if configured by user /// To enable a provider, remove it from this list pub const SWQOS_BLACKLIST: &[SwqosType] = &[ SwqosType::NextBlock, // NextBlock is disabled by default ]; /// SWQOS 提交通道:HTTP、gRPC 或 QUIC(低延迟)。 /// BlockRazor 支持 gRPC 和 HTTP。 /// Node1 支持 QUIC。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum SwqosTransport { #[default] Http, Grpc, Quic, } /// Astralane 三种提交方式:QUIC TPU、Plain HTTP(`/iris`)、Binary HTTP(`/irisb` + bincode)。 /// 与全局 [`crate::common::TradeConfig::mev_protection`] 配合:HTTP 加 `mev-protect=true`;QUIC 选 `:9000` / `:7000`。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] pub enum AstralaneTransport { /// Binary over HTTP:`…/irisb?api-key=…&method=sendTransaction`(与 `AstralaneClient` 当前序列化一致)。 #[default] Binary, /// Plain HTTP:`…/iris?…`(非 irisb 路径)。 Plain, /// QUIC(`host:7000`;MEV 时 `host:9000`)。 Quic, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum TradeType { Create, CreateAndBuy, Buy, Sell, } impl std::fmt::Display for TradeType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let s = match self { TradeType::Create => "Create", TradeType::CreateAndBuy => "Create and Buy", TradeType::Buy => "Buy", TradeType::Sell => "Sell", }; write!(f, "{}", s) } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SwqosType { Jito, NextBlock, ZeroSlot, Temporal, Bloxroute, Node1, FlashBlock, BlockRazor, Astralane, Stellium, Lightspeed, Soyas, Speedlanding, Helius, Default, } impl SwqosType { /// Label for log alignment; same as Debug output (e.g. "Soyas", "Speedlanding"). #[inline] pub fn as_str(self) -> &'static str { match self { Self::Jito => "Jito", Self::NextBlock => "NextBlock", Self::ZeroSlot => "ZeroSlot", Self::Temporal => "Temporal", Self::Bloxroute => "Bloxroute", Self::Node1 => "Node1", Self::FlashBlock => "FlashBlock", Self::BlockRazor => "BlockRazor", Self::Astralane => "Astralane", Self::Stellium => "Stellium", Self::Lightspeed => "Lightspeed", Self::Soyas => "Soyas", Self::Speedlanding => "Speedlanding", Self::Helius => "Helius", Self::Default => "Default", } } pub fn values() -> Vec { vec![ Self::Jito, Self::NextBlock, Self::ZeroSlot, Self::Temporal, Self::Bloxroute, Self::Node1, Self::FlashBlock, Self::BlockRazor, Self::Astralane, Self::Stellium, Self::Lightspeed, Self::Soyas, Self::Speedlanding, Self::Helius, Self::Default, ] } } pub type SwqosClient = dyn SwqosClientTrait + Send + Sync + 'static; #[async_trait::async_trait] pub trait SwqosClientTrait { async fn send_transaction( &self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool, ) -> Result<()>; async fn send_transactions( &self, trade_type: TradeType, transactions: &Vec, wait_confirmation: bool, ) -> Result<()>; fn get_tip_account(&self) -> Result; fn get_swqos_type(&self) -> SwqosType; /// Minimum tip in SOL required by this provider. Helius returns lower value when swqos_only is true. #[inline] fn min_tip_sol(&self) -> f64 { match self.get_swqos_type() { SwqosType::Jito => SWQOS_MIN_TIP_JITO, SwqosType::NextBlock => SWQOS_MIN_TIP_NEXTBLOCK, SwqosType::ZeroSlot => SWQOS_MIN_TIP_ZERO_SLOT, SwqosType::Temporal => SWQOS_MIN_TIP_TEMPORAL, SwqosType::Bloxroute => SWQOS_MIN_TIP_BLOXROUTE, SwqosType::Node1 => SWQOS_MIN_TIP_NODE1, SwqosType::FlashBlock => SWQOS_MIN_TIP_FLASHBLOCK, SwqosType::BlockRazor => SWQOS_MIN_TIP_BLOCKRAZOR, SwqosType::Astralane => SWQOS_MIN_TIP_ASTRALANE, 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::Helius => SWQOS_MIN_TIP_HELIUS, SwqosType::Default => SWQOS_MIN_TIP_DEFAULT, } } } /// 地理区域,用于默认 SWQOS 端点下标(见 `constants::swqos`)。 /// /// 各服务商常量表在**缺独立 PoP**时,于**已公布的端点集合内**按地理距离选最近项;[`SwqosRegion::Default`] 不表示地球上的位置,表中为全局/枢纽回退,不适用地理就近。 #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SwqosRegion { NewYork, Frankfurt, Amsterdam, /// Ireland (EU); Jito publishes `dublin.mainnet.block-engine.jito.wtf`. Dublin, SLC, Tokyo, /// Southeast Asia (Singapore); not interchangeable with [`SwqosRegion::Tokyo`]. Singapore, London, LosAngeles, /// 非地理区域:未指定区域时的回退,对应表中全局 URL 或枢纽,**不按地理距离选取**。 Default, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum SwqosConfig { Default(String), /// Jito(uuid, region, custom_url) Jito(String, SwqosRegion, Option), /// NextBlock(api_token, region, custom_url) NextBlock(String, SwqosRegion, Option), /// Bloxroute(api_token, region, custom_url) Bloxroute(String, SwqosRegion, Option), /// Temporal(api_token, region, custom_url) Temporal(String, SwqosRegion, Option), /// ZeroSlot(api_token, region, custom_url) ZeroSlot(String, SwqosRegion, Option), /// Node1(api_token, region, custom_url, transport). transport=None => HTTP; Some(Quic) => QUIC (port 16666, UUID auth). Node1(String, SwqosRegion, Option, Option), /// FlashBlock(api_token, region, custom_url) FlashBlock(String, SwqosRegion, Option), /// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP. BlockRazor(String, SwqosRegion, Option, Option), /// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`](`/irisb`)。 Astralane(String, SwqosRegion, Option, Option), /// Stellium(api_token, region, custom_url) Stellium(String, SwqosRegion, Option), /// Lightspeed(api_key, region, custom_url) - Solana Vibe Station /// Endpoint format: https://.rpc.solanavibestation.com/lightspeed?api_key= /// Minimum tip: 0.001 SOL 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), /// 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), } impl SwqosConfig { pub fn swqos_type(&self) -> SwqosType { match self { SwqosConfig::Default(_) => SwqosType::Default, SwqosConfig::Jito(_, _, _) => SwqosType::Jito, SwqosConfig::NextBlock(_, _, _) => SwqosType::NextBlock, SwqosConfig::Bloxroute(_, _, _) => SwqosType::Bloxroute, SwqosConfig::Temporal(_, _, _) => SwqosType::Temporal, SwqosConfig::ZeroSlot(_, _, _) => SwqosType::ZeroSlot, SwqosConfig::Node1(_, _, _, _) => SwqosType::Node1, SwqosConfig::FlashBlock(_, _, _) => SwqosType::FlashBlock, SwqosConfig::BlockRazor(_, _, _, _) => SwqosType::BlockRazor, SwqosConfig::Astralane(_, _, _, _) => SwqosType::Astralane, SwqosConfig::Stellium(_, _, _) => SwqosType::Stellium, SwqosConfig::Lightspeed(_, _, _) => SwqosType::Lightspeed, SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas, SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding, SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius, } } /// Check if current config is in the blacklist pub fn is_blacklisted(&self) -> bool { SWQOS_BLACKLIST.contains(&self.swqos_type()) } pub fn get_endpoint(swqos_type: SwqosType, region: SwqosRegion, url: Option) -> String { if let Some(custom_url) = url { return custom_url; } match swqos_type { SwqosType::Jito => SWQOS_ENDPOINTS_JITO[region as usize].to_string(), SwqosType::NextBlock => SWQOS_ENDPOINTS_NEXTBLOCK[region as usize].to_string(), SwqosType::ZeroSlot => SWQOS_ENDPOINTS_ZERO_SLOT[region as usize].to_string(), SwqosType::Temporal => SWQOS_ENDPOINTS_TEMPORAL[region as usize].to_string(), SwqosType::Bloxroute => SWQOS_ENDPOINTS_BLOX[region as usize].to_string(), SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(), SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(), SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(), SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string(), 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::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(), SwqosType::Default => "".to_string(), } } pub fn get_endpoint_with_transport( swqos_type: SwqosType, region: SwqosRegion, url: Option, transport: Option, _mev_protection: bool, ) -> String { if let Some(custom_url) = url { return custom_url; } match swqos_type { SwqosType::BlockRazor => { // transport=None 或 transport=Grpc => gRPC; transport=Http => HTTP let use_http = transport.map_or(false, |t| t == SwqosTransport::Http); if use_http { SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string() } else { SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC[region as usize].to_string() } } SwqosType::Node1 => { let use_quic = transport.map_or(false, |t| t == SwqosTransport::Quic); if use_quic { SWQOS_ENDPOINTS_NODE1_QUIC[region as usize].to_string() } else { SWQOS_ENDPOINTS_NODE1[region as usize].to_string() } } _ => Self::get_endpoint(swqos_type, region, None), } } pub async fn get_swqos_client( rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig, mev_protection: bool, ) -> Result> { match swqos_config { SwqosConfig::Jito(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Jito, region, url); let jito_client = JitoClient::new(rpc_url.clone(), endpoint, auth_token); Ok(Arc::new(jito_client)) } SwqosConfig::NextBlock(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::NextBlock, region, url); let nextblock_client = NextBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(nextblock_client)) } SwqosConfig::ZeroSlot(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::ZeroSlot, region, url); let zeroslot_client = ZeroSlotClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(zeroslot_client)) } SwqosConfig::Temporal(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Temporal, region, url); let temporal_client = TemporalClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(temporal_client)) } SwqosConfig::Bloxroute(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Bloxroute, region, url); let bloxroute_client = BloxrouteClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(bloxroute_client)) } SwqosConfig::Node1(auth_token, 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_NODE1_QUIC[region as usize].to_string()); let node1_quic = Node1QuicClient::connect(&quic_endpoint, &auth_token, rpc_url.clone()) .await?; Ok(Arc::new(node1_quic)) } else { let endpoint = SwqosConfig::get_endpoint(SwqosType::Node1, region, url); let node1_client = Node1Client::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(node1_client)) } } SwqosConfig::FlashBlock(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::FlashBlock, region, url); let flashblock_client = FlashBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(flashblock_client)) } SwqosConfig::BlockRazor(auth_token, region, url, transport) => { // BlockRazor: transport=None 或 transport=Grpc 时使用 gRPC,transport=Http 时使用 HTTP let use_http = transport.map_or(false, |t| t == SwqosTransport::Http); let endpoint = SwqosConfig::get_endpoint_with_transport( SwqosType::BlockRazor, region, url, transport, mev_protection, ); if use_http { let blockrazor_client = BlockRazorClient::new_http( rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection, ); Ok(Arc::new(blockrazor_client)) } else { // 使用 gRPC 模式(默认或用户明确指定了 gRPC) let blockrazor_client = BlockRazorClient::new_grpc( rpc_url.clone(), endpoint.to_string(), auth_token, mev_protection, ) .await?; Ok(Arc::new(blockrazor_client)) } } SwqosConfig::Astralane(auth_token, region, url, mode) => { let mode = mode.unwrap_or_default(); match mode { AstralaneTransport::Quic => { let quic_endpoint = url.unwrap_or_else(|| { if mev_protection { SWQOS_ENDPOINTS_ASTRALANE_QUIC_MEV[region as usize].to_string() } else { SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string() } }); let astralane_client = AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token) .await?; Ok(Arc::new(astralane_client)) } AstralaneTransport::Plain => { let endpoint = url.unwrap_or_else(|| { SWQOS_ENDPOINTS_ASTRALANE_PLAIN[region as usize].to_string() }); let astralane_client = AstralaneClient::new( rpc_url.clone(), endpoint, auth_token, mev_protection, ); Ok(Arc::new(astralane_client)) } AstralaneTransport::Binary => { let endpoint = url.unwrap_or_else(|| { SWQOS_ENDPOINTS_ASTRALANE_BINARY[region as usize].to_string() }); let astralane_client = AstralaneClient::new( rpc_url.clone(), endpoint, auth_token, mev_protection, ); Ok(Arc::new(astralane_client)) } } } SwqosConfig::Stellium(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Stellium, region, url); let stellium_client = StelliumClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(stellium_client)) } SwqosConfig::Lightspeed(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Lightspeed, region, url); let lightspeed_client = LightspeedClient::new(rpc_url.clone(), endpoint.to_string(), auth_token); Ok(Arc::new(lightspeed_client)) } SwqosConfig::Soyas(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Soyas, region, url); let soyas_client = SoyasClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).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::Helius(api_key, region, url, swqos_only) => { let swqos_only = swqos_only.unwrap_or(false); let endpoint = SwqosConfig::get_endpoint(SwqosType::Helius, region, url.clone()); let api_key_opt = if api_key.is_empty() { None } else { Some(api_key.clone()) }; let helius_client = HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only); Ok(Arc::new(helius_client)) } SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment); let rpc_client = SolRpcClient::new(Arc::new(rpc)); Ok(Arc::new(rpc_client)) } } } }