diff --git a/CHANGELOG_CN.md b/CHANGELOG_CN.md new file mode 100644 index 0000000..5765952 --- /dev/null +++ b/CHANGELOG_CN.md @@ -0,0 +1,21 @@ +# 更新日志 + +## [3.3.6] - 2025-01-30 + +### 新增 +- **Stellium SWQOS 支持**:全新 Stellium 客户端实现 + - 使用标准 Solana `sendTransaction` RPC 格式 + - 自动连接保活,60 秒 ping 间隔 + - 5 个小费账户用于负载分配 + - 支持 8 个区域端点(纽约、法兰克福、阿姆斯特丹、东京、伦敦等) + - 最低小费要求:0.001 SOL + +### 变更 +- **更新最低小费要求**以提高交易成功率: + - NextBlock: 0.00001 → 0.001 SOL + - ZeroSlot: 0.00001 → 0.001 SOL + - Temporal: 0.00001 → 0.001 SOL + - BloxRoute: 0.00001 → 0.001 SOL + - FlashBlock: 0.00001 → 0.001 SOL + - BlockRazor: 0.00001 → 0.001 SOL +- 增强异步执行器,添加小费验证警告 diff --git a/Cargo.toml b/Cargo.toml index 5b301d0..0c18818 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "3.3.5" +version = "3.3.6" edition = "2021" authors = [ "William ", diff --git a/README.md b/README.md index 7e63f9b..088c680 100644 --- a/README.md +++ b/README.md @@ -87,14 +87,14 @@ Add the dependency to your `Cargo.toml`: ```toml # Add to your Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.3.5" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.3.6" } ``` ### Use crates.io ```toml # Add to your Cargo.toml -sol-trade-sdk = "3.3.5" +sol-trade-sdk = "3.3.6" ``` ## 🛠️ Usage Examples diff --git a/README_CN.md b/README_CN.md index 189a12c..c4c6003 100755 --- a/README_CN.md +++ b/README_CN.md @@ -87,14 +87,14 @@ git clone https://github.com/0xfnzero/sol-trade-sdk ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.3.5" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.3.6" } ``` ### 使用 crates.io ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = "3.3.5" +sol-trade-sdk = "3.3.6" ``` ## 🛠️ 使用示例 diff --git a/examples/bonk_copy_trading/src/main.rs b/examples/bonk_copy_trading/src/main.rs index 9c58a01..bd140a2 100644 --- a/examples/bonk_copy_trading/src/main.rs +++ b/examples/bonk_copy_trading/src/main.rs @@ -3,8 +3,10 @@ use std::sync::{ Arc, }; -use sol_trade_sdk::common::{spl_associated_token_account::get_associated_token_address, GasFeeStrategy}; use sol_trade_sdk::common::TradeConfig; +use sol_trade_sdk::common::{ + fast_fn::get_associated_token_address_with_program_id_fast_use_seed, GasFeeStrategy, +}; use sol_trade_sdk::{ common::AnyResult, swqos::SwqosConfig, @@ -125,7 +127,16 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()> let recent_blockhash = client.rpc.get_latest_blockhash().await?; let gas_fee_strategy = GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy(150000,150000, 500000,500000, 0.001, 0.001, 256 * 1024, 0); + gas_fee_strategy.set_global_fee_strategy( + 150000, + 150000, + 500000, + 500000, + 0.001, + 0.001, + 256 * 1024, + 0, + ); // Buy tokens println!("Buying tokens from Bonk..."); @@ -174,7 +185,12 @@ async fn bonk_copy_trade_with_grpc(trade_info: BonkTradeEvent) -> AnyResult<()> let rpc = client.rpc.clone(); let payer = client.payer.pubkey(); - let account = get_associated_token_address(&payer, &mint_pubkey); + let account = get_associated_token_address_with_program_id_fast_use_seed( + &payer, + &mint_pubkey, + &trade_info.base_token_program, + client.use_seed_optimize, + ); let balance = rpc.get_token_account_balance(&account).await?; println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); diff --git a/examples/bonk_sniper_trading/src/main.rs b/examples/bonk_sniper_trading/src/main.rs index c964e39..2a1e9d3 100644 --- a/examples/bonk_sniper_trading/src/main.rs +++ b/examples/bonk_sniper_trading/src/main.rs @@ -1,4 +1,4 @@ -use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address; +use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed; use sol_trade_sdk::common::TradeConfig; use sol_trade_sdk::{ common::AnyResult, @@ -95,7 +95,16 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult< let recent_blockhash = client.rpc.get_latest_blockhash().await?; let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy(150000,150000, 500000,500000, 0.001, 0.001, 256 * 1024, 0); + gas_fee_strategy.set_global_fee_strategy( + 150000, + 150000, + 500000, + 500000, + 0.001, + 0.001, + 256 * 1024, + 0, + ); let token_type = if trade_info.quote_token_mint == sol_trade_sdk::constants::USD1_TOKEN_ACCOUNT { @@ -144,7 +153,12 @@ async fn bonk_sniper_trade_with_shreds(trade_info: BonkTradeEvent) -> AnyResult< let rpc = client.rpc.clone(); let payer = client.payer.pubkey(); - let account = get_associated_token_address(&payer, &mint_pubkey); + let account = get_associated_token_address_with_program_id_fast_use_seed( + &payer, + &mint_pubkey, + &trade_info.base_token_program, + client.use_seed_optimize, + ); let balance = rpc.get_token_account_balance(&account).await?; println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); diff --git a/examples/pumpfun_copy_trading/src/main.rs b/examples/pumpfun_copy_trading/src/main.rs index efacc89..0f67692 100644 --- a/examples/pumpfun_copy_trading/src/main.rs +++ b/examples/pumpfun_copy_trading/src/main.rs @@ -4,7 +4,7 @@ use std::sync::{ }; use sol_trade_sdk::common::{ - spl_associated_token_account::get_associated_token_address, TradeConfig, + fast_fn::get_associated_token_address_with_program_id_fast_use_seed, TradeConfig, }; use sol_trade_sdk::TradeTokenType; use sol_trade_sdk::{ @@ -121,7 +121,16 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul let recent_blockhash = client.rpc.get_latest_blockhash().await?; let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy(150000,150000, 500000,500000, 0.001, 0.001, 256 * 1024, 0); + gas_fee_strategy.set_global_fee_strategy( + 150000, + 150000, + 500000, + 500000, + 0.001, + 0.001, + 256 * 1024, + 0, + ); // Buy tokens println!("Buying tokens from PumpFun..."); @@ -164,7 +173,12 @@ async fn pumpfun_copy_trade_with_grpc(trade_info: PumpFunTradeEvent) -> AnyResul let rpc = client.rpc.clone(); let payer = client.payer.pubkey(); - let account = get_associated_token_address(&payer, &mint_pubkey); + let account = get_associated_token_address_with_program_id_fast_use_seed( + &payer, + &mint_pubkey, + &trade_info.token_program, + client.use_seed_optimize, + ); let balance = rpc.get_token_account_balance(&account).await?; println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); diff --git a/examples/pumpfun_sniper_trading/src/main.rs b/examples/pumpfun_sniper_trading/src/main.rs index b866cd4..04c1e64 100644 --- a/examples/pumpfun_sniper_trading/src/main.rs +++ b/examples/pumpfun_sniper_trading/src/main.rs @@ -1,4 +1,4 @@ -use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address; +use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed; use sol_trade_sdk::common::TradeConfig; use sol_trade_sdk::TradeTokenType; use sol_trade_sdk::{ @@ -89,7 +89,16 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR let recent_blockhash = client.rpc.get_latest_blockhash().await?; let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy(150000,150000, 500000,500000, 0.001, 0.001, 256 * 1024, 0); + gas_fee_strategy.set_global_fee_strategy( + 150000, + 150000, + 500000, + 500000, + 0.001, + 0.001, + 256 * 1024, + 0, + ); // Buy tokens println!("Buying tokens from PumpFun..."); @@ -130,7 +139,12 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR let rpc = client.rpc.clone(); let payer = client.payer.pubkey(); - let account = get_associated_token_address(&payer, &mint_pubkey); + let account = get_associated_token_address_with_program_id_fast_use_seed( + &payer, + &mint_pubkey, + &trade_info.token_program, + client.use_seed_optimize, + ); let balance = rpc.get_token_account_balance(&account).await?; println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); @@ -144,7 +158,11 @@ async fn pumpfun_sniper_trade_with_shreds(trade_info: PumpFunTradeEvent) -> AnyR slippage_basis_points: slippage_basis_points, recent_blockhash: Some(recent_blockhash), with_tip: false, - extension_params: Box::new(PumpFunParams::immediate_sell(trade_info.creator_vault, trade_info.token_program, true)), + extension_params: Box::new(PumpFunParams::immediate_sell( + trade_info.creator_vault, + trade_info.token_program, + true, + )), address_lookup_table_account: None, wait_transaction_confirmed: true, create_output_token_ata: true, diff --git a/examples/raydium_amm_v4_trading/src/main.rs b/examples/raydium_amm_v4_trading/src/main.rs index cbf5c82..90461fc 100644 --- a/examples/raydium_amm_v4_trading/src/main.rs +++ b/examples/raydium_amm_v4_trading/src/main.rs @@ -1,13 +1,14 @@ +use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed; use sol_trade_sdk::{ - common::TradeConfig, instruction::utils::raydium_amm_v4::fetch_amm_info, - trading::common::get_multi_token_balances, TradeTokenType, -}; -use sol_trade_sdk::{ - common::{spl_associated_token_account::get_associated_token_address, AnyResult}, + common::AnyResult, swqos::SwqosConfig, trading::{core::params::RaydiumAmmV4Params, factory::DexType}, SolanaTrade, }; +use sol_trade_sdk::{ + common::TradeConfig, instruction::utils::raydium_amm_v4::fetch_amm_info, + trading::common::get_multi_token_balances, TradeTokenType, +}; use solana_commitment_config::CommitmentConfig; use solana_sdk::signature::Keypair; use solana_sdk::signer::Signer; @@ -123,8 +124,9 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent) let amm_info = fetch_amm_info(&client.rpc, trade_info.amm).await?; let (coin_reserve, pc_reserve) = get_multi_token_balances(&client.rpc, &amm_info.token_coin, &amm_info.token_pc).await?; - let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT - || amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT { + let mint_pubkey = if amm_info.pc_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT + || amm_info.pc_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT + { amm_info.coin_mint } else { amm_info.pc_mint @@ -140,7 +142,16 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent) ); let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy(150000,150000, 500000,500000, 0.001, 0.001, 256 * 1024, 0); + gas_fee_strategy.set_global_fee_strategy( + 150000, + 150000, + 500000, + 500000, + 0.001, + 0.001, + 256 * 1024, + 0, + ); // Buy tokens println!("Buying tokens from Raydium_amm_v4..."); @@ -172,7 +183,12 @@ async fn raydium_amm_v4_copy_trade_with_grpc(trade_info: RaydiumAmmV4SwapEvent) let rpc = client.rpc.clone(); let payer = client.payer.pubkey(); - let account = get_associated_token_address(&payer, &mint_pubkey); + let account = get_associated_token_address_with_program_id_fast_use_seed( + &payer, + &mint_pubkey, + &trade_info.token_program, + client.use_seed_optimize, + ); let balance = rpc.get_token_account_balance(&account).await?; println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); diff --git a/examples/raydium_cpmm_trading/src/main.rs b/examples/raydium_cpmm_trading/src/main.rs index 7f374a8..78c7f85 100644 --- a/examples/raydium_cpmm_trading/src/main.rs +++ b/examples/raydium_cpmm_trading/src/main.rs @@ -1,6 +1,6 @@ -use sol_trade_sdk::common::spl_associated_token_account::get_associated_token_address; +use sol_trade_sdk::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed; use sol_trade_sdk::common::TradeConfig; -use sol_trade_sdk::constants::{WSOL_TOKEN_ACCOUNT, USDC_TOKEN_ACCOUNT}; +use sol_trade_sdk::constants::{USDC_TOKEN_ACCOUNT, WSOL_TOKEN_ACCOUNT}; use sol_trade_sdk::trading::core::params::RaydiumCpmmParams; use sol_trade_sdk::trading::factory::DexType; use sol_trade_sdk::TradeTokenType; @@ -130,11 +130,20 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) -> let recent_blockhash = client.rpc.get_latest_blockhash().await?; let gas_fee_strategy = sol_trade_sdk::common::GasFeeStrategy::new(); - gas_fee_strategy.set_global_fee_strategy(150000,150000, 500000,500000, 0.001, 0.001, 256 * 1024, 0); + gas_fee_strategy.set_global_fee_strategy( + 150000, + 150000, + 500000, + 500000, + 0.001, + 0.001, + 256 * 1024, + 0, + ); let buy_params = RaydiumCpmmParams::from_pool_address_by_rpc(&client.rpc, &trade_info.pool_state).await?; - + let is_wsol = trade_info.input_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || trade_info.output_token_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT; @@ -166,7 +175,12 @@ async fn raydium_cpmm_copy_trade_with_grpc(trade_info: RaydiumCpmmSwapEvent) -> let rpc = client.rpc.clone(); let payer = client.payer.pubkey(); - let account = get_associated_token_address(&payer, &mint_pubkey); + let account = get_associated_token_address_with_program_id_fast_use_seed( + &payer, + &mint_pubkey, + &trade_info.output_token_program, + client.use_seed_optimize, + ); let balance = rpc.get_token_account_balance(&account).await?; println!("Balance: {:?}", balance); let amount_token = balance.amount.parse::().unwrap(); diff --git a/src/common/spl_associated_token_account.rs b/src/common/spl_associated_token_account.rs index f1e6ce7..8a2222c 100644 --- a/src/common/spl_associated_token_account.rs +++ b/src/common/spl_associated_token_account.rs @@ -5,6 +5,14 @@ use solana_sdk::{ use crate::constants::{ASSOCIATED_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM}; +/// Get the associated token address with a specified token program ID. +/// +/// # Warning +/// **This SDK enables seed optimization by default.** When seed is enabled, you should use +/// [`get_associated_token_address_with_program_id_fast_use_seed`] instead for better performance. +/// This function performs PDA derivation which is slower than the optimized version. +/// +/// [`get_associated_token_address_with_program_id_fast_use_seed`]: crate::common::token_account::get_associated_token_address_with_program_id_fast_use_seed pub fn get_associated_token_address_with_program_id( wallet_address: &Pubkey, token_mint_address: &Pubkey, @@ -17,6 +25,14 @@ pub fn get_associated_token_address_with_program_id( .0 } +/// Get the associated token address for the default token program. +/// +/// # Warning +/// **This SDK enables seed optimization by default.** When seed is enabled, you should use +/// [`get_associated_token_address_with_program_id_fast_use_seed`] instead for better performance. +/// This function performs PDA derivation which is slower than the optimized version. +/// +/// [`get_associated_token_address_with_program_id_fast_use_seed`]: crate::common::token_account::get_associated_token_address_with_program_id_fast_use_seed pub fn get_associated_token_address( wallet_address: &Pubkey, token_mint_address: &Pubkey, diff --git a/src/constants/swqos.rs b/src/constants/swqos.rs index baeab1c..6d8a07f 100755 --- a/src/constants/swqos.rs +++ b/src/constants/swqos.rs @@ -109,6 +109,14 @@ pub const ASTRALANE_TIP_ACCOUNTS: &[Pubkey] = &[ pubkey!("astrawVNP4xDBKT7rAdxrLYiTSTdqtUr63fSMduivXK"), ]; +pub const STELLIUM_TIP_ACCOUNTS: &[Pubkey] = &[ + pubkey!("ste11JV3MLMM7x7EJUM2sXcJC1H7F4jBLnP9a9PG8PH"), + pubkey!("ste11MWPjXCRfQryCshzi86SGhuXjF4Lv6xMXD2AoSt"), + pubkey!("ste11p5x8tJ53H1NbNQsRBg1YNRd4GcVpxtDw8PBpmb"), + pubkey!("ste11p7e2KLYou5bwtt35H7BM6uMdo4pvioGjJXKFcN"), + pubkey!("ste11TMV68LMi1BguM4RQujtbNCZvf1sjsASpqgAvSX"), +]; + // NewYork, // Frankfurt, // Amsterdam, @@ -217,13 +225,25 @@ pub const SWQOS_ENDPOINTS_ASTRALANE: [&str; 8] = [ "http://lim.gateway.astralane.io/iris", ]; +pub const SWQOS_ENDPOINTS_STELLIUM: [&str; 8] = [ + "http://ewr1.flashrpc.com", + "http://fra1.flashrpc.com", + "http://ams1.flashrpc.com", + "http://ewr1.flashrpc.com", + "http://tyo1.flashrpc.com", + "http://lhr1.flashrpc.com", + "http://ewr1.flashrpc.com", + "http://fra1.flashrpc.com", +]; + pub const SWQOS_MIN_TIP_DEFAULT: f64 = 0.00001; // 其它SWQOS默认最低小费 pub const SWQOS_MIN_TIP_JITO: f64 = SWQOS_MIN_TIP_DEFAULT; -pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = SWQOS_MIN_TIP_DEFAULT; -pub const SWQOS_MIN_TIP_ZERO_SLOT: f64 = SWQOS_MIN_TIP_DEFAULT; -pub const SWQOS_MIN_TIP_TEMPORAL: f64 = SWQOS_MIN_TIP_DEFAULT; -pub const SWQOS_MIN_TIP_BLOXROUTE: f64 = SWQOS_MIN_TIP_DEFAULT; +pub const SWQOS_MIN_TIP_NEXTBLOCK: f64 = 0.001; +pub const SWQOS_MIN_TIP_ZERO_SLOT: f64 = 0.001; +pub const SWQOS_MIN_TIP_TEMPORAL: f64 = 0.001; +pub const SWQOS_MIN_TIP_BLOXROUTE: f64 = 0.001; pub const SWQOS_MIN_TIP_NODE1: f64 = 0.002; // 如需更高阈值可调整 -pub const SWQOS_MIN_TIP_FLASHBLOCK: f64 = SWQOS_MIN_TIP_DEFAULT; -pub const SWQOS_MIN_TIP_BLOCKRAZOR: f64 = SWQOS_MIN_TIP_DEFAULT; +pub const SWQOS_MIN_TIP_FLASHBLOCK: f64 = 0.001; +pub const SWQOS_MIN_TIP_BLOCKRAZOR: f64 = 0.001; pub const SWQOS_MIN_TIP_ASTRALANE: f64 = SWQOS_MIN_TIP_DEFAULT; +pub const SWQOS_MIN_TIP_STELLIUM: f64 = 0.001; // Stellium requires minimum 0.001 SOL tip diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 56e4b42..ac4686a 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -10,6 +10,7 @@ pub mod node1; pub mod flashblock; pub mod blockrazor; pub mod astralane; +pub mod stellium; use std::sync::Arc; @@ -20,29 +21,31 @@ use tokio::sync::RwLock; use anyhow::Result; use crate::{ - common::SolanaRpcClient, + 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_BLOX, + SWQOS_ENDPOINTS_JITO, + SWQOS_ENDPOINTS_NEXTBLOCK, + SWQOS_ENDPOINTS_TEMPORAL, + SWQOS_ENDPOINTS_ZERO_SLOT, + SWQOS_ENDPOINTS_NODE1, SWQOS_ENDPOINTS_FLASHBLOCK, SWQOS_ENDPOINTS_BLOCKRAZOR, - SWQOS_ENDPOINTS_ASTRALANE - }, + SWQOS_ENDPOINTS_ASTRALANE, + SWQOS_ENDPOINTS_STELLIUM + }, swqos::{ - bloxroute::BloxrouteClient, - jito::JitoClient, - nextblock::NextBlockClient, - solana_rpc::SolRpcClient, - temporal::TemporalClient, - zeroslot::ZeroSlotClient, - node1::Node1Client, + bloxroute::BloxrouteClient, + jito::JitoClient, + nextblock::NextBlockClient, + solana_rpc::SolRpcClient, + temporal::TemporalClient, + zeroslot::ZeroSlotClient, + node1::Node1Client, flashblock::FlashBlockClient, blockrazor::BlockRazorClient, - astralane::AstralaneClient + astralane::AstralaneClient, + stellium::StelliumClient } }; @@ -81,6 +84,7 @@ pub enum SwqosType { FlashBlock, BlockRazor, Astralane, + Stellium, Default, } @@ -96,6 +100,7 @@ impl SwqosType { Self::FlashBlock, Self::BlockRazor, Self::Astralane, + Self::Stellium, Self::Default, ] } @@ -144,6 +149,8 @@ pub enum SwqosConfig { BlockRazor(String, SwqosRegion, Option), /// Astralane(api_token, region, custom_url) Astralane(String, SwqosRegion, Option), + /// Stellium(api_token, region, custom_url) + Stellium(String, SwqosRegion, Option), } impl SwqosConfig { @@ -151,7 +158,7 @@ impl SwqosConfig { if let Some(custom_url) = url { return custom_url; } - + match swqos_type { SwqosType::Jito => SWQOS_ENDPOINTS_JITO[region as usize].to_string(), SwqosType::NextBlock => SWQOS_ENDPOINTS_NEXTBLOCK[region as usize].to_string(), @@ -162,6 +169,7 @@ impl SwqosConfig { 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::Stellium => SWQOS_ENDPOINTS_STELLIUM[region as usize].to_string(), SwqosType::Default => "".to_string(), } } @@ -249,11 +257,20 @@ impl SwqosConfig { ); 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 + ); + Arc::new(stellium_client) + }, SwqosConfig::Default(endpoint) => { let rpc = SolanaRpcClient::new_with_commitment( endpoint, commitment - ); + ); let rpc_client = SolRpcClient::new(Arc::new(rpc)); Arc::new(rpc_client) } diff --git a/src/swqos/stellium.rs b/src/swqos/stellium.rs new file mode 100644 index 0000000..07494bb --- /dev/null +++ b/src/swqos/stellium.rs @@ -0,0 +1,190 @@ +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::sync::atomic::{AtomicBool, Ordering}; + +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::STELLIUM_TIP_ACCOUNTS}; + + +#[derive(Clone)] +pub struct StelliumClient { + pub endpoint: String, + pub auth_token: String, + pub rpc_client: Arc, + pub http_client: Client, + keep_alive_running: Arc, +} + +#[async_trait::async_trait] +impl SwqosClientTrait for StelliumClient { + 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 = *STELLIUM_TIP_ACCOUNTS.choose(&mut rand::rng()).or_else(|| STELLIUM_TIP_ACCOUNTS.first()).unwrap(); + Ok(tip_account.to_string()) + } + + fn get_swqos_type(&self) -> SwqosType { + SwqosType::Stellium + } +} + +impl StelliumClient { + pub fn new(rpc_url: String, endpoint: String, auth_token: String) -> Self { + let rpc_client = SolanaRpcClient::new(rpc_url); + let http_client = Client::builder() + // Optimized connection pool settings for high performance + .pool_idle_timeout(Duration::from_secs(120)) + .pool_max_idle_per_host(256) // Increased from 64 to 256 + .tcp_keepalive(Some(Duration::from_secs(60))) // Reduced from 1200 to 60 + .tcp_nodelay(true) // Disable Nagle's algorithm for lower latency + .http2_keep_alive_interval(Duration::from_secs(10)) + .http2_keep_alive_timeout(Duration::from_secs(5)) + .http2_adaptive_window(true) // Enable adaptive flow control + .timeout(Duration::from_millis(3000)) // Reduced from 10s to 3s + .connect_timeout(Duration::from_millis(2000)) // Reduced from 5s to 2s + .build() + .unwrap(); + + let keep_alive_running = Arc::new(AtomicBool::new(true)); + + let client = Self { + rpc_client: Arc::new(rpc_client), + endpoint: endpoint.clone(), + auth_token: auth_token.clone(), + http_client: http_client.clone(), + keep_alive_running: keep_alive_running.clone(), + }; + + // 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.keep_alive_running.clone(); + + 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 + let url = format!("{}/{}", endpoint, auth_token); + match http_client.get(&url).send().await { + Ok(response) => { + if !response.status().is_success() { + eprintln!(" [Stellium] Ping failed with status: {}", response.status()); + } + } + Err(e) => { + eprintln!(" [Stellium] Ping request error: {:?}", e); + } + } + } + }); + } + + 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?; + + // Stellium uses standard Solana sendTransaction format + let request_body = serde_json::to_string(&json!({ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": [ + content, + { "encoding": "base64" } + ] + }))?; + + // Build the URL with the API key + let url = format!("{}/{}", self.endpoint, self.auth_token); + + // Send request to Stellium + let response_text = self.http_client.post(&url) + .body(request_body) + .header("Content-Type", "application/json") + .header("Connection", "keep-alive") + .header("Keep-Alive", "timeout=30, max=1000") + .send() + .await? + .text() + .await?; + + // Parse response + if let Ok(response_json) = serde_json::from_str::(&response_text) { + if response_json.get("result").is_some() { + println!(" [Stellium] {} submitted: {:?}", trade_type, start_time.elapsed()); + } else if let Some(_error) = response_json.get("error") { + eprintln!(" [Stellium] {} submission failed: {:?}", trade_type, _error); + } + } else { + eprintln!(" [Stellium] {} 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!(" [Stellium] {} confirmation failed: {:?}", trade_type, start_time.elapsed()); + return Err(e); + }, + } + println!(" signature: {:?}", signature); + println!(" [Stellium] {} 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(()) + } + + /// Stop the ping task + pub fn stop_ping_task(&self) { + self.keep_alive_running.store(false, Ordering::Relaxed); + } +} + +impl Drop for StelliumClient { + fn drop(&mut self) { + // Stop ping task when client is dropped + self.keep_alive_running.store(false, Ordering::Relaxed); + } +} diff --git a/src/trading/core/async_executor.rs b/src/trading/core/async_executor.rs index e5e6e0f..4fc68c1 100644 --- a/src/trading/core/async_executor.rs +++ b/src/trading/core/async_executor.rs @@ -26,6 +26,7 @@ use crate::{ SWQOS_MIN_TIP_FLASHBLOCK, SWQOS_MIN_TIP_BLOCKRAZOR, SWQOS_MIN_TIP_ASTRALANE, + SWQOS_MIN_TIP_STELLIUM, }, }; @@ -167,8 +168,15 @@ pub async fn execute_parallel( SwqosType::FlashBlock => SWQOS_MIN_TIP_FLASHBLOCK, SwqosType::BlockRazor => SWQOS_MIN_TIP_BLOCKRAZOR, SwqosType::Astralane => SWQOS_MIN_TIP_ASTRALANE, + SwqosType::Stellium => SWQOS_MIN_TIP_STELLIUM, SwqosType::Default => SWQOS_MIN_TIP_DEFAULT, }; + if config.2.tip < min_tip { + println!( + "⚠️ Config filtered: {:?} tip {} is below minimum required tip {}", + config.0, config.2.tip, min_tip + ); + } config.2.tip >= min_tip } else { true