style: run rustfmt

This commit is contained in:
hookenful
2026-03-14 18:16:55 +02:00
parent 0ec826fcbd
commit 2abcf3839e
76 changed files with 1763 additions and 1352 deletions
+62 -31
View File
@@ -4,18 +4,18 @@ use reqwest::Client;
use std::{sync::Arc, time::Instant};
use tracing::{error, info, warn};
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use bincode::serialize as bincode_serialize;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use std::time::Duration;
use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
/// Empty body for getHealth POST; avoid per-request allocation.
static PING_BODY: &[u8] = &[];
@@ -42,11 +42,21 @@ pub struct AstralaneClient {
#[async_trait::async_trait]
impl SwqosClientTrait for AstralaneClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await?;
}
@@ -54,7 +64,10 @@ impl SwqosClientTrait for AstralaneClient {
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *ASTRALANE_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ASTRALANE_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *ASTRALANE_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| ASTRALANE_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -100,36 +113,48 @@ impl AstralaneClient {
async fn start_ping_task(&self) {
match &self.backend {
AstralaneBackend::Http { endpoint, auth_token, http_client, ping_handle, stop_ping } => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let http_client = http_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
warn!(target: "sol_trade_sdk", "Astralane ping request failed: {}", e);
AstralaneBackend::Http {
endpoint,
auth_token,
http_client,
ping_handle,
stop_ping,
} => {
let endpoint = endpoint.clone();
let auth_token = auth_token.clone();
let http_client = http_client.clone();
let ping_handle = ping_handle.clone();
let stop_ping = stop_ping.clone();
let handle = tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(30));
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) =
Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
warn!(target: "sol_trade_sdk", "Astralane ping request failed: {}", e);
}
}
});
let mut guard = ping_handle.lock().await;
if let Some(old) = guard.as_ref() {
old.abort();
}
});
let mut guard = ping_handle.lock().await;
if let Some(old) = guard.as_ref() {
old.abort();
}
*guard = Some(handle);
*guard = Some(handle);
}
AstralaneBackend::Quic(_) => {}
}
}
/// Send ping request: POST endpoint?api-key=...&method=getHealth
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
async fn send_ping_request(
http_client: &Client,
endpoint: &str,
auth_token: &str,
) -> Result<()> {
let response = http_client
.post(endpoint)
.query(&[("api-key", auth_token), ("method", "getHealth")])
@@ -145,10 +170,16 @@ impl AstralaneClient {
Ok(())
}
async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction_impl(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let signature = transaction.get_signature();
let body_bytes = bincode_serialize(transaction).map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
let body_bytes = bincode_serialize(transaction)
.map_err(|e| anyhow::anyhow!("Astralane binary serialize failed: {}", e))?;
match &self.backend {
AstralaneBackend::Http { endpoint, auth_token, http_client, .. } => {
+18 -26
View File
@@ -49,14 +49,16 @@ impl AstralaneQuicClient {
/// Generates a self-signed TLS certificate with the API key as the Common Name (CN).
pub async fn connect(server_addr: &str, api_key: &str) -> Result<Self> {
let _ = rustls::crypto::ring::default_provider().install_default();
let addr = SocketAddr::from_str(server_addr).or_else(|_| {
use std::net::ToSocketAddrs;
server_addr
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.ok_or_else(|| anyhow::anyhow!("Cannot resolve address: {}", server_addr))
}).context("Invalid server address")?;
let addr = SocketAddr::from_str(server_addr)
.or_else(|_| {
use std::net::ToSocketAddrs;
server_addr
.to_socket_addrs()
.ok()
.and_then(|mut addrs| addrs.next())
.ok_or_else(|| anyhow::anyhow!("Cannot resolve address: {}", server_addr))
})
.context("Invalid server address")?;
info!("[astralane-quic] Building TLS config (CN = api_key)");
let client_config = Self::build_client_config(api_key)?;
@@ -116,10 +118,8 @@ impl AstralaneQuicClient {
guard.clone()
};
let mut send_stream = conn
.open_uni()
.await
.context("Failed to open unidirectional stream")?;
let mut send_stream =
conn.open_uni().await.context("Failed to open unidirectional stream")?;
send_stream
.write_all(transaction_bytes)
@@ -154,19 +154,15 @@ impl AstralaneQuicClient {
/// Close the connection gracefully.
pub async fn close(&self) {
self.connection
.lock()
.await
.close(error_code::OK.into(), b"client closing");
self.connection.lock().await.close(error_code::OK.into(), b"client closing");
}
fn build_client_config(api_key: &str) -> Result<ClientConfig> {
let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256)?;
let mut cert_params = CertificateParams::new(vec![])?;
cert_params.distinguished_name.push(
rcgen::DnType::CommonName,
rcgen::DnValue::Utf8String(api_key.to_string()),
);
cert_params
.distinguished_name
.push(rcgen::DnType::CommonName, rcgen::DnValue::Utf8String(api_key.to_string()));
let cert = cert_params.self_signed(&key_pair)?;
let cert_der = CertificateDer::from(cert.der().to_vec());
@@ -181,9 +177,7 @@ impl AstralaneQuicClient {
crypto.alpn_protocols = vec![ALPN_ASTRALANE_TPU.to_vec()];
let mut transport = TransportConfig::default();
transport.max_idle_timeout(Some(
IdleTimeout::try_from(Duration::from_secs(30)).unwrap(),
));
transport.max_idle_timeout(Some(IdleTimeout::try_from(Duration::from_secs(30)).unwrap()));
transport.keep_alive_interval(Some(Duration::from_secs(25)));
let mut client_config =
@@ -196,9 +190,7 @@ impl AstralaneQuicClient {
impl Drop for AstralaneQuicClient {
fn drop(&mut self) {
self.connection
.get_mut()
.close(error_code::OK.into(), b"client closing");
self.connection.get_mut().close(error_code::OK.into(), b"client closing");
}
}
+68 -32
View File
@@ -1,20 +1,22 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::BLOCKRAZOR_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
#[derive(Clone)]
pub struct BlockRazorClient {
@@ -28,16 +30,29 @@ pub struct BlockRazorClient {
#[async_trait::async_trait]
impl SwqosClientTrait for BlockRazorClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *BLOCKRAZOR_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *BLOCKRAZOR_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -50,26 +65,23 @@ impl BlockRazorClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
// 官方文档:請求中唯一允許的 header 是 Content-Type: text/plain;避免默认 User-Agent 等导致 500
let http_client = default_http_client_builder()
.user_agent("")
.build()
.unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
let http_client = default_http_client_builder().user_agent("").build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
@@ -79,7 +91,7 @@ impl BlockRazorClient {
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
@@ -87,20 +99,21 @@ impl BlockRazorClient {
eprintln!("BlockRazor ping request failed: {}", e);
}
}
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
let mut interval = tokio::time::interval(Duration::from_secs(30)); // 30s keepalive to avoid server ~5min idle close
loop {
interval.tick().await;
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("BlockRazor ping request failed: {}", e);
}
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
@@ -112,7 +125,11 @@ impl BlockRazorClient {
}
/// Send ping request: POST /v2/health?auth=... (Keep Alive). Only required param: auth.
async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> {
async fn send_ping_request(
http_client: &Client,
endpoint: &str,
auth_token: &str,
) -> Result<()> {
let ping_url = endpoint.replace("/v2/sendTransaction", "/v2/health");
let response = http_client
.post(&ping_url)
@@ -132,11 +149,18 @@ impl BlockRazorClient {
/// Send transaction via v2 API: plain Base64 body, Content-Type: text/plain. Only required URI param: auth.
/// 文档要求:auth 以 URI 参数传入;body 为纯 Base64 编码交易;唯一允许的 header 为 Content-Type: text/plain。
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let response = self.http_client
let response = self
.http_client
.post(&self.endpoint)
.query(&[("auth", self.auth_token.as_str())])
.header("Content-Type", "text/plain")
@@ -153,7 +177,10 @@ impl BlockRazorClient {
} else {
let body = response.text().await.unwrap_or_default();
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [blockrazor] {} submission failed: status {} body: {}", trade_type, status, body);
eprintln!(
" [blockrazor] {} submission failed: status {} body: {}",
trade_type, status, body
);
}
return Err(anyhow::anyhow!(
"BlockRazor sendTransaction failed: status {} body: {}",
@@ -168,10 +195,14 @@ impl BlockRazorClient {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [blockrazor] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [blockrazor] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
@@ -181,7 +212,12 @@ impl BlockRazorClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
@@ -193,7 +229,7 @@ impl Drop for BlockRazorClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
+46 -15
View File
@@ -6,17 +6,16 @@ use rand::seq::IndexedRandom;
use reqwest::Client;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::BLOX_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct BloxrouteClient {
pub endpoint: String,
@@ -27,16 +26,29 @@ pub struct BloxrouteClient {
#[async_trait::async_trait]
impl SwqosClientTrait for BloxrouteClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *BLOX_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOX_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *BLOX_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| BLOX_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -56,9 +68,15 @@ impl BloxrouteClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Single format! for body to avoid json! + to_string() double allocation
let body = format!(
@@ -67,7 +85,9 @@ impl BloxrouteClient {
);
let endpoint = format!("{}/api/v2/submit", self.endpoint);
let response_text = self.http_client.post(&endpoint)
let response_text = self
.http_client
.post(&endpoint)
.body(body)
.header("Content-Type", "application/json")
.header("Authorization", self.auth_token.as_str())
@@ -95,10 +115,14 @@ impl BloxrouteClient {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [bloxroute] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [bloxroute] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
@@ -108,7 +132,12 @@ impl BloxrouteClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
_wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let contents = serialization::serialize_transactions_batch_sync(
@@ -123,7 +152,9 @@ impl BloxrouteClient {
let body = format!(r#"{{"entries":[{}]}}"#, entries);
let endpoint = format!("{}/api/v2/submit-batch", self.endpoint);
let response_text = self.http_client.post(&endpoint)
let response_text = self
.http_client
.post(&endpoint)
.body(body)
.header("Content-Type", "application/json")
.header("Authorization", self.auth_token.as_str())
@@ -144,4 +175,4 @@ impl BloxrouteClient {
Ok(())
}
}
}
+23 -11
View File
@@ -1,9 +1,9 @@
use crate::common::types::SolanaRpcClient;
use crate::swqos::serialization;
use anyhow::Result;
use base64::engine::general_purpose::{self, STANDARD};
use base64::Engine;
use bincode::serialize;
use crate::swqos::serialization;
use reqwest::Client;
use serde_json;
use serde_json::json;
@@ -117,7 +117,11 @@ pub async fn poll_any_transaction_confirmation(
loop {
if start.elapsed() >= timeout {
return Err(anyhow::anyhow!("Transaction confirmation timed out after {}s ({} signatures polled)", timeout.as_secs(), signatures.len()));
return Err(anyhow::anyhow!(
"Transaction confirmation timed out after {}s ({} signatures polled)",
timeout.as_secs(),
signatures.len()
));
}
poll_count += 1;
@@ -205,7 +209,7 @@ pub async fn poll_any_transaction_confirmation(
let ui_err = meta.err.unwrap();
let tx_err: TransactionError =
serde_json::from_value(serde_json::to_value(&ui_err)?)?;
// Use Solana InstructionError codes directly
let mut code = 0u32;
let mut index = None;
@@ -230,7 +234,7 @@ pub async fn poll_any_transaction_confirmation(
}
_ => {}
}
return Err(anyhow::Error::new(TradeError {
code: code,
message: format!("{} {:?}", tx_err, error_msg),
@@ -241,11 +245,16 @@ pub async fn poll_any_transaction_confirmation(
}
}
pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &str, transaction: &Transaction) -> Result<Signature, anyhow::Error> {
pub async fn send_nb_transaction(
client: Client,
endpoint: &str,
auth_token: &str,
transaction: &Transaction,
) -> Result<Signature, anyhow::Error> {
// Serialize transaction
let serialized = bincode::serialize(transaction)
.map_err(|e| anyhow::anyhow!("Transaction serialization failed: {}", e))?;
// Base64 encode
let encoded = STANDARD.encode(serialized);
@@ -266,18 +275,21 @@ pub async fn send_nb_transaction(client: Client, endpoint: &str, auth_token: &st
.await
.map_err(|e| anyhow::anyhow!("Request failed: {}", e))?;
let resp = response.json::<serde_json::Value>().await
let resp = response
.json::<serde_json::Value>()
.await
.map_err(|e| anyhow::anyhow!("Response parsing failed: {}", e))?;
if let Some(reason) = resp["reason"].as_str() {
return Err(anyhow::anyhow!(reason.to_string()));
}
let signature = resp["signature"].as_str()
let signature = resp["signature"]
.as_str()
.ok_or_else(|| anyhow::anyhow!("Missing signature field in response"))?;
let signature = Signature::from_str(signature)
.map_err(|e| anyhow::anyhow!("Invalid signature: {}", e))?;
let signature =
Signature::from_str(signature).map_err(|e| anyhow::anyhow!("Invalid signature: {}", e))?;
Ok(signature)
}
@@ -314,4 +326,4 @@ pub async fn serialize_smart_transaction_and_encode(
_ => return Err(anyhow::anyhow!("Unsupported encoding")),
};
Ok((serialized, *signature))
}
}
+44 -13
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,14 +8,13 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::FLASHBLOCK_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct FlashBlockClient {
pub endpoint: String,
@@ -24,16 +25,29 @@ pub struct FlashBlockClient {
#[async_trait::async_trait]
impl SwqosClientTrait for FlashBlockClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *FLASHBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| FLASHBLOCK_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *FLASHBLOCK_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| FLASHBLOCK_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -49,9 +63,15 @@ impl FlashBlockClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// FlashBlock API format
let request_body = serde_json::to_string(&json!({
@@ -61,7 +81,9 @@ impl FlashBlockClient {
let url = format!("{}/api/v2/submit-batch", self.endpoint);
// Send request to FlashBlock
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body)
.header("Authorization", &self.auth_token)
.header("Content-Type", "application/json")
@@ -88,9 +110,13 @@ impl FlashBlockClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [FlashBlock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [FlashBlock] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
@@ -100,7 +126,12 @@ impl FlashBlockClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
+9 -27
View File
@@ -20,7 +20,9 @@ use std::sync::Arc;
use std::time::Instant;
use crate::common::SolanaRpcClient;
use crate::constants::swqos::{HELIUS_TIP_ACCOUNTS, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY};
use crate::constants::swqos::{
HELIUS_TIP_ACCOUNTS, SWQOS_MIN_TIP_HELIUS, SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY,
};
use crate::swqos::{SwqosClientTrait, SwqosType, TradeType};
#[derive(Clone)]
@@ -43,12 +45,7 @@ impl HeliusClient {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let submit_url = Self::build_submit_url(&endpoint, api_key.as_deref(), swqos_only);
Self {
submit_url,
rpc_client: Arc::new(rpc_client),
http_client,
swqos_only,
}
Self { submit_url, rpc_client: Arc::new(rpc_client), http_client, swqos_only }
}
/// Build URL once at construction; no per-request allocation.
@@ -132,17 +129,10 @@ impl HeliusClient {
return Err(anyhow::anyhow!("Helius Sender error: {}", err_msg));
}
if response_json.get("result").is_some() && crate::common::sdk_log::sdk_log_enabled() {
println!(
" [helius] {} submitted: {:?}",
trade_type,
start_time.elapsed()
);
println!(" [helius] {} submitted: {:?}", trade_type, start_time.elapsed());
}
} else if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(
" [helius] {} submission failed: {:?}",
trade_type, response_text
);
eprintln!(" [helius] {} submission failed: {:?}", trade_type, response_text);
}
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
@@ -159,15 +149,8 @@ impl HeliusClient {
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(
" signature: {:?}",
signature
);
println!(
" [helius] {} confirmed: {:?}",
trade_type,
start_time.elapsed()
);
println!(" signature: {:?}", signature);
println!(" [helius] {} confirmed: {:?}", trade_type, start_time.elapsed());
}
Ok(())
}
@@ -191,8 +174,7 @@ impl SwqosClientTrait for HeliusClient {
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation)
.await?;
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
+38 -17
View File
@@ -1,5 +1,7 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode, FormatBase64VersionedTransaction};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
FormatBase64VersionedTransaction,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -7,14 +9,13 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::JITO_TIP_ACCOUNTS};
pub struct JitoClient {
pub endpoint: String,
pub auth_token: String,
@@ -24,11 +25,21 @@ pub struct JitoClient {
#[async_trait::async_trait]
impl SwqosClientTrait for JitoClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction_impl(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions_impl(trade_type, transactions, wait_confirmation).await
}
@@ -52,9 +63,15 @@ impl JitoClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction_impl(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction_impl(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"id": 1,
@@ -76,8 +93,7 @@ impl JitoClient {
let response = if self.auth_token.is_empty() {
self.http_client.post(&endpoint)
} else {
self.http_client.post(&endpoint)
.header("x-jito-auth", &self.auth_token)
self.http_client.post(&endpoint).header("x-jito-auth", &self.auth_token)
};
let response_text = response
.body(request_body)
@@ -104,7 +120,7 @@ impl JitoClient {
println!(" signature: {:?}", signature);
println!(" [jito] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
@@ -114,9 +130,15 @@ impl JitoClient {
Ok(())
}
pub async fn send_transactions_impl(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, _wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions_impl(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
_wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let txs_base64 = transactions.iter().map(|tx| tx.to_base64_string()).collect::<Vec<String>>();
let txs_base64 =
transactions.iter().map(|tx| tx.to_base64_string()).collect::<Vec<String>>();
let body = serde_json::json!({
"jsonrpc": "2.0",
"method": "sendBundle",
@@ -135,8 +157,7 @@ impl JitoClient {
let response = if self.auth_token.is_empty() {
self.http_client.post(&endpoint)
} else {
self.http_client.post(&endpoint)
.header("x-jito-auth", &self.auth_token)
self.http_client.post(&endpoint).header("x-jito-auth", &self.auth_token)
};
let response_text = response
.body(body.to_string())
@@ -156,4 +177,4 @@ impl JitoClient {
Ok(())
}
}
}
+44 -12
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,10 +8,10 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::LIGHTSPEED_TIP_ACCOUNTS};
@@ -23,16 +25,29 @@ pub struct LightspeedClient {
#[async_trait::async_trait]
impl SwqosClientTrait for LightspeedClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *LIGHTSPEED_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| LIGHTSPEED_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *LIGHTSPEED_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| LIGHTSPEED_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -50,9 +65,15 @@ impl LightspeedClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Lightspeed uses standard Solana JSON-RPC format for sendTransaction
let request_body = serde_json::to_string(&json!({
@@ -70,7 +91,9 @@ impl LightspeedClient {
]
}))?;
let response_text = self.http_client.post(&self.endpoint)
let response_text = self
.http_client
.post(&self.endpoint)
.body(request_body)
.header("Content-Type", "application/json")
.send()
@@ -93,9 +116,13 @@ impl LightspeedClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [lightspeed] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [lightspeed] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
@@ -105,7 +132,12 @@ impl LightspeedClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
+101 -164
View File
@@ -1,22 +1,22 @@
pub mod astralane;
pub mod astralane_quic;
pub mod common;
pub mod serialization;
pub mod solana_rpc;
pub mod jito;
pub mod nextblock;
pub mod zeroslot;
pub mod temporal;
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 flashblock;
pub mod blockrazor;
pub mod astralane;
pub mod stellium;
pub mod lightspeed;
pub mod serialization;
pub mod solana_rpc;
pub mod soyas;
pub mod speedlanding;
pub mod helius;
pub mod stellium;
pub mod temporal;
pub mod zeroslot;
use std::sync::Arc;
@@ -29,55 +29,25 @@ use anyhow::Result;
use crate::{
common::SolanaRpcClient,
constants::swqos::{
SWQOS_ENDPOINTS_BLOX,
SWQOS_ENDPOINTS_JITO,
SWQOS_ENDPOINTS_NEXTBLOCK,
SWQOS_ENDPOINTS_TEMPORAL,
SWQOS_ENDPOINTS_ZERO_SLOT,
SWQOS_ENDPOINTS_NODE1,
SWQOS_ENDPOINTS_NODE1_QUIC,
SWQOS_ENDPOINTS_FLASHBLOCK,
SWQOS_ENDPOINTS_BLOCKRAZOR,
SWQOS_ENDPOINTS_ASTRALANE,
SWQOS_ENDPOINTS_ASTRALANE_QUIC,
SWQOS_ENDPOINTS_STELLIUM,
SWQOS_ENDPOINTS_SOYAS,
SWQOS_ENDPOINTS_SPEEDLANDING,
SWQOS_ENDPOINTS_HELIUS,
SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_NEXTBLOCK,
SWQOS_MIN_TIP_ZERO_SLOT,
SWQOS_MIN_TIP_TEMPORAL,
SWQOS_MIN_TIP_BLOXROUTE,
SWQOS_MIN_TIP_NODE1,
SWQOS_MIN_TIP_FLASHBLOCK,
SWQOS_MIN_TIP_BLOCKRAZOR,
SWQOS_MIN_TIP_ASTRALANE,
SWQOS_MIN_TIP_STELLIUM,
SWQOS_MIN_TIP_LIGHTSPEED,
SWQOS_MIN_TIP_SOYAS,
SWQOS_MIN_TIP_SPEEDLANDING,
SWQOS_MIN_TIP_HELIUS,
SWQOS_ENDPOINTS_ASTRALANE, SWQOS_ENDPOINTS_ASTRALANE_QUIC, SWQOS_ENDPOINTS_BLOCKRAZOR,
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::{
bloxroute::BloxrouteClient,
jito::JitoClient,
nextblock::NextBlockClient,
solana_rpc::SolRpcClient,
temporal::TemporalClient,
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::Node1Client,
node1_quic::Node1QuicClient,
flashblock::FlashBlockClient,
blockrazor::BlockRazorClient,
astralane::AstralaneClient,
stellium::StelliumClient,
lightspeed::LightspeedClient,
soyas::SoyasClient,
speedlanding::SpeedlandingClient,
helius::HeliusClient,
}
},
};
lazy_static::lazy_static! {
@@ -90,7 +60,7 @@ lazy_static::lazy_static! {
/// 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
SwqosType::NextBlock, // NextBlock is disabled by default
];
/// SWQOS 提交通道:HTTP 或 QUIC(低延迟)。部分提供商(如 Astralane)支持 QUIC。
@@ -166,8 +136,18 @@ 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<()>;
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.
@@ -243,7 +223,7 @@ pub enum SwqosConfig {
}
impl SwqosConfig {
pub fn swqos_type(&self) -> SwqosType{
pub fn swqos_type(&self) -> SwqosType {
match self {
SwqosConfig::Default(_) => SwqosType::Default,
SwqosConfig::Jito(_, _, _) => SwqosType::Jito,
@@ -292,167 +272,124 @@ impl SwqosConfig {
}
}
pub async fn get_swqos_client(rpc_url: String, commitment: CommitmentConfig, swqos_config: SwqosConfig) -> Result<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);
let jito_client = JitoClient::new(
rpc_url.clone(),
endpoint,
auth_token
);
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
);
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
);
let zeroslot_client =
ZeroSlotClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(zeroslot_client))
},
SwqosConfig::Temporal(auth_token, region, url) => {
}
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
);
let temporal_client =
TemporalClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(temporal_client))
},
SwqosConfig::Bloxroute(auth_token, region, url) => {
}
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
);
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?;
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,
);
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
);
let flashblock_client =
FlashBlockClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(flashblock_client))
},
}
SwqosConfig::BlockRazor(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region, url);
let blockrazor_client = BlockRazorClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
);
let blockrazor_client =
BlockRazorClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
Ok(Arc::new(blockrazor_client))
},
}
SwqosConfig::Astralane(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_ASTRALANE_QUIC[region as usize].to_string());
let quic_endpoint = url.unwrap_or_else(|| {
SWQOS_ENDPOINTS_ASTRALANE_QUIC[region as usize].to_string()
});
let astralane_client =
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token).await?;
AstralaneClient::new_quic(rpc_url.clone(), &quic_endpoint, auth_token)
.await?;
Ok(Arc::new(astralane_client))
} else {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url);
let astralane_client = AstralaneClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token,
);
let astralane_client =
AstralaneClient::new(rpc_url.clone(), endpoint.to_string(), auth_token);
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
);
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
);
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?;
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?;
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,
);
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 = SolanaRpcClient::new_with_commitment(endpoint, commitment);
let rpc_client = SolRpcClient::new(Arc::new(rpc));
Ok(Arc::new(rpc_client))
}
}
}
}
}
+45 -13
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,10 +8,10 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::NEXTBLOCK_TIP_ACCOUNTS};
@@ -23,16 +25,29 @@ pub struct NextBlockClient {
#[async_trait::async_trait]
impl SwqosClientTrait for NextBlockClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *NEXTBLOCK_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| NEXTBLOCK_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -54,9 +69,15 @@ impl NextBlockClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"transaction": {
@@ -65,7 +86,9 @@ impl NextBlockClient {
"frontRunningProtection": false
}))?;
let response_text = self.http_client.post(&self.endpoint)
let response_text = self
.http_client
.post(&self.endpoint)
.body(request_body)
.header("Authorization", &self.auth_token)
.header("Content-Type", "application/json")
@@ -89,9 +112,13 @@ impl NextBlockClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [nextblock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [nextblock] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
@@ -101,10 +128,15 @@ impl NextBlockClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
}
}
+65 -30
View File
@@ -1,21 +1,23 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::NODE1_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
#[derive(Clone)]
pub struct Node1Client {
@@ -29,16 +31,29 @@ pub struct Node1Client {
#[async_trait::async_trait]
impl SwqosClientTrait for Node1Client {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *NODE1_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NODE1_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *NODE1_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| NODE1_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -51,22 +66,22 @@ impl Node1Client {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
@@ -76,7 +91,7 @@ impl Node1Client {
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
@@ -90,14 +105,15 @@ impl Node1Client {
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!("Node1 ping request failed: {}", e);
}
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
@@ -109,7 +125,11 @@ impl Node1Client {
}
/// Send ping request to /ping endpoint
async fn send_ping_request(http_client: &Client, endpoint: &str, _auth_token: &str) -> Result<()> {
async fn send_ping_request(
http_client: &Client,
endpoint: &str,
_auth_token: &str,
) -> Result<()> {
// Build ping URL
let ping_url = if endpoint.ends_with('/') {
format!("{}ping", endpoint)
@@ -118,10 +138,8 @@ impl Node1Client {
};
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url)
.timeout(Duration::from_millis(1500))
.send()
.await?;
let response =
http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await?;
let status = response.status();
let _ = response.bytes().await;
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
@@ -130,9 +148,15 @@ impl Node1Client {
Ok(())
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
@@ -145,7 +169,9 @@ impl Node1Client {
}))?;
// Node1 uses api-key header instead of URL parameter
let response_text = self.http_client.post(&self.endpoint)
let response_text = self
.http_client
.post(&self.endpoint)
.body(request_body)
.header("Content-Type", "application/json")
.header("api-key", &self.auth_token)
@@ -173,10 +199,14 @@ impl Node1Client {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [node1] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [node1] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
@@ -186,7 +216,12 @@ impl Node1Client {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
@@ -198,7 +233,7 @@ impl Drop for Node1Client {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
+28 -38
View File
@@ -10,8 +10,8 @@ use quinn::{ClientConfig, Connection, Endpoint, IdleTimeout, RecvStream, Transpo
use std::net::ToSocketAddrs;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::timeout;
use tokio::sync::Mutex;
use tokio::time::timeout;
use uuid::Uuid;
use crate::common::SolanaRpcClient;
@@ -41,44 +41,34 @@ pub struct Node1QuicClient {
impl Node1QuicClient {
/// Connect and authenticate. Reuse the returned client for all subsequent sends.
pub async fn connect(
server_addr: &str,
api_key: &str,
rpc_url: String,
) -> Result<Self> {
pub async fn connect(server_addr: &str, api_key: &str, rpc_url: String) -> Result<Self> {
let socket_addr = server_addr
.to_socket_addrs()
.context("resolve Node1 QUIC server address")?
.next()
.context("no socket address for Node1 QUIC")?;
let api_key_uuid = Uuid::parse_str(api_key).context("Node1 API key must be a valid UUID")?;
let api_key_uuid =
Uuid::parse_str(api_key).context("Node1 API key must be a valid UUID")?;
let api_key_bytes: [u8; 16] = *api_key_uuid.as_bytes();
let server_name = server_addr
.split(':')
.next()
.unwrap_or(server_addr);
let server_name = server_addr.split(':').next().unwrap_or(server_addr);
let client_config = Self::build_client_config()?;
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)
.context("create QUIC endpoint")?;
let mut endpoint =
Endpoint::client("0.0.0.0:0".parse()?).context("create QUIC endpoint")?;
endpoint.set_default_client_config(client_config);
let connecting = endpoint
.connect(socket_addr, server_name)
.context("Node1 QUIC connect failed")?;
let connecting =
endpoint.connect(socket_addr, server_name).context("Node1 QUIC connect failed")?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Node1 QUIC connect timeout")?
.context("Node1 QUIC handshake failed")?;
timeout(
AUTH_TIMEOUT,
Self::authenticate(&connection, &api_key_bytes),
)
.await
.context("Node1 QUIC auth timeout")??;
timeout(AUTH_TIMEOUT, Self::authenticate(&connection, &api_key_bytes))
.await
.context("Node1 QUIC auth timeout")??;
Ok(Self {
endpoint,
@@ -96,8 +86,7 @@ impl Node1QuicClient {
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
.with_no_client_auth();
let client_crypto = QuicClientConfig::try_from(crypto)
.context("build QUIC TLS config")?;
let client_crypto = QuicClientConfig::try_from(crypto).context("build QUIC TLS config")?;
let mut client_config = ClientConfig::new(Arc::new(client_crypto));
let mut transport = TransportConfig::default();
@@ -140,12 +129,9 @@ impl Node1QuicClient {
.context("Node1 QUIC reconnect timeout")?
.context("Node1 QUIC re-handshake failed")?;
timeout(
AUTH_TIMEOUT,
Self::authenticate(&connection, &self.api_key_uuid),
)
.await
.context("Node1 QUIC re-auth timeout")??;
timeout(AUTH_TIMEOUT, Self::authenticate(&connection, &self.api_key_uuid))
.await
.context("Node1 QUIC re-auth timeout")??;
let mut g = self.connection.lock().await;
*g = connection.clone();
@@ -201,16 +187,16 @@ impl SwqosClientTrait for Node1QuicClient {
let signature = transaction.signatures.first().copied().unwrap_or_default();
let tx_bytes = bincode::serialize(transaction).context("Node1 QUIC: bincode serialize")?;
let (status, msg) = timeout(
SEND_TIMEOUT,
self.send_transaction_bytes(&tx_bytes),
)
.await
.context("Node1 QUIC send timeout")??;
let (status, msg) = timeout(SEND_TIMEOUT, self.send_transaction_bytes(&tx_bytes))
.await
.context("Node1 QUIC send timeout")??;
if status != 200 {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [node1-quic] {} submit failed: status={} msg={}", trade_type, status, msg);
eprintln!(
" [node1-quic] {} submit failed: status={} msg={}",
trade_type, status, msg
);
}
anyhow::bail!("Node1 QUIC submit failed: status={} msg={}", status, msg);
}
@@ -229,7 +215,11 @@ impl SwqosClientTrait for Node1QuicClient {
}
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
eprintln!(" [node1-quic] {} confirmation failed: {:?}", trade_type, start.elapsed());
eprintln!(
" [node1-quic] {} confirmation failed: {:?}",
trade_type,
start.elapsed()
);
}
Err(e)
}
+15 -8
View File
@@ -119,9 +119,11 @@ impl SpeedlandingClient {
let _guard = self.reconnect.lock().await;
let current = self.connection.load_full();
if current.close_reason().is_some() {
let connecting = self
.endpoint
.connect_with(self.client_config.clone(), self.addr, self.server_name.as_str())?;
let connecting = self.endpoint.connect_with(
self.client_config.clone(),
self.addr,
self.server_name.as_str(),
)?;
let connection = timeout(CONNECT_TIMEOUT, connecting)
.await
.context("Speedlanding QUIC reconnect timeout")?
@@ -151,7 +153,8 @@ impl SwqosClientTrait for SpeedlandingClient {
let start_time = Instant::now();
let (buf_guard, signature) = serialize_transaction_bincode_sync(transaction)?;
let connection = self.ensure_connected().await?;
let mut send_result = timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
let mut send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
let need_retry = match &send_result {
Ok(Ok(())) => false,
Ok(Err(_)) | Err(_) => true,
@@ -161,16 +164,20 @@ impl SwqosClientTrait for SpeedlandingClient {
eprintln!(" [speedlanding] {} send failed or timeout, reconnecting", trade_type);
}
let connection = self.ensure_connected().await?;
send_result = timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
send_result =
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &*buf_guard)).await;
}
send_result
.context("Speedlanding QUIC send timeout")??;
send_result.context("Speedlanding QUIC send timeout")??;
match poll_transaction_confirmation(&self.rpc_client, signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [speedlanding] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [speedlanding] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
}
return Err(e);
}
+49 -16
View File
@@ -1,21 +1,22 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::sync::atomic::{AtomicBool, Ordering};
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use solana_transaction_status::UiTransactionEncoding;
use std::time::Duration;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::STELLIUM_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct StelliumClient {
pub endpoint: String,
@@ -27,16 +28,29 @@ pub struct StelliumClient {
#[async_trait::async_trait]
impl SwqosClientTrait for StelliumClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *STELLIUM_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| STELLIUM_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *STELLIUM_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| STELLIUM_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -79,7 +93,9 @@ impl StelliumClient {
tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
let url = format!("{}/{}", endpoint, auth_token);
if let Ok(resp) = http_client.get(&url).timeout(Duration::from_millis(1500)).send().await {
if let Ok(resp) =
http_client.get(&url).timeout(Duration::from_millis(1500)).send().await
{
let status = resp.status();
let _ = resp.bytes().await;
if !status.is_success() && crate::common::sdk_log::sdk_log_enabled() {
@@ -111,9 +127,15 @@ impl StelliumClient {
});
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Stellium uses standard Solana sendTransaction format
let request_body = serde_json::to_string(&json!({
@@ -130,7 +152,9 @@ impl StelliumClient {
let url = format!("{}/{}", self.endpoint, self.auth_token);
// Send request to Stellium
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body)
.header("Content-Type", "application/json")
.header("Connection", "keep-alive")
@@ -159,10 +183,14 @@ impl StelliumClient {
Err(e) => {
if crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
println!(" [Stellium] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [Stellium] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
}
return Err(e);
},
}
}
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
println!(" signature: {:?}", signature);
@@ -172,7 +200,12 @@ impl StelliumClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
+73 -37
View File
@@ -1,27 +1,29 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
use std::{sync::Arc, time::Instant};
use std::time::Duration;
use sha2::{Digest, Sha256};
use solana_transaction_status::UiTransactionEncoding;
use sha2::{Sha256, Digest};
use std::time::Duration;
use std::{sync::Arc, time::Instant};
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::NOZOMI_TIP_ACCOUNTS};
use tokio::task::JoinHandle;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::task::JoinHandle;
const SPECIAL_API_KEY_PREFIX: &str = "298b5025";
const SPECIAL_API_KEY_SUFFIX: &str = "a055323";
const SPECIAL_API_KEY_HASH: &str = "e7be933c8058aebcb4d08a6120fb4dfd2ead568d42527a3fc2b60a703f25e48d";
const SPECIAL_API_KEY_HASH: &str =
"e7be933c8058aebcb4d08a6120fb4dfd2ead568d42527a3fc2b60a703f25e48d";
const TEMPORAL_COMMUNITY_TIP_ADDRESS: &str = "mwGELGMgGGrNL1UibNCQeJHDE7qdPptWRYB6noUHmTj";
#[inline]
@@ -31,7 +33,6 @@ fn fast_sha256_hex(input: &str) -> String {
format!("{:x}", hasher.finalize())
}
#[derive(Clone)]
pub struct TemporalClient {
pub rpc_client: Arc<SolanaRpcClient>,
@@ -44,18 +45,30 @@ pub struct TemporalClient {
#[async_trait::async_trait]
impl SwqosClientTrait for TemporalClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let api_key = &self.auth_token;
if api_key.len() >= SPECIAL_API_KEY_PREFIX.len() + SPECIAL_API_KEY_SUFFIX.len() {
if api_key.starts_with(SPECIAL_API_KEY_PREFIX) && api_key.ends_with(SPECIAL_API_KEY_SUFFIX) {
if api_key.starts_with(SPECIAL_API_KEY_PREFIX)
&& api_key.ends_with(SPECIAL_API_KEY_SUFFIX)
{
let current_api_key_hash = fast_sha256_hex(api_key);
if current_api_key_hash == SPECIAL_API_KEY_HASH {
@@ -64,7 +77,10 @@ impl SwqosClientTrait for TemporalClient {
}
}
let tip_account = *NOZOMI_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| NOZOMI_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *NOZOMI_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| NOZOMI_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -77,22 +93,22 @@ impl TemporalClient {
pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self {
let rpc_client = SolanaRpcClient::new(rpc_url);
let http_client = default_http_client_builder().build().unwrap();
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
let client = Self {
rpc_client: Arc::new(rpc_client),
endpoint,
auth_token,
http_client,
ping_handle: Arc::new(tokio::sync::Mutex::new(None)),
stop_ping: Arc::new(AtomicBool::new(false)),
};
// Start ping task
let client_clone = client.clone();
tokio::spawn(async move {
client_clone.start_ping_task().await;
});
client
}
@@ -102,7 +118,7 @@ impl TemporalClient {
let auth_token = self.auth_token.clone();
let http_client = self.http_client.clone();
let stop_ping = self.stop_ping.clone();
let handle = tokio::spawn(async move {
// Immediate first ping to warm connection and reduce first-submit cold start latency
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
@@ -114,12 +130,13 @@ impl TemporalClient {
if stop_ping.load(Ordering::Relaxed) {
break;
}
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await {
if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await
{
eprintln!("Temporal ping request failed: {}", e);
}
}
});
// Update ping_handle - use Mutex to safely update
{
let mut ping_guard = self.ping_handle.lock().await;
@@ -131,7 +148,11 @@ impl TemporalClient {
}
/// Send ping request to /ping endpoint
async fn send_ping_request(http_client: &Client, endpoint: &str, _auth_token: &str) -> Result<()> {
async fn send_ping_request(
http_client: &Client,
endpoint: &str,
_auth_token: &str,
) -> Result<()> {
// Build ping URL (no auth token required for ping endpoint)
let ping_url = if endpoint.ends_with('/') {
format!("{}ping", endpoint)
@@ -140,10 +161,8 @@ impl TemporalClient {
};
// Short timeout for ping; consume body so connection is returned to pool for reuse by submit
let response = http_client.get(&ping_url)
.timeout(Duration::from_millis(1500))
.send()
.await?;
let response =
http_client.get(&ping_url).timeout(Duration::from_millis(1500)).send().await?;
let status = response.status();
let _ = response.bytes().await;
if !status.is_success() {
@@ -152,9 +171,15 @@ impl TemporalClient {
Ok(())
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
// Build request body according to Nozomi documentation requirements
let request_body = serde_json::to_string(&json!({
@@ -172,7 +197,9 @@ impl TemporalClient {
url.push_str("/?c=");
url.push_str(&self.auth_token);
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body)
.header("Content-Type", "application/json")
.send()
@@ -195,9 +222,13 @@ impl TemporalClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [nozomi] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(
" [nozomi] {} confirmation failed: {:?}",
trade_type,
start_time.elapsed()
);
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
@@ -207,7 +238,12 @@ impl TemporalClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
@@ -219,7 +255,7 @@ impl Drop for TemporalClient {
fn drop(&mut self) {
// Ensure ping task stops when client is destroyed
self.stop_ping.store(true, Ordering::Relaxed);
// Try to stop ping task immediately
// Use tokio::spawn to avoid blocking Drop
let ping_handle = self.ping_handle.clone();
@@ -231,4 +267,4 @@ impl Drop for TemporalClient {
*ping_guard = None;
});
}
}
}
+40 -13
View File
@@ -1,4 +1,6 @@
use crate::swqos::common::{default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode};
use crate::swqos::common::{
default_http_client_builder, poll_transaction_confirmation, serialize_transaction_and_encode,
};
use rand::seq::IndexedRandom;
use reqwest::Client;
use serde_json::json;
@@ -6,14 +8,13 @@ use std::{sync::Arc, time::Instant};
use solana_transaction_status::UiTransactionEncoding;
use crate::swqos::SwqosClientTrait;
use crate::swqos::{SwqosType, TradeType};
use anyhow::Result;
use solana_sdk::transaction::VersionedTransaction;
use crate::swqos::{SwqosType, TradeType};
use crate::swqos::SwqosClientTrait;
use crate::{common::SolanaRpcClient, constants::swqos::ZEROSLOT_TIP_ACCOUNTS};
#[derive(Clone)]
pub struct ZeroSlotClient {
pub endpoint: String,
@@ -24,16 +25,29 @@ pub struct ZeroSlotClient {
#[async_trait::async_trait]
impl SwqosClientTrait for ZeroSlotClient {
async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
self.send_transaction(trade_type, transaction, wait_confirmation).await
}
async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
self.send_transactions(trade_type, transactions, wait_confirmation).await
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *ZEROSLOT_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ZEROSLOT_TIP_ACCOUNTS.first()).unwrap();
let tip_account = *ZEROSLOT_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| ZEROSLOT_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
@@ -49,9 +63,15 @@ impl ZeroSlotClient {
Self { rpc_client: Arc::new(rpc_client), endpoint, auth_token, http_client }
}
pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction, wait_confirmation: bool) -> Result<()> {
pub async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let (content, signature) =
serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64)?;
let request_body = serde_json::to_string(&json!({
"jsonrpc": "2.0",
@@ -69,7 +89,9 @@ impl ZeroSlotClient {
url.push_str(&self.auth_token);
// 4. Use `text().await?` directly, avoiding async JSON parsing from `json().await?`
let response_text = self.http_client.post(&url)
let response_text = self
.http_client
.post(&url)
.body(request_body) // Pass string directly, avoiding `json()` overhead
.header("Content-Type", "application/json") // Explicitly specify JSON header
.send()
@@ -95,7 +117,7 @@ impl ZeroSlotClient {
println!(" signature: {:?}", signature);
println!(" [0slot] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
@@ -105,10 +127,15 @@ impl ZeroSlotClient {
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>, wait_confirmation: bool) -> Result<()> {
pub async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
}
}