diff --git a/Cargo.toml b/Cargo.toml index 49852fb..665d688 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ solana-address-lookup-table-interface = "3.0.0" solana-compute-budget-interface = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } solana-transaction-status-client-types = "3.0.0" +solana-tls-utils = "3.0.8" borsh = { version = "1.5.3", features = ["derive"] } isahc = "1.7.2" @@ -106,6 +107,7 @@ parking_lot = "0.12" arc-swap = "1.7" sha2 = "0.10" tonic-prost = "0.14.2" +quinn = {version = "0.11", default-features = false, features = ["rustls"]} # Performance optimization dependencies crossbeam-queue = "0.3" diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index cd24599..c2be678 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -123,6 +123,13 @@ pub const LIGHTSPEED_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("9tYF5yPDC1NP8s6diiB3kAX6ZZnva9DM3iDwJkBRarBB"), ]; +pub const SOYAS_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("soyas4s6L8KWZ8rsSk1mF3d1mQScoTGGAgjk98bF8nP"), + pubkey!("soyascXFW5wEEYiwfEmHy2pNwomqzvggJosGVD6TJdY"), + pubkey!("soyasDBdKjADwPz3xk82U3TNPRDKEWJj7wWLajNHZ1L"), + pubkey!("soyasE2abjBAynmHbGWgEwk4ctBy7JMTUCNrMbjcnyH"), +]; + // NewYork, // Frankfurt, // Amsterdam, @@ -242,6 +249,17 @@ pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [ "http://fra1.flashrpc.com", ]; +pub const SWQOS_ENDPOINTS_SOYAS: [&str; 8] = [ + "nyc.landing.soyas.xyz:9000", + "fra.landing.soyas.xyz:9000", + "ams.landing.soyas.xyz:9000", + "nyc.landing.soyas.xyz:9000", + "nyc.landing.soyas.xyz:9000", + "ams.landing.soyas.xyz:9000", + "nyc.landing.soyas.xyz:9000", + "fra.landing.soyas.xyz:9000", +]; + 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; @@ -254,3 +272,4 @@ pub const SWQOS_MIN_TIP_BLOCKRAZOR: f64 = 0.0001; 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 \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 6d0b21f..f7a5dd8 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,9 +198,15 @@ impl TradingClient { let mut swqos_clients: Vec> = vec![]; for swqos in swqos_configs { - let swqos_client = - SwqosConfig::get_swqos_client(rpc_url.clone(), commitment.clone(), swqos.clone()); - swqos_clients.push(swqos_client); + match SwqosConfig::get_swqos_client(rpc_url.clone(), commitment.clone(), swqos.clone()) + .await + { + Ok(swqos_client) => swqos_clients.push(swqos_client), + Err(err) => eprintln!( + "failed to create {:?} swqos client: {err}. Excluding from swqos list", + swqos.swqos_type() + ), + } } let rpc = diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 80c8c6b..4e36907 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -12,6 +12,7 @@ pub mod blockrazor; pub mod astralane; pub mod stellium; pub mod lightspeed; +pub mod soyas; use std::sync::Arc; @@ -33,7 +34,8 @@ use crate::{ SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_ASTRALANE, - SWQOS_ENDPOINTS_STELLIUM + SWQOS_ENDPOINTS_STELLIUM, + SWQOS_ENDPOINTS_SOYAS }, swqos::{ bloxroute::BloxrouteClient, @@ -47,7 +49,8 @@ use crate::{ blockrazor::BlockRazorClient, astralane::AstralaneClient, stellium::StelliumClient, - lightspeed::LightspeedClient + lightspeed::LightspeedClient, + soyas::SoyasClient } }; @@ -88,6 +91,7 @@ pub enum SwqosType { Astralane, Stellium, Lightspeed, + Soyas, Default, } @@ -105,6 +109,7 @@ impl SwqosType { Self::Astralane, Self::Stellium, Self::Lightspeed, + Self::Soyas, Self::Default, ] } @@ -159,9 +164,29 @@ pub enum SwqosConfig { /// 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), } 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, + } + } + pub fn get_endpoint(swqos_type: SwqosType, region: SwqosRegion, url: Option) -> String { if let Some(custom_url) = url { return custom_url; @@ -179,11 +204,12 @@ impl SwqosConfig { SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[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::Default => "".to_string(), } } - pub fn get_swqos_client(rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig) -> Arc { + pub async fn get_swqos_client(rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig) -> Result> { match swqos_config { SwqosConfig::Jito(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Jito, region, url); @@ -192,7 +218,7 @@ impl SwqosConfig { endpoint, auth_token ); - Arc::new(jito_client) + Ok(Arc::new(jito_client)) } SwqosConfig::NextBlock(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::NextBlock, region, url); @@ -201,7 +227,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(nextblock_client) + Ok(Arc::new(nextblock_client)) }, SwqosConfig::ZeroSlot(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::ZeroSlot, region, url); @@ -210,7 +236,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(zeroslot_client) + Ok(Arc::new(zeroslot_client)) }, SwqosConfig::Temporal(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Temporal, region, url); @@ -219,7 +245,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(temporal_client) + Ok(Arc::new(temporal_client)) }, SwqosConfig::Bloxroute(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Bloxroute, region, url); @@ -228,7 +254,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(bloxroute_client) + Ok(Arc::new(bloxroute_client)) }, SwqosConfig::Node1(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Node1, region, url); @@ -237,7 +263,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(node1_client) + Ok(Arc::new(node1_client)) }, SwqosConfig::FlashBlock(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::FlashBlock, region, url); @@ -246,7 +272,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(flashblock_client) + Ok(Arc::new(flashblock_client)) }, SwqosConfig::BlockRazor(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url); @@ -255,7 +281,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(blockrazor_client) + Ok(Arc::new(blockrazor_client)) }, SwqosConfig::Astralane(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url); @@ -264,7 +290,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(astralane_client) + Ok(Arc::new(astralane_client)) }, SwqosConfig::Stellium(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Stellium, region, url); @@ -273,7 +299,7 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(stellium_client) + Ok(Arc::new(stellium_client)) }, SwqosConfig::Lightspeed(auth_token, region, url) => { let endpoint = SwqosConfig::get_endpoint(SwqosType::Lightspeed, region, url); @@ -282,7 +308,16 @@ impl SwqosConfig { endpoint.to_string(), auth_token ); - Arc::new(lightspeed_client) + 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::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment( @@ -290,7 +325,7 @@ impl SwqosConfig { commitment ); let rpc_client = SolRpcClient::new(Arc::new(rpc)); - Arc::new(rpc_client) + Ok(Arc::new(rpc_client)) } } } diff --git a/src/swqos/soyas.rs b/src/swqos/soyas.rs new file mode 100644 index 0000000..05363c5 --- /dev/null +++ b/src/swqos/soyas.rs @@ -0,0 +1,153 @@ +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::SOYAS_TIP_ACCOUNTS, + swqos::{SwqosType, TradeType}, +}; + +const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu"; +const SOYAS_SERVER: &str = "soyas-landing"; +const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25); +const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60); + +pub struct SoyasClient { + pub rpc_client: Arc, + endpoint: Endpoint, + client_config: ClientConfig, + addr: SocketAddr, + connection: ArcSwap, + reconnect: Mutex<()>, +} + +impl SoyasClient { + 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, SOYAS_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) -> anyhow::Result<()> { + let _guard = self.reconnect.try_lock()?; + let connection = self + .endpoint + .connect_with(self.client_config.clone(), self.addr, SOYAS_SERVER)? + .await?; + self.connection.store(Arc::new(connection)); + Ok(()) + } + + async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> anyhow::Result<()> { + let mut stream = connection.open_uni().await?; + stream.write_all(payload).await?; + stream.finish()?; + Ok(()) + } +} + +#[async_trait::async_trait] +impl SwqosClientTrait for SoyasClient { + async fn send_transaction( + &self, + trade_type: TradeType, + transaction: &VersionedTransaction, + ) -> Result<()> { + 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_ok() { + return Ok(()); + } + eprintln!(" [soyas] failed to send transaction; reconnecting"); + self.reconnect().await?; + let connection = self.connection.load_full(); + Self::try_send_bytes(&connection, &serialized_tx).await?; + + let start_time: Instant = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, *signature).await { + Ok(_) => (), + Err(e) => { + println!(" signature: {:?}", signature); + println!(" [soyas] {} confirmation failed: {:?}", trade_type, start_time.elapsed()); + return Err(e); + } + } + println!(" signature: {:?}", signature); + println!(" [soyas] {} confirmed: {:?}", trade_type, start_time.elapsed()); + Ok(()) + } + + async fn send_transactions( + &self, + trade_type: TradeType, + transactions: &Vec, + ) -> Result<()> { + for transaction in transactions { + self.send_transaction(trade_type, transaction).await?; + } + Ok(()) + } + + fn get_tip_account(&self) -> Result { + let tip_account = *SOYAS_TIP_ACCOUNTS + .choose(&mut rand::rng()) + .or_else(|| SOYAS_TIP_ACCOUNTS.first()) + .unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::Soyas + } +} diff --git a/src/trading/core/async_executor.rs b/src/trading/core/async_executor.rs index e46ef5f..af4c872 100644 --- a/src/trading/core/async_executor.rs +++ b/src/trading/core/async_executor.rs @@ -28,6 +28,7 @@ use crate::{ SWQOS_MIN_TIP_ASTRALANE, SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_LIGHTSPEED, + SWQOS_MIN_TIP_SOYAS }, }; @@ -256,6 +257,7 @@ pub async fn execute_parallel( 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::Default => SWQOS_MIN_TIP_DEFAULT, }; if config.2.tip < min_tip {