diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index cb257a4..08d42aa 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -171,6 +171,13 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"), ]; +pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("6993ZufwyEDNdB94kciDTGB17ANXguiNH22VmMQU1ami"), + pubkey!("E1BkG293HQocKfCkfPS7tEvs8Enh6FK8pUKi17EH1ami"), + pubkey!("2Ga87xvZwpP9WRsSdiPiWre21cQbDcFhGLmT5EMo1ami"), + pubkey!("CGzT5jzT68vGUWVagQAQqESfrCEHSKYe9DAjfgfC1ami"), +]; + // `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项): // 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。 // @@ -434,6 +441,19 @@ pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [ "https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender ]; +pub const SWQOS_ENDPOINTS_SOLAMI: [&str; 10] = [ + "nyc.landing.solami.fast:11000", + "fra.landing.solami.fast:11000", + "ams.landing.solami.fast:11000", + "ams.landing.solami.fast:11000", // Dublin: no IE PoP; use nearest published EU endpoint + "nyc.landing.solami.fast:11000", // SLC: no Mountain/West PoP published; US fallback + "sgp.landing.solami.fast:11000", // Tokyo: PR only provided SGP for APAC + "sgp.landing.solami.fast:11000", + "fra.landing.solami.fast:11000", + "nyc.landing.solami.fast:11000", + "landing.solami.fast:11000", +]; + 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; @@ -450,6 +470,7 @@ 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_SOLAMI: f64 = 0.0001; /// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed). pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005; @@ -476,6 +497,7 @@ mod tests { &SWQOS_ENDPOINTS_SOYAS, &SWQOS_ENDPOINTS_SPEEDLANDING, &SWQOS_ENDPOINTS_HELIUS, + &SWQOS_ENDPOINTS_SOLAMI, ]; #[test] diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index db775e9..a6e3ad4 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -11,6 +11,7 @@ pub mod nextblock; pub mod node1; pub mod node1_quic; pub mod serialization; +pub mod solami; pub mod solana_rpc; pub mod soyas; pub mod speedlanding; @@ -33,21 +34,21 @@ use crate::{ 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_ENDPOINTS_SOLAMI, 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_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::{ 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, + node1_quic::Node1QuicClient, solami::SolamiClient, solana_rpc::SolRpcClient, + soyas::SoyasClient, speedlanding::SpeedlandingClient, stellium::StelliumClient, + temporal::TemporalClient, zeroslot::ZeroSlotClient, }, }; @@ -121,6 +122,7 @@ pub enum SwqosType { Soyas, Speedlanding, Helius, + Solami, Default, } @@ -143,6 +145,7 @@ impl SwqosType { Self::Soyas => "Soyas", Self::Speedlanding => "Speedlanding", Self::Helius => "Helius", + Self::Solami => "Solami", Self::Default => "Default", } } @@ -163,6 +166,7 @@ impl SwqosType { Self::Soyas, Self::Speedlanding, Self::Helius, + Self::Solami, Self::Default, ] } @@ -204,6 +208,7 @@ pub trait SwqosClientTrait { SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS, SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING, SwqosType::Helius => SWQOS_MIN_TIP_HELIUS, + SwqosType::Solami => SWQOS_MIN_TIP_SOLAMI, SwqosType::Default => SWQOS_MIN_TIP_DEFAULT, } } @@ -264,6 +269,8 @@ 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), + /// Solami(api_key, region, custom_url) + Solami(String, SwqosRegion, Option), } impl SwqosConfig { @@ -284,6 +291,7 @@ impl SwqosConfig { SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas, SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding, SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius, + SwqosConfig::Solami(_, _, _) => SwqosType::Solami, } } @@ -312,6 +320,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::Solami => SWQOS_ENDPOINTS_SOLAMI[region as usize].to_string(), SwqosType::Default => "".to_string(), } } @@ -512,6 +521,12 @@ impl SwqosConfig { HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only); Ok(Arc::new(helius_client)) } + SwqosConfig::Solami(auth_token, region, url) => { + let endpoint = SwqosConfig::get_endpoint(SwqosType::Solami, region, url); + let solami_client = + SolamiClient::new(rpc_url.clone(), endpoint.to_string(), auth_token).await?; + Ok(Arc::new(solami_client)) + } SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment(endpoint, commitment); let rpc_client = SolRpcClient::new(Arc::new(rpc)); diff --git a/src/swqos/solami.rs b/src/swqos/solami.rs new file mode 100644 index 0000000..fb4f991 --- /dev/null +++ b/src/swqos/solami.rs @@ -0,0 +1,162 @@ +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_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::SOLAMI_TIP_ACCOUNTS, + swqos::{SwqosType, TradeType}, +}; + +const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu"; +const SOLAMI_SERVER: &str = "solami-landing"; +const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25); +const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); + +pub struct SolamiClient { + pub rpc_client: Arc, + endpoint: Endpoint, + client_config: ClientConfig, + addr: SocketAddr, + connection: ArcSwap, + reconnect: Mutex<()>, +} + +impl SolamiClient { + 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, SOLAMI_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, SOLAMI_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 SolamiClient { + 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!(" [Solami] {} 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!(" [Solami] {} 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!( + " [Solami] {} confirmation failed: {:?}", + trade_type, + start_time.elapsed() + ); + return Err(e); + } + } + if wait_confirmation { + println!(" signature: {:?}", signature); + println!(" [Solami] {} 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 = *SOLAMI_TIP_ACCOUNTS + .choose(&mut rand::rng()) + .or_else(|| SOLAMI_TIP_ACCOUNTS.first()) + .unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::Solami + } +}