feat(swqos): Add Soyas
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
+9
-3
@@ -198,9 +198,15 @@ impl TradingClient {
|
||||
let mut swqos_clients: Vec<Arc<SwqosClient>> = 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 =
|
||||
|
||||
+50
-15
@@ -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://<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>),
|
||||
}
|
||||
|
||||
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>) -> 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<SwqosClient> {
|
||||
pub async fn get_swqos_client(rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig) -> Result<Arc<SwqosClient>> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SolanaRpcClient>,
|
||||
endpoint: Endpoint,
|
||||
client_config: ClientConfig,
|
||||
addr: SocketAddr,
|
||||
connection: ArcSwap<Connection>,
|
||||
reconnect: Mutex<()>,
|
||||
}
|
||||
|
||||
impl SoyasClient {
|
||||
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
|
||||
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<VersionedTransaction>,
|
||||
) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
self.send_transaction(trade_type, transaction).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_tip_account(&self) -> Result<String> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user