Files
sol-trade-sdk/src/swqos/mod.rs
T

523 lines
22 KiB
Rust
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Self> {
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<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()>;
fn get_tip_account(&self) -> Result<String>;
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<String>),
/// NextBlock(api_token, region, custom_url)
NextBlock(String, SwqosRegion, Option<String>),
/// Bloxroute(api_token, region, custom_url)
Bloxroute(String, SwqosRegion, Option<String>),
/// Temporal(api_token, region, custom_url)
Temporal(String, SwqosRegion, Option<String>),
/// ZeroSlot(api_token, region, custom_url)
ZeroSlot(String, SwqosRegion, Option<String>),
/// Node1(api_token, region, custom_url, transport). transport=None => HTTP; Some(Quic) => QUIC (port 16666, UUID auth).
Node1(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// FlashBlock(api_token, region, custom_url)
FlashBlock(String, SwqosRegion, Option<String>),
/// BlockRazor(api_token, region, custom_url, transport). transport=None 或 Grpc => gRPC; Some(Http) => HTTP.
BlockRazor(String, SwqosRegion, Option<String>, Option<SwqosTransport>),
/// Astralane(api_token, region, custom_url, mode). `None` => [`AstralaneTransport::Binary`]`/irisb`)。
Astralane(String, SwqosRegion, Option<String>, Option<AstralaneTransport>),
/// Stellium(api_token, region, custom_url)
Stellium(String, SwqosRegion, Option<String>),
/// Lightspeed(api_key, region, custom_url) - Solana Vibe Station
/// Endpoint format: https://<tier>.rpc.solanavibestation.com/lightspeed?api_key=<key>
/// Minimum tip: 0.001 SOL
Lightspeed(String, SwqosRegion, Option<String>),
/// Soyas(api_token, region, custom_url)
Soyas(String, SwqosRegion, Option<String>),
/// To apply for an API key, please contact -> https://t.me/speedlanding_bot?start=0xzero
/// Minimum tip: 0.001 SOL
Speedlanding(String, SwqosRegion, Option<String>),
/// 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<String>, Option<bool>),
}
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>) -> 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<String>,
transport: Option<SwqosTransport>,
_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<Arc<SwqosClient>> {
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 时使用 gRPCtransport=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))
}
}
}
}