feat(swqos): add Solami

This commit is contained in:
civa
2026-03-02 03:00:49 +00:00
parent d2ce193e2c
commit f19f8a5d1c
3 changed files with 198 additions and 0 deletions
+19
View File
@@ -155,6 +155,13 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
];
pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[
pubkey!("6993ZufwyEDNdB94kciDTGB17ANXguiNH22VmMQU1ami"),
pubkey!("E1BkG293HQocKfCkfPS7tEvs8Enh6FK8pUKi17EH1ami"),
pubkey!("2Ga87xvZwpP9WRsSdiPiWre21cQbDcFhGLmT5EMo1ami"),
pubkey!("CGzT5jzT68vGUWVagQAQqESfrCEHSKYe9DAjfgfC1ami"),
];
// NewYork,
// Frankfurt,
// Amsterdam,
@@ -313,6 +320,17 @@ pub const SWQOS_ENDPOINTS_HELIUS: [&str; 8] = [
"https://sender.helius-rpc.com/fast",
];
pub const SWQOS_ENDPOINTS_SOLAMI: [&str; 8] = [
"nyc.landing.solami.fast:11000",
"fra.landing.solami.fast:11000",
"ams.landing.solami.fast:11000",
"nyc.landing.solami.fast:11000",
"sgp.landing.solami.fast:11000",
"fra.landing.solami.fast:11000",
"nyc.landing.solami.fast:11000",
"landing.solami.fast:11000",
];
pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费
pub const SWQOS_MIN_TIP_JITO: f64 = 0.00001;
pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001;
@@ -331,3 +349,4 @@ pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires mini
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
pub const SWQOS_MIN_TIP_SOLAMI: f64 = 0.0001;
+21
View File
@@ -15,6 +15,7 @@ pub mod lightspeed;
pub mod soyas;
pub mod speedlanding;
pub mod helius;
pub mod solami;
use std::sync::Arc;
@@ -40,6 +41,7 @@ use crate::{
SWQOS_ENDPOINTS_SOYAS,
SWQOS_ENDPOINTS_SPEEDLANDING,
SWQOS_ENDPOINTS_HELIUS,
SWQOS_ENDPOINTS_SOLAMI,
SWQOS_MIN_TIP_DEFAULT,
SWQOS_MIN_TIP_JITO,
SWQOS_MIN_TIP_NEXTBLOCK,
@@ -55,6 +57,7 @@ use crate::{
SWQOS_MIN_TIP_SOYAS,
SWQOS_MIN_TIP_SPEEDLANDING,
SWQOS_MIN_TIP_HELIUS,
SWQOS_MIN_TIP_SOLAMI,
},
swqos::{
bloxroute::BloxrouteClient,
@@ -72,6 +75,7 @@ use crate::{
soyas::SoyasClient,
speedlanding::SpeedlandingClient,
helius::HeliusClient,
solami::SolamiClient,
}
};
@@ -122,6 +126,7 @@ pub enum SwqosType {
Soyas,
Speedlanding,
Helius,
Solami,
Default,
}
@@ -142,6 +147,7 @@ impl SwqosType {
Self::Soyas,
Self::Speedlanding,
Self::Helius,
Self::Solami,
Self::Default,
]
}
@@ -173,6 +179,7 @@ pub trait SwqosClientTrait {
SwqosType::Soyas => SWQOS_MIN_TIP_SOYAS,
SwqosType::Speedlanding => SWQOS_MIN_TIP_SPEEDLANDING,
SwqosType::Helius => SWQOS_MIN_TIP_HELIUS,
SwqosType::Solami => SWQOS_MIN_TIP_SOLAMI,
SwqosType::Default => SWQOS_MIN_TIP_DEFAULT,
}
}
@@ -225,6 +232,9 @@ pub enum SwqosConfig {
/// Helius Sender: dual routing to validators and Jito. API key optional (custom TPS only).
/// (api_key, region, custom_url, swqos_only). swqos_only: None => false (min tip 0.0002 SOL); Some(true) => SWQOS-only (min tip 0.000005 SOL, much lower).
Helius(String, SwqosRegion, Option<String>, Option<bool>),
/// Register on https://solami.fast and use your rpc key
/// Solami(api_key, region, custom_url)
Solami(String, SwqosRegion, Option<String>),
}
impl SwqosConfig {
@@ -245,6 +255,7 @@ impl SwqosConfig {
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
SwqosConfig::Solami(_, _, _) => SwqosType::Solami,
}
}
@@ -273,6 +284,7 @@ impl SwqosConfig {
SwqosType::Soyas => SWQOS_ENDPOINTS_SOYAS[region as usize].to_string(),
SwqosType::Speedlanding => SWQOS_ENDPOINTS_SPEEDLANDING[region as usize].to_string(),
SwqosType::Helius => SWQOS_ENDPOINTS_HELIUS[region as usize].to_string(),
SwqosType::Solami => SWQOS_ENDPOINTS_SOLAMI[region as usize].to_string(),
SwqosType::Default => "".to_string(),
}
}
@@ -408,6 +420,15 @@ impl SwqosConfig {
);
Ok(Arc::new(helius_client))
},
SwqosConfig::Solami(auth_token, region, url) => {
let endpoint = SwqosConfig::get_endpoint(SwqosType::Solami, region, url);
let solami_client = SolamiClient::new(
rpc_url.clone(),
endpoint.to_string(),
auth_token
).await?;
Ok(Arc::new(solami_client))
},
SwqosConfig::Default(endpoint) => {
let rpc = SolanaRpcClient::new_with_commitment(
endpoint,
+158
View File
@@ -0,0 +1,158 @@
use anyhow::Context as _;
use anyhow::Result;
use arc_swap::ArcSwap;
use quinn::{
crypto::rustls::QuicClientConfig, ClientConfig, Connection, Endpoint, IdleTimeout,
TransportConfig,
};
use rand::seq::IndexedRandom as _;
use solana_client::rpc_client::SerializableTransaction;
use solana_sdk::{signature::Keypair, transaction::VersionedTransaction};
use solana_tls_utils::{new_dummy_x509_certificate, SkipServerVerification};
use std::time::Instant;
use std::{
net::{SocketAddr, ToSocketAddrs as _},
sync::Arc,
time::Duration,
};
use tokio::sync::Mutex;
use crate::common::SolanaRpcClient;
use crate::swqos::common::poll_transaction_confirmation;
use crate::swqos::SwqosClientTrait;
use crate::{
constants::swqos::SOLAMI_TIP_ACCOUNTS,
swqos::{SwqosType, TradeType},
};
const ALPN_TPU_PROTOCOL_ID: &[u8] = b"solana-tpu";
const SOLAMI_SERVER: &str = "solami-landing";
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
pub struct SolamiClient {
pub rpc_client: Arc<SolanaRpcClient>,
endpoint: Endpoint,
client_config: ClientConfig,
addr: SocketAddr,
connection: ArcSwap<Connection>,
reconnect: Mutex<()>,
}
impl SolamiClient {
pub async fn new(rpc_url: String, endpoint_string: String, api_key: String) -> Result<Self> {
let rpc_client = SolanaRpcClient::new(rpc_url);
let keypair = Keypair::from_base58_string(&api_key);
let (cert, key) = new_dummy_x509_certificate(&keypair);
let mut crypto = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(SkipServerVerification::new())
.with_client_auth_cert(vec![cert], key)
.context("failed to configure client certificate")?;
crypto.alpn_protocols = vec![ALPN_TPU_PROTOCOL_ID.to_vec()];
let client_crypto = QuicClientConfig::try_from(crypto)
.context("failed to convert rustls config into quinn crypto config")?;
let mut client_config = ClientConfig::new(Arc::new(client_crypto));
let mut transport = TransportConfig::default();
transport.keep_alive_interval(Some(KEEP_ALIVE_INTERVAL));
transport.max_idle_timeout(Some(IdleTimeout::try_from(MAX_IDLE_TIMEOUT)?));
client_config.transport_config(Arc::new(transport));
let mut endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(client_config.clone());
let addr = endpoint_string
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("Address not resolved"))?;
let connection = endpoint.connect(addr, SOLAMI_SERVER)?.await?;
Ok(Self {
rpc_client: Arc::new(rpc_client),
endpoint,
client_config,
addr,
connection: ArcSwap::from_pointee(connection),
reconnect: Mutex::new(()),
})
}
async fn reconnect(&self) -> Result<()> {
let _guard = self.reconnect.try_lock()?;
let connection = self
.endpoint
.connect_with(self.client_config.clone(), self.addr, SOLAMI_SERVER)?
.await?;
self.connection.store(Arc::new(connection));
Ok(())
}
async fn try_send_bytes(connection: &Connection, payload: &[u8]) -> Result<()> {
let mut stream = connection.open_uni().await?;
stream.write_all(payload).await?;
stream.finish()?;
Ok(())
}
}
#[async_trait::async_trait]
impl SwqosClientTrait for SolamiClient {
async fn send_transaction(
&self,
trade_type: TradeType,
transaction: &VersionedTransaction,
wait_confirmation: bool,
) -> Result<()> {
let start_time = Instant::now();
let signature = transaction.get_signature();
let serialized_tx = bincode::serialize(transaction)?;
let connection = self.connection.load_full();
if Self::try_send_bytes(&connection, &serialized_tx).await.is_err() {
eprintln!(" [Solami] {} submission failed, reconnecting", trade_type);
self.reconnect().await?;
let connection = self.connection.load_full();
if let Err(e) = Self::try_send_bytes(&connection, &serialized_tx).await {
eprintln!(" [Solami] {} submission failed: {:?}", trade_type, e);
return Err(e.into());
}
}
match poll_transaction_confirmation(&self.rpc_client, *signature, wait_confirmation).await {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" [Solami] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
}
}
if wait_confirmation {
println!(" signature: {:?}", signature);
println!(" [Solami] {} confirmed: {:?}", trade_type, start_time.elapsed());
}
Ok(())
}
async fn send_transactions(
&self,
trade_type: TradeType,
transactions: &Vec<VersionedTransaction>,
wait_confirmation: bool,
) -> Result<()> {
for transaction in transactions {
self.send_transaction(trade_type, transaction, wait_confirmation).await?;
}
Ok(())
}
fn get_tip_account(&self) -> Result<String> {
let tip_account = *SOLAMI_TIP_ACCOUNTS
.choose(&mut rand::rng())
.or_else(|| SOLAMI_TIP_ACCOUNTS.first())
.unwrap();
Ok(tip_account.to_string())
}
fn get_swqos_type(&self) -> SwqosType {
SwqosType::Solami
}
}