style: run rustfmt
This commit is contained in:
+62
-31
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user