From df00d3f26a0ad9f5e379cd617c7401ee4123ea26 Mon Sep 17 00:00:00 2001 From: wei <1415121722@qq.com> Date: Sun, 7 Sep 2025 15:22:06 +0800 Subject: [PATCH] feat: add Astralane --- README.md | 4 +- README_CN.md | 4 +- examples/trading_client/src/main.rs | 1 + src/constants/swqos.rs | 22 +++ src/main.rs | 1 + src/swqos/astralane.rs | 225 ++++++++++++++++++++++++++++ src/swqos/mod.rs | 19 ++- 7 files changed, 272 insertions(+), 4 deletions(-) create mode 100644 src/swqos/astralane.rs diff --git a/README.md b/README.md index ece4697..ba86ea3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A comprehensive Rust SDK for seamless interaction with Solana DEX trading progra 6. **Event Subscription**: Subscribe to PumpFun, PumpSwap, Bonk, Raydium CPMM, and Raydium AMM V4 program trading events 7. **Yellowstone gRPC**: Subscribe to program events using Yellowstone gRPC 8. **ShredStream Support**: Subscribe to program events using ShredStream -9. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, and other services +9. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, Astralane and other services 10. **Concurrent Trading**: Send transactions using multiple MEV services simultaneously; the fastest succeeds while others fail 11. **Unified Trading Interface**: Use unified trading protocol enums for trading operations 12. **Middleware System**: Support for custom instruction middleware to modify, add, or remove instructions before transaction execution @@ -119,6 +119,7 @@ When configuring SWQOS services, note the different parameter requirements for e - **FlashBlock**: The first parameter is API Token, Add community TG admin [xyz_0xfnzero](https://t.me/xyz_0xfnzero) to get a free key and instantly speed up your trading (with tip refunds available)! - **BlockRazor**: The first parameter is API Token, Add official TG support to get a free key and instantly accelerate your trades! - **Node1**: The first parameter is API Token, Add the official TG support at https://t.me/node1_me to get a free key and instantly accelerate your trades! +- **Astralane**: The first parameter is API Token #### Custom URL Support @@ -202,6 +203,7 @@ let trade_config = TradeConfig { - **FlashBlock**: High-speed transaction execution with API key authentication - [Official Documentation](https://doc.flashblock.trade/) - **BlockRazor**: High-speed transaction execution with API key authentication - [Official Documentation](https://blockrazor.gitbook.io/blockrazor/) - **Node1**: High-speed transaction execution with API key authentication - [Official Documentation](https://node1.me/docs.html) +- **Astralane**: Blockchain network acceleration ## New Architecture Features diff --git a/README_CN.md b/README_CN.md index 356fe60..2e63a99 100755 --- a/README_CN.md +++ b/README_CN.md @@ -13,7 +13,7 @@ 6. **事件订阅**: 订阅 PumpFun、PumpSwap、Bonk、Raydium CPMM 和 Raydium AMM V4 程序的交易事件 7. **Yellowstone gRPC**: 使用 Yellowstone gRPC 订阅程序事件 8. **ShredStream 支持**: 使用 ShredStream 订阅程序事件 -9. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1 等服务 +9. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1、Astralane 等服务 10. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败 11. **统一交易接口**: 使用统一的交易协议枚举进行交易操作 12. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除 @@ -119,6 +119,7 @@ sol-trade-sdk = "0.5.7" - **FlashBlock**: 第一个参数是 API Token, 添加社区tg管理员[xyz_0xfnzero](https://t.me/xyz_0xfnzero)获取免费key立即加速你的交易(可获得小费返还)! - **BlockRazor**: 第一个参数是 API Token, 添加tg官方客服获取免费key立即加速你的交易! - **Node1**: 第一个参数是 API Token, 添加tg官方客服https://t.me/node1_me 获取免费key立即加速你的交易! +- **Astralane**: 第一个参数是 API Token #### 自定义 URL 支持 @@ -202,6 +203,7 @@ let trade_config = TradeConfig { - **FlashBlock**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://doc.flashblock.trade/) - **BlockRazor**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://blockrazor.gitbook.io/blockrazor/) - **Node1**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://node1.me/docs.html) +- **Astralane**: 高速交易执行,支持 API 密钥认证 ## 新架构特性 diff --git a/examples/trading_client/src/main.rs b/examples/trading_client/src/main.rs index 695a3a8..9b84fe9 100644 --- a/examples/trading_client/src/main.rs +++ b/examples/trading_client/src/main.rs @@ -45,6 +45,7 @@ fn create_swqos_configs(rpc_url: &str) -> Vec { // Add tg official customer https://t.me/node1_me to get free Node1 key SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None), + SwqosConfig::Astralane("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Default(rpc_url.to_string()), ] } diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index 487e11f..c5b245b 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -98,6 +98,17 @@ pub const BLOCKRAZOR_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"), ]; +pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("astrazznxsGUhWShqgNtAdfrzP2G83DzcWVJDxwV9bF"), + pubkey!("astra4uejePWneqNaJKuFFA8oonqCE1sqF6b45kDMZm"), + pubkey!("astra9xWY93QyfG6yM8zwsKsRodscjQ2uU2HKNL5prk"), + pubkey!("astraRVUuTHjpwEVvNBeQEgwYx9w9CFyfxjYoobCZhL"), + pubkey!("astraEJ2fEj8Xmy6KLG7B3VfbKfsHXhHrNdCQx7iGJK"), + pubkey!("astraubkDw81n4LuutzSQ8uzHCv4BhPVhfvTcYv8SKC"), + pubkey!("astraZW5GLFefxNPAatceHhYjfA1ciq9gvfEg2S47xk"), + pubkey!("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"), +]; + // NewYork, // Frankfurt, // Amsterdam, @@ -195,3 +206,14 @@ pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [ "http://frankfurt.solana.blockrazor.xyz:443/sendTransaction", ]; +pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [ + "http://ny.gateway.astralane.io/iris", + "http://fr.gateway.astralane.io/iris", + "http://ams.gateway.astralane.io/iris", + "http://ny.gateway.astralane.io/iris", + "http://jp.gateway.astralane.io/iris", + "http://ny.gateway.astralane.io/iris", + "http://lax.gateway.astralane.io/iris", + "http://lim.gateway.astralane.io/iris", +]; + diff --git a/src/main.rs b/src/main.rs index a042ba5..fb31e9e 100755 --- a/src/main.rs +++ b/src/main.rs @@ -83,6 +83,7 @@ fn create_swqos_configs(rpc_url: &str) -> Vec { SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt, None), + SwqosConfig::Astralane("your api_token".to_string(), SwqosRegion::Frankfurt, None), SwqosConfig::Default(rpc_url.to_string()), ] } diff --git a/src/swqos/astralane.rs b/src/swqos/astralane.rs new file mode 100644 index 0000000..6a336b3 --- /dev/null +++ b/src/swqos/astralane.rs @@ -0,0 +1,225 @@ +use crate::swqos::common::{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 anyhow::Result; +use solana_sdk::transaction::VersionedTransaction; +use crate::swqos::{SwqosType, TradeType}; +use crate::swqos::SwqosClientTrait; + +use crate::{common::SolanaRpcClient, constants::swqos::ASTRALANE_TIP_ACCOUNTS}; + +use tokio::task::JoinHandle; +use std::sync::atomic::{AtomicBool, Ordering}; + +#[derive(Clone)] +pub struct AstralaneClient { + pub endpoint: String, + pub auth_token: String, + pub rpc_client: Arc, + pub http_client: Client, + pub ping_handle: Arc>>>, + pub stop_ping: Arc, +} + +#[async_trait::async_trait] +impl SwqosClientTrait for AstralaneClient { + async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<()> { + self.send_transaction(trade_type, transaction).await + } + + async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result<()> { + self.send_transactions(trade_type, transactions).await + } + + fn get_tip_account(&self) -> Result { + let tip_account = *ASTRALANE_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| ASTRALANE_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::Astralane + } +} + +impl AstralaneClient { + pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + let http_client = Client::builder() + // Due to ping mechanism, can extend connection pool idle timeout + .pool_idle_timeout(Duration::from_secs(300)) // 5 minutes, longer than ping interval + .pool_max_idle_per_host(32) // Reduce connections as they will be more stable + // TCP keepalive can be set longer as ping will actively maintain connections + .tcp_keepalive(Some(Duration::from_secs(300))) // 5 minutes + // HTTP/2 keepalive interval can be longer + .http2_keep_alive_interval(Duration::from_secs(30)) // 30 seconds + // Request timeout can be appropriately extended as connections are more stable + .timeout(Duration::from_secs(15)) // 15 seconds + .connect_timeout(Duration::from_secs(5)) + .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 + } + + /// Start periodic ping task to keep connections active + async fn start_ping_task(&self) { + let endpoint = self.endpoint.clone(); + 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 { + let mut interval = tokio::time::interval(Duration::from_secs(60)); // Ping every 60 seconds + + loop { + interval.tick().await; + + if stop_ping.load(Ordering::Relaxed) { + break; + } + + // Send ping request + tokio::time::sleep(Duration::from_secs(5)).await; + if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await { + eprintln!("Astralane ping request failed: {}", e); + } + } + }); + + // Update ping_handle - use Mutex to safely update + { + let mut ping_guard = self.ping_handle.lock().await; + if let Some(old_handle) = ping_guard.as_ref() { + old_handle.abort(); + } + *ping_guard = Some(handle); + } + } + + /// Send ping request to /gethealth endpoint + async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> { + // Build ping URL by replacing /iris with /gethealth + let ping_url = if endpoint.ends_with("/iris") { + endpoint.replace("/iris", "/gethealth") + } else if endpoint.ends_with("/iris/") { + endpoint.replace("/iris/", "/gethealth") + } else if endpoint.ends_with('/') { + format!("{}gethealth", endpoint) + } else { + format!("{}/gethealth", endpoint) + }; + + // Send GET request to /gethealth endpoint with api_key header + let response = http_client.get(&ping_url) + .header("api_key", auth_token) + .send() + .await?; + + if response.status().is_success() { + // ping successful, connection remains active + // println!("send getHealth to keep connection alive"); + } else { + eprintln!("Astralane ping request returned non-success status: {}", response.status()); + } + + Ok(()) + } + + pub async fn send_transaction(&self, trade_type: TradeType, transaction: &VersionedTransaction) -> Result<()> { + let start_time = Instant::now(); + let (content, signature) = serialize_transaction_and_encode(transaction, UiTransactionEncoding::Base64).await?; + println!(" Transaction encoded to base64: {:?}", start_time.elapsed()); + + let request_body = serde_json::to_string(&json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": [ + content, + { "encoding": "base64", "skipPreflight": true }, + { "mevProtect": false } + ] + }))?; + + // Send request with api_key header (like your example) + let response_text = self.http_client.post(&self.endpoint) + .body(request_body) + .header("Content-Type", "application/json") + .header("api_key", &self.auth_token) + .send() + .await? + .text() + .await?; + + // Parse JSON response + if let Ok(response_json) = serde_json::from_str::(&response_text) { + if response_json.get("result").is_some() { + println!(" astralane {} submitted: {:?}", trade_type, start_time.elapsed()); + } else if let Some(_error) = response_json.get("error") { + eprintln!(" astralane {} submission failed: {:?}", trade_type, _error); + } + } else { + eprintln!(" astralane {} submission failed: {:?}", trade_type, response_text); + } + + let start_time: Instant = Instant::now(); + match poll_transaction_confirmation(&self.rpc_client, signature).await { + Ok(_) => (), + Err(e) => { + println!(" signature: {:?}", signature); + println!(" astralane {} confirmation failed: {:?}", trade_type, start_time.elapsed()); + return Err(e); + }, + } + println!(" signature: {:?}", signature); + println!(" astralane {} confirmed: {:?}", trade_type, start_time.elapsed()); + + Ok(()) + } + + pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec) -> Result<()> { + for transaction in transactions { + self.send_transaction(trade_type, transaction).await?; + } + Ok(()) + } +} + +impl Drop for AstralaneClient { + 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(); + tokio::spawn(async move { + let mut ping_guard = ping_handle.lock().await; + if let Some(handle) = ping_guard.as_ref() { + handle.abort(); + } + *ping_guard = None; + }); + } +} diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 38787e8..5b8f981 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -8,6 +8,7 @@ pub mod bloxroute; pub mod node1; pub mod flashblock; pub mod blockrazor; +pub mod astralane; use std::sync::Arc; @@ -26,7 +27,8 @@ use crate::{ SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_FLASHBLOCK, - SWQOS_ENDPOINTS_BLOCKRAZOR + SWQOS_ENDPOINTS_BLOCKRAZOR, + SWQOS_ENDPOINTS_ASTRALANE }, swqos::{ bloxroute::BloxrouteClient, @@ -37,7 +39,8 @@ use crate::{ zeroslot::ZeroSlotClient, node1::Node1Client, flashblock::FlashBlockClient, - blockrazor::BlockRazorClient + blockrazor::BlockRazorClient, + astralane::AstralaneClient } }; @@ -75,6 +78,7 @@ pub enum SwqosType { Node1, FlashBlock, BlockRazor, + Astralane, Default, } @@ -111,6 +115,7 @@ pub enum SwqosConfig { Node1(String, SwqosRegion, Option), FlashBlock(String, SwqosRegion, Option), BlockRazor(String, SwqosRegion, Option), + Astralane(String, SwqosRegion, Option), } impl SwqosConfig { @@ -128,6 +133,7 @@ impl SwqosConfig { SwqosType::Node1 => SWQOS_ENDPOINTS_NODE1[region as usize].to_string(), SwqosType::FlashBlock => SWQOS_ENDPOINTS_FLASHBLOCK[region as usize].to_string(), SwqosType::BlockRazor => SWQOS_ENDPOINTS_BLOCKRAZOR[region as usize].to_string(), + SwqosType::Astralane => SWQOS_ENDPOINTS_ASTRALANE[region as usize].to_string(), SwqosType::Default => "".to_string(), } } @@ -206,6 +212,15 @@ impl SwqosConfig { ); Arc::new(blockrazor_client) }, + SwqosConfig::Astralane(auth_token, region, url) => { + let endpoint = SwqosConfig::get_endpoint(SwqosType::Astralane, region, url); + let astralane_client = AstralaneClient::new( + rpc_url.clone(), + endpoint.to_string(), + auth_token + ); + Arc::new(astralane_client) + }, SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment( endpoint,