diff --git a/README.md b/README.md index 9999479..961e1a9 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, Node1, and other services +9. **Multiple MEV Protection**: Support for Jito, Nextblock, ZeroSlot, Temporal, Bloxroute, FlashBlock, BlockRazor, Node1, 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 @@ -117,6 +117,7 @@ When configuring SWQOS services, note the different parameter requirements for e - **ZeroSlot**: The first parameter is API Token - **Temporal**: The first parameter is API Token - **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! When using multiple MEV services, you need to use `Durable Nonce`. You need to initialize a `NonceCache` class (or write your own nonce management class), get the latest `nonce` value, and use it as the `blockhash` when trading. @@ -173,8 +174,9 @@ let trade_config = TradeConfig { - **ZeroSlot**: Zero-latency transactions - **Temporal**: Time-sensitive transactions - **Bloxroute**: Blockchain network acceleration -- **FlashBlock**: High-speed transaction execution with API key authentication -- **Node1**: High-speed transaction execution with API key authentication +- **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) ## New Architecture Features diff --git a/README_CN.md b/README_CN.md index adffb95..634a792 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、Node1 等服务 +9. **多种 MEV 保护**: 支持 Jito、Nextblock、ZeroSlot、Temporal、Bloxroute、FlashBlock、BlockRazor、Node1 等服务 10. **并发交易**: 同时使用多个 MEV 服务发送交易,最快的成功,其他失败 11. **统一交易接口**: 使用统一的交易协议枚举进行交易操作 12. **中间件系统**: 支持自定义指令中间件,可在交易执行前对指令进行修改、添加或移除 @@ -117,6 +117,7 @@ sol-trade-sdk = "0.5.7" - **ZeroSlot**: 第一个参数是 API Token - **Temporal**: 第一个参数是 API Token - **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立即加速你的交易! 当使用多个MEV服务时,需要使用`Durable Nonce`。你需要初始化`NonceCache`类(或者自行写一个管理nonce的类),获取最新的`nonce`值,并在交易的时候作为`blockhash`使用。 @@ -174,6 +175,7 @@ let trade_config = TradeConfig { - **Temporal**: 时间敏感交易 - **Bloxroute**: 区块链网络加速 - **FlashBlock**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://doc.flashblock.trade/) +- **BlockRazor**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://blockrazor.gitbook.io/blockrazor/) - **Node1**: 高速交易执行,支持 API 密钥认证 - [官方文档](https://node1.me/docs.html) ## 新架构特性 diff --git a/examples/trading_client/src/main.rs b/examples/trading_client/src/main.rs index 3892e38..75bcba1 100644 --- a/examples/trading_client/src/main.rs +++ b/examples/trading_client/src/main.rs @@ -44,6 +44,7 @@ fn create_swqos_configs(rpc_url: &str) -> Vec { SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt), // Add tg official customer https://t.me/node1_me to get free Node1 key SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Default(rpc_url.to_string()), ] } diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index b147c60..0e628ff 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -81,6 +81,23 @@ pub const FLASHBLOCK_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("FLAsHZTRcf3Dy1APaz6j74ebdMC6Xx4g6i9YxjyrDybR"), ]; +pub const BLOCKRAZOR_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("FjmZZrFvhnqqb9ThCuMVnENaM3JGVuGWNyCAxRJcFpg9"), + pubkey!("6No2i3aawzHsjtThw81iq1EXPJN6rh8eSJCLaYZfKDTG"), + pubkey!("A9cWowVAiHe9pJfKAj3TJiN9VpbzMUq6E4kEvf5mUT22"), + pubkey!("Gywj98ophM7GmkDdaWs4isqZnDdFCW7B46TXmKfvyqSm"), + pubkey!("68Pwb4jS7eZATjDfhmTXgRJjCiZmw1L7Huy4HNpnxJ3o"), + pubkey!("4ABhJh5rZPjv63RBJBuyWzBK3g9gWMUQdTZP2kiW31V9"), + pubkey!("B2M4NG5eyZp5SBQrSdtemzk5TqVuaWGQnowGaCBt8GyM"), + pubkey!("5jA59cXMKQqZAVdtopv8q3yyw9SYfiE3vUCbt7p8MfVf"), + pubkey!("5YktoWygr1Bp9wiS1xtMtUki1PeYuuzuCF98tqwYxf61"), + pubkey!("295Avbam4qGShBYK7E9H5Ldew4B3WyJGmgmXfiWdeeyV"), + pubkey!("EDi4rSy2LZgKJX74mbLTFk4mxoTgT6F7HxxzG2HBAFyK"), + pubkey!("BnGKHAC386n4Qmv9xtpBVbRaUTKixjBe3oagkPFKtoy6"), + pubkey!("Dd7K2Fp7AtoN8xCghKDRmyqr5U169t48Tw5fEd3wT9mq"), + pubkey!("AP6qExwrbRgBAVaehg4b5xHENX815sMabtBzUzVB4v8S"), +]; + // NewYork, // Frankfurt, // Amsterdam, @@ -167,3 +184,14 @@ pub const SWQOS_ENDPOINTS_FLASHBLOCK: [&str; 8] = [ "http://ny.flashblock.trade", ]; +pub const SWQOS_ENDPOINTS_BLOCKRAZOR: [&str; 8] = [ + "http://newyork.solana.blockrazor.xyz:443/sendTransaction", + "http://frankfurt.solana.blockrazor.xyz:443/sendTransaction", + "http://amsterdam.solana.blockrazor.xyz:443/sendTransaction", + "http://newyork.solana.blockrazor.xyz:443/sendTransaction", + "http://tokyo.solana.blockrazor.xyz:443/sendTransaction", + "http://frankfurt.solana.blockrazor.xyz:443/sendTransaction", + "http://newyork.solana.blockrazor.xyz:443/sendTransaction", + "http://frankfurt.solana.blockrazor.xyz:443/sendTransaction", +]; + diff --git a/src/main.rs b/src/main.rs index e2fdcec..524cc70 100755 --- a/src/main.rs +++ b/src/main.rs @@ -82,6 +82,7 @@ fn create_swqos_configs(rpc_url: &str) -> Vec { SwqosConfig::Temporal("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Node1("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::FlashBlock("your api_token".to_string(), SwqosRegion::Frankfurt), + SwqosConfig::BlockRazor("your api_token".to_string(), SwqosRegion::Frankfurt), SwqosConfig::Default(rpc_url.to_string()), ] } diff --git a/src/swqos/blockrazor.rs b/src/swqos/blockrazor.rs new file mode 100644 index 0000000..38ec7a7 --- /dev/null +++ b/src/swqos/blockrazor.rs @@ -0,0 +1,227 @@ +use crate::swqos::common::{poll_transaction_confirmation, serialize_transaction_and_encode}; +use rand::seq::IndexedRandom; +use reqwest::{Client, header::{HeaderMap, HeaderValue, CONTENT_TYPE}}; +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::BLOCKRAZOR_TIP_ACCOUNTS}; + +use tokio::task::JoinHandle; +use std::sync::atomic::{AtomicBool, Ordering}; + +#[derive(Clone)] +pub struct BlockRazorClient { + 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 BlockRazorClient { + 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 = *BLOCKRAZOR_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| BLOCKRAZOR_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::BlockRazor + } +} + +impl BlockRazorClient { + 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 + if let Err(e) = Self::send_ping_request(&http_client, &endpoint, &auth_token).await { + eprintln!("BlockRazor 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 /health endpoint + async fn send_ping_request(http_client: &Client, endpoint: &str, auth_token: &str) -> Result<()> { + // Build health URL by replacing sendTransaction with health + let ping_url = if endpoint.ends_with("sendTransaction") { + endpoint.replace("sendTransaction", "health") + } else if endpoint.ends_with("/sendTransaction") { + endpoint.replace("/sendTransaction", "/health") + } else { + // Fallback to original logic if endpoint doesn't end with sendTransaction + if endpoint.ends_with('/') { + format!("{}health", endpoint) + } else { + format!("{}/health", endpoint) + } + }; + + // Prepare headers + let mut headers = HeaderMap::new(); + headers.insert("apikey", HeaderValue::from_str(auth_token)?); + headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); + + // Send GET request to /health endpoint with headers + let response = http_client.get(&ping_url) + .headers(headers) + .send() + .await?; + + if response.status().is_success() { + // ping successful, connection remains active + // Can optionally log, but to reduce noise, not printing here + } else { + eprintln!("BlockRazor ping request failed with status: {}, body: {}", response.status(), body); + } + + 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()); + + // BlockRazor使用fast模式的请求格式 + let request_body = serde_json::to_string(&json!({ + "transaction": content, + "mode": "fast" + }))?; + + // BlockRazor使用apikey header + let response_text = self.http_client.post(&self.endpoint) + .body(request_body) + .header("Content-Type", "application/json") + .header("apikey", &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() || response_json.get("signature").is_some() { + println!(" blockrazor {} submitted: {:?}", trade_type, start_time.elapsed()); + } else if let Some(_error) = response_json.get("error") { + eprintln!(" blockrazor {} submission failed: {:?}", trade_type, _error); + } + } else { + eprintln!(" blockrazor {} 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!(" blockrazor {} confirmation failed: {:?}", trade_type, start_time.elapsed()); + return Err(e); + }, + } + println!(" signature: {:?}", signature); + println!(" blockrazor {} 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 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(); + 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 17014d0..22382e8 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -7,6 +7,7 @@ pub mod temporal; pub mod bloxroute; pub mod node1; pub mod flashblock; +pub mod blockrazor; use std::sync::Arc; @@ -24,7 +25,8 @@ use crate::{ SWQOS_ENDPOINTS_TEMPORAL, SWQOS_ENDPOINTS_ZERO_SLOT, SWQOS_ENDPOINTS_NODE1, - SWQOS_ENDPOINTS_FLASHBLOCK + SWQOS_ENDPOINTS_FLASHBLOCK, + SWQOS_ENDPOINTS_BLOCKRAZOR }, swqos::{ bloxroute::BloxrouteClient, @@ -34,7 +36,8 @@ use crate::{ temporal::TemporalClient, zeroslot::ZeroSlotClient, node1::Node1Client, - flashblock::FlashBlockClient + flashblock::FlashBlockClient, + blockrazor::BlockRazorClient } }; @@ -71,6 +74,7 @@ pub enum SwqosType { Bloxroute, Node1, FlashBlock, + BlockRazor, Default, } @@ -106,6 +110,7 @@ pub enum SwqosConfig { ZeroSlot(String, SwqosRegion), Node1(String, SwqosRegion), FlashBlock(String, SwqosRegion), + BlockRazor(String, SwqosRegion), } impl SwqosConfig { @@ -118,6 +123,7 @@ impl SwqosConfig { SwqosType::Bloxroute => SWQOS_ENDPOINTS_BLOX[region as usize].to_string(), 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::Default => "".to_string(), } } @@ -187,6 +193,15 @@ impl SwqosConfig { ); Arc::new(flashblock_client) }, + SwqosConfig::BlockRazor(auth_token, region) => { + let endpoint = SwqosConfig::get_endpoint(SwqosType::BlockRazor, region); + let blockrazor_client = BlockRazorClient::new( + rpc_url.clone(), + endpoint.to_string(), + auth_token + ); + Arc::new(blockrazor_client) + }, SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment( endpoint,