Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22c1a149e7 | |||
| 8657273ba6 | |||
| 99fb4ee604 | |||
| 3719d4a9f4 | |||
| f19f8a5d1c |
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "sol-trade-sdk"
|
||||
version = "4.0.20"
|
||||
version = "4.0.21"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"William <byteblock6@gmail.com>",
|
||||
|
||||
@@ -171,6 +171,19 @@ pub const SPEEDLANDING_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("speede8xCcUq2Tiv1efXeTuE3k9TDNq8TnGKaKSc6J4"),
|
||||
];
|
||||
|
||||
pub const SOLAMI_TIP_ACCOUNTS: &[Pubkey] = &[
|
||||
pubkey!("15qWd4huAkoxvhDsHMfpUn27TW1YBYMMJJ2jkAkbeam"),
|
||||
pubkey!("9XuGciSwr5wb7dLTQm91JhuBTvj3GG8WjuRDc3obeam"),
|
||||
pubkey!("kiQioJNyFG7pU36ELLsRKXkeT48kFbk3b6rSgrWbeam"),
|
||||
pubkey!("kjmVhW1UzJrW2sU5bY5NtZ79jpvjSStsj37Pzmabeam"),
|
||||
pubkey!("kREnjPWFpt4AHeY5pijPmyXaCrMnbatUQJo7d3Xbeam"),
|
||||
pubkey!("praRZG6N6MdbsT4EFpKgZJWReZGXQhAMFcH68oCbeam"),
|
||||
pubkey!("SqoKQKU5uwBxovq3R7yEBxFwptc4z7vwoghU3M9beam"),
|
||||
pubkey!("sV72TY66T1RfmDSeHPPbwX6wwJ3bBv5hd4ehJ8tbeam"),
|
||||
pubkey!("swf8MyEeLo7gtRUo27UuJj6naCASUrypU7dbteSbeam"),
|
||||
pubkey!("uiuaQsxA47JybQAVN4FTfYuoEDkMiXV1r591Aewbeam"),
|
||||
];
|
||||
|
||||
// `SwqosRegion` 与下列各 `SWQOS_ENDPOINTS_*` 下标严格对应(共 10 项):
|
||||
// 0 NewYork, 1 Frankfurt, 2 Amsterdam, 3 Dublin, 4 SLC, 5 Tokyo, 6 Singapore, 7 London, 8 LosAngeles, 9 Default。
|
||||
//
|
||||
@@ -434,6 +447,19 @@ pub const SWQOS_ENDPOINTS_HELIUS: [&str; 10] = [
|
||||
"https://sender.helius-rpc.com/fast", // Default: 非地理区域;全局 Sender
|
||||
];
|
||||
|
||||
pub const SWQOS_ENDPOINTS_SOLAMI: [&str; 10] = [
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev:11000",
|
||||
"beam.solami.dev: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;
|
||||
@@ -450,6 +476,7 @@ pub const SWQOS_MIN_TIP_SOYAS: f64 = 0.001; // Soyas requires minimum 0.001 SOL
|
||||
pub const SWQOS_MIN_TIP_SPEEDLANDING: f64 = 0.001; // Speedlanding requires minimum 0.001 SOL tip
|
||||
/// Helius Sender: 0.0002 SOL when not swqos_only; use SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY when swqos_only=true.
|
||||
pub const SWQOS_MIN_TIP_HELIUS: f64 = 0.0002;
|
||||
pub const SWQOS_MIN_TIP_SOLAMI: f64 = 0.0001;
|
||||
/// Helius Sender with swqos_only: minimum 0.000005 SOL (much lower tip allowed).
|
||||
pub const SWQOS_MIN_TIP_HELIUS_SWQOS_ONLY: f64 = 0.000005;
|
||||
|
||||
@@ -476,6 +503,7 @@ mod tests {
|
||||
&SWQOS_ENDPOINTS_SOYAS,
|
||||
&SWQOS_ENDPOINTS_SPEEDLANDING,
|
||||
&SWQOS_ENDPOINTS_HELIUS,
|
||||
&SWQOS_ENDPOINTS_SOLAMI,
|
||||
];
|
||||
|
||||
#[test]
|
||||
|
||||
+24
-9
@@ -11,6 +11,7 @@ pub mod nextblock;
|
||||
pub mod node1;
|
||||
pub mod node1_quic;
|
||||
pub mod serialization;
|
||||
pub mod solami;
|
||||
pub mod solana_rpc;
|
||||
pub mod soyas;
|
||||
pub mod speedlanding;
|
||||
@@ -33,21 +34,21 @@ use crate::{
|
||||
SWQOS_ENDPOINTS_BLOCKRAZOR, SWQOS_ENDPOINTS_BLOCKRAZOR_GRPC, 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_ENDPOINTS_SOLAMI, 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_MIN_TIP_SOLAMI, SWQOS_MIN_TIP_SOYAS, SWQOS_MIN_TIP_SPEEDLANDING,
|
||||
SWQOS_MIN_TIP_STELLIUM, SWQOS_MIN_TIP_TEMPORAL, SWQOS_MIN_TIP_ZERO_SLOT,
|
||||
},
|
||||
swqos::{
|
||||
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_quic::Node1QuicClient, solami::SolamiClient, solana_rpc::SolRpcClient,
|
||||
soyas::SoyasClient, speedlanding::SpeedlandingClient, stellium::StelliumClient,
|
||||
temporal::TemporalClient, zeroslot::ZeroSlotClient,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -121,6 +122,7 @@ pub enum SwqosType {
|
||||
Soyas,
|
||||
Speedlanding,
|
||||
Helius,
|
||||
Solami,
|
||||
Default,
|
||||
}
|
||||
|
||||
@@ -143,6 +145,7 @@ impl SwqosType {
|
||||
Self::Soyas => "Soyas",
|
||||
Self::Speedlanding => "Speedlanding",
|
||||
Self::Helius => "Helius",
|
||||
Self::Solami => "Solami",
|
||||
Self::Default => "Default",
|
||||
}
|
||||
}
|
||||
@@ -163,6 +166,7 @@ impl SwqosType {
|
||||
Self::Soyas,
|
||||
Self::Speedlanding,
|
||||
Self::Helius,
|
||||
Self::Solami,
|
||||
Self::Default,
|
||||
]
|
||||
}
|
||||
@@ -204,6 +208,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,
|
||||
}
|
||||
}
|
||||
@@ -264,6 +269,8 @@ 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>),
|
||||
/// Solami(api_key, region, custom_url)
|
||||
Solami(String, SwqosRegion, Option<String>),
|
||||
}
|
||||
|
||||
impl SwqosConfig {
|
||||
@@ -284,6 +291,7 @@ impl SwqosConfig {
|
||||
SwqosConfig::Soyas(_, _, _) => SwqosType::Soyas,
|
||||
SwqosConfig::Speedlanding(_, _, _) => SwqosType::Speedlanding,
|
||||
SwqosConfig::Helius(_, _, _, _) => SwqosType::Helius,
|
||||
SwqosConfig::Solami(_, _, _) => SwqosType::Solami,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +320,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(),
|
||||
}
|
||||
}
|
||||
@@ -512,6 +521,12 @@ impl SwqosConfig {
|
||||
HeliusClient::new(rpc_url.clone(), endpoint, api_key_opt, swqos_only);
|
||||
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, commitment);
|
||||
let rpc_client = SolRpcClient::new(Arc::new(rpc));
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
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::signer::Signer;
|
||||
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 tokio::time::timeout;
|
||||
|
||||
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-beam";
|
||||
const KEEP_ALIVE_INTERVAL: Duration = Duration::from_secs(25);
|
||||
const MAX_IDLE_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const SEND_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
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_bytes = bs58::decode(api_key.trim())
|
||||
.into_vec()
|
||||
.map_err(|e| anyhow::anyhow!("Solami api_token base58 decode failed: {}", e))?;
|
||||
let keypair = Keypair::try_from(keypair_bytes.as_slice()).map_err(|e| {
|
||||
anyhow::anyhow!("Solami api_token is not a valid Solana keypair: {}", e)
|
||||
})?;
|
||||
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 connecting = endpoint.connect(addr, SOLAMI_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Solami QUIC connect timeout")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Solami QUIC handshake failed (verify wallet pubkey {} is registered and UDP {} is reachable)",
|
||||
keypair.pubkey(),
|
||||
endpoint_string
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Self {
|
||||
rpc_client: Arc::new(rpc_client),
|
||||
endpoint,
|
||||
client_config,
|
||||
addr,
|
||||
connection: ArcSwap::from_pointee(connection),
|
||||
reconnect: Mutex::new(()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn ensure_connected(&self) -> Result<Arc<Connection>> {
|
||||
let current = self.connection.load_full();
|
||||
if current.close_reason().is_none() {
|
||||
return Ok(current);
|
||||
}
|
||||
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, SOLAMI_SERVER)?;
|
||||
let connection = timeout(CONNECT_TIMEOUT, connecting)
|
||||
.await
|
||||
.context("Solami QUIC reconnect timeout")?
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Solami QUIC re-handshake failed (peer {} SNI {})",
|
||||
self.addr, SOLAMI_SERVER
|
||||
)
|
||||
})?;
|
||||
self.connection.store(Arc::new(connection));
|
||||
return Ok(self.connection.load_full());
|
||||
}
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
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.ensure_connected().await?;
|
||||
let mut send_result =
|
||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &serialized_tx)).await;
|
||||
let need_retry = matches!(&send_result, Ok(Err(_)) | Err(_));
|
||||
if need_retry {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Solami",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"reconnecting",
|
||||
);
|
||||
}
|
||||
let connection = self.ensure_connected().await?;
|
||||
send_result =
|
||||
timeout(SEND_TIMEOUT, Self::try_send_bytes(&connection, &serialized_tx)).await;
|
||||
}
|
||||
match send_result {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Solami",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
&e,
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
Err(_) => {
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submission_failed(
|
||||
"Solami",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
"timeout",
|
||||
);
|
||||
}
|
||||
anyhow::bail!("Solami QUIC send timeout");
|
||||
}
|
||||
}
|
||||
if crate::common::sdk_log::sdk_log_enabled() {
|
||||
crate::common::sdk_log::log_swqos_submitted("Solami", trade_type, start_time.elapsed());
|
||||
}
|
||||
let start_time = Instant::now();
|
||||
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!(
|
||||
" [{:width$}] {} confirmation failed: {:?}",
|
||||
"Solami",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
if wait_confirmation && crate::common::sdk_log::sdk_log_enabled() {
|
||||
println!(" signature: {:?}", signature);
|
||||
println!(
|
||||
" [{:width$}] {} confirmed: {:?}",
|
||||
"Solami",
|
||||
trade_type,
|
||||
start_time.elapsed(),
|
||||
width = crate::common::sdk_log::SWQOS_LABEL_WIDTH
|
||||
);
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user