feat: major gas fee strategy and documentation overhaul

- feat(gas): add comprehensive gas fee strategy management system
  * Implement GasFeeStrategyType with Default/LowTipHighCuPrice/HighTipLowCuPrice strategies
  * Add dynamic gas fee configuration per SWQOS service and trade type
  * Integrate arc-swap for thread-safe strategy updates

- docs: restructure documentation with dedicated guides
  * Add ADDRESS_LOOKUP_TABLE.md/CN.md for lookup table configuration
  * Add NONCE_CACHE.md/CN.md for nonce management documentation
  * Add TRADING_PARAMETERS.md/CN.md for comprehensive parameter guides
  * Simplify main README.md with focused overview

- refactor: update all examples with new gas fee strategy integration
  * Modernize 14 trading examples with consistent parameter handling
  * Improve error handling and configuration management
  * Enhance parallel trading implementation
This commit is contained in:
ysq
2025-09-19 00:49:45 +08:00
parent fad343ffbf
commit 006686e8a3
45 changed files with 2133 additions and 1681 deletions
+316
View File
@@ -0,0 +1,316 @@
use crate::swqos::{SwqosType, TradeType};
use arc_swap::ArcSwap;
use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GasFeeStrategyType {
Default,
LowTipHighCuPrice,
HighTipLowCuPrice,
}
impl GasFeeStrategyType {
pub fn values() -> Vec<Self> {
vec![Self::Default, Self::LowTipHighCuPrice, Self::HighTipLowCuPrice]
}
}
#[derive(Debug, Clone, Copy)]
pub struct GasFeeStrategyValue {
pub cu_limit: u32,
pub cu_price: u64,
pub tip: f64,
}
#[derive(Debug)]
pub struct GasFeeStrategy {
strategies: ArcSwap<HashMap<(SwqosType, TradeType, GasFeeStrategyType), GasFeeStrategyValue>>,
enabled_types: ArcSwap<HashMap<TradeType, Vec<GasFeeStrategyType>>>,
swqos_disabled_types: ArcSwap<HashMap<(SwqosType, TradeType), Vec<GasFeeStrategyType>>>,
}
static INSTANCE: OnceLock<Arc<GasFeeStrategy>> = OnceLock::new();
impl GasFeeStrategy {
pub fn instance() -> Arc<GasFeeStrategy> {
INSTANCE.get_or_init(|| Arc::new(GasFeeStrategy::new())).clone()
}
fn new() -> Self {
Self {
strategies: ArcSwap::new(Arc::new(HashMap::new())),
enabled_types: ArcSwap::new(Arc::new(HashMap::new())),
swqos_disabled_types: ArcSwap::new(Arc::new(HashMap::new())),
}
}
pub fn add_high_low_fee_strategies(
&self,
swqos_types: &[SwqosType],
trade_type: TradeType,
cu_limit: u32,
low_cu_price: u64,
high_cu_price: u64,
low_tip: f64,
high_tip: f64,
) -> &Self {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for swqos_type in swqos_types {
if swqos_type.eq(&SwqosType::Default) {
continue;
}
new_map.insert(
(*swqos_type, trade_type, GasFeeStrategyType::LowTipHighCuPrice),
GasFeeStrategyValue { cu_limit, cu_price: high_cu_price, tip: low_tip },
);
new_map.insert(
(*swqos_type, trade_type, GasFeeStrategyType::HighTipLowCuPrice),
GasFeeStrategyValue { cu_limit, cu_price: low_cu_price, tip: high_tip },
);
}
Arc::new(new_map)
});
self
}
pub fn add_high_low_fee_strategy(
&self,
swqos_type: SwqosType,
trade_type: TradeType,
cu_limit: u32,
low_cu_price: u64,
high_cu_price: u64,
low_tip: f64,
high_tip: f64,
) -> &Self {
if swqos_type.eq(&SwqosType::Default) {
return self;
}
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
new_map.insert(
(swqos_type, trade_type, GasFeeStrategyType::LowTipHighCuPrice),
GasFeeStrategyValue { cu_limit, cu_price: high_cu_price, tip: low_tip },
);
new_map.insert(
(swqos_type, trade_type, GasFeeStrategyType::HighTipLowCuPrice),
GasFeeStrategyValue { cu_limit, cu_price: low_cu_price, tip: high_tip },
);
Arc::new(new_map)
});
self
}
pub fn add_default_fee_strategies(
&self,
swqos_types: &[SwqosType],
trade_type: TradeType,
cu_price: u64,
tip: f64,
cu_limit: u32,
) -> &Self {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
for swqos_type in swqos_types {
new_map.insert(
(*swqos_type, trade_type, GasFeeStrategyType::Default),
GasFeeStrategyValue { cu_limit, cu_price, tip },
);
}
Arc::new(new_map)
});
self
}
pub fn add_default_fee_strategy(
&self,
swqos_type: SwqosType,
trade_type: TradeType,
cu_price: u64,
tip: f64,
cu_limit: u32,
) -> &Self {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
new_map.insert(
(swqos_type, trade_type, GasFeeStrategyType::Default),
GasFeeStrategyValue { cu_limit, cu_price, tip },
);
Arc::new(new_map)
});
self
}
pub fn remove_default_fee_strategy(
&self,
swqos_type: SwqosType,
trade_type: TradeType,
) -> &Self {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
new_map.remove(&(swqos_type, trade_type, GasFeeStrategyType::Default));
Arc::new(new_map)
});
self
}
pub fn remove_high_low_fee_strategy(
&self,
swqos_type: SwqosType,
trade_type: TradeType,
) -> &Self {
self.strategies.rcu(|current_map| {
let mut new_map = (**current_map).clone();
new_map.remove(&(swqos_type, trade_type, GasFeeStrategyType::LowTipHighCuPrice));
new_map.remove(&(swqos_type, trade_type, GasFeeStrategyType::HighTipLowCuPrice));
Arc::new(new_map)
});
self
}
pub fn get_all_strategies(
&self,
) -> HashMap<(SwqosType, TradeType, GasFeeStrategyType), GasFeeStrategyValue> {
(**self.strategies.load()).clone()
}
pub fn get_strategies(
&self,
trade_type: TradeType,
) -> Vec<(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)> {
let strategies = self.strategies.load();
let mut result = Vec::new();
let mut swqos_types = std::collections::HashSet::new();
for (swqos_type, t_type, _) in strategies.keys() {
if *t_type == trade_type {
swqos_types.insert(*swqos_type);
}
}
for swqos_type in swqos_types {
for strategy_type in GasFeeStrategyType::values() {
if let Some(strategy_value) =
strategies.get(&(swqos_type, trade_type, strategy_type))
{
result.push((swqos_type, strategy_type, *strategy_value));
}
}
}
result
}
pub fn get_available_strategies(
&self,
trade_type: TradeType,
) -> Vec<(SwqosType, GasFeeStrategyType, GasFeeStrategyValue)> {
let strategies = self.strategies.load();
let enabled_types = self.get_enabled_strategy_types(trade_type);
let mut result = Vec::new();
let mut swqos_types = std::collections::HashSet::new();
for (swqos_type, t_type, _) in strategies.keys() {
if *t_type == trade_type {
swqos_types.insert(*swqos_type);
}
}
for swqos_type in swqos_types {
let disabled_types = self.get_swqos_disabled_strategy_types(swqos_type, trade_type);
for strategy_type in &enabled_types {
if disabled_types.contains(strategy_type) {
continue;
}
if let Some(strategy_value) =
strategies.get(&(swqos_type, trade_type, *strategy_type))
{
result.push((swqos_type, *strategy_type, *strategy_value));
}
}
}
result
}
pub fn set_enabled_strategy_types(
&self,
trade_type: TradeType,
types: &[GasFeeStrategyType],
) -> &Self {
self.enabled_types.rcu(|current_map| {
let mut new_map = (**current_map).clone();
new_map.insert(trade_type, types.to_vec());
Arc::new(new_map)
});
self
}
pub fn get_enabled_strategy_types(&self, trade_type: TradeType) -> Vec<GasFeeStrategyType> {
let strategies = self.enabled_types.load();
(**strategies).get(&trade_type).cloned().unwrap_or_default()
}
pub fn set_swqos_disabled_strategy_types(
&self,
swqos_type: SwqosType,
trade_type: TradeType,
types: &[GasFeeStrategyType],
) -> &Self {
self.swqos_disabled_types.rcu(|current_map| {
let mut new_map = (**current_map).clone();
new_map.insert((swqos_type, trade_type), types.to_vec());
Arc::new(new_map)
});
self
}
pub fn get_swqos_disabled_strategy_types(
&self,
swqos_type: SwqosType,
trade_type: TradeType,
) -> Vec<GasFeeStrategyType> {
let disabled_strategies = self.swqos_disabled_types.load();
(**disabled_strategies).get(&(swqos_type, trade_type)).cloned().unwrap_or_default()
}
pub fn clear(&self) -> &Self {
self.strategies.store(Arc::new(HashMap::new()));
self.enabled_types.store(Arc::new(HashMap::new()));
self.swqos_disabled_types.store(Arc::new(HashMap::new()));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn demo() {
GasFeeStrategy::instance()
// 给SwqosType::Default在 Buy 时添加默认策略
.add_default_fee_strategy(SwqosType::Default, TradeType::Buy, 100, 0.0001, 10)
// 给SwqosType::Jito在 Buy 时添加高低价策略
.add_high_low_fee_strategy(SwqosType::Jito, TradeType::Buy, 10, 100, 10000, 0.001, 0.1)
// 给SwqosType::Jito在 Buy 时添加默认策略
.add_default_fee_strategy(SwqosType::Jito, TradeType::Buy, 100, 0.0001, 10)
// 设置在 Buy 时启用策略 - 启用两个高低价策略、默认策略
.set_enabled_strategy_types(
TradeType::Buy,
&[
GasFeeStrategyType::HighTipLowCuPrice,
GasFeeStrategyType::LowTipHighCuPrice,
GasFeeStrategyType::Default,
],
)
// 设置SwqosType::Jito在 Buy 时禁用策略 - 禁用 (默认策略)
.set_swqos_disabled_strategy_types(
SwqosType::Jito,
TradeType::Buy,
&[GasFeeStrategyType::Default],
);
// 获取在 Buy 时可用的策略
let strategies = GasFeeStrategy::instance().get_available_strategies(TradeType::Buy);
println!("strategies: {:?}", strategies);
// 获取所有 Buy 策略(包括禁用的)
let all_strategies = GasFeeStrategy::instance().get_strategies(TradeType::Buy);
println!("all_strategies: {:?}", all_strategies);
}
}
+2
View File
@@ -6,5 +6,7 @@ pub mod nonce_cache;
pub mod seed;
pub mod subscription_handle;
pub mod types;
pub mod gas_fee_strategy;
pub use types::*;
pub use gas_fee_strategy::*;
+3 -17
View File
@@ -15,8 +15,6 @@ pub struct NonceInfo {
pub nonce_account: Option<Pubkey>,
/// Current nonce value
pub current_nonce: Hash,
/// Next available time (Unix timestamp in seconds)
pub next_buy_time: i64,
/// Whether it has been used
pub used: bool,
}
@@ -39,7 +37,6 @@ impl NonceCache {
nonce_info: Mutex::new(NonceInfo {
nonce_account: None,
current_nonce: Hash::default(),
next_buy_time: 0,
used: false,
}),
})
@@ -50,7 +47,7 @@ impl NonceCache {
/// Initialize nonce information
pub fn init(&self, nonce_account_str: Option<String>) {
let nonce_account = nonce_account_str.and_then(|s| Pubkey::from_str(&s).ok());
self.update_nonce_info_partial(nonce_account, None, None, Some(false));
self.update_nonce_info_partial(nonce_account, None, Some(false));
}
/// Get a copy of NonceInfo
@@ -59,7 +56,6 @@ impl NonceCache {
NonceInfo {
nonce_account: nonce_info.nonce_account,
current_nonce: nonce_info.current_nonce,
next_buy_time: nonce_info.next_buy_time,
used: nonce_info.used,
}
}
@@ -69,7 +65,6 @@ impl NonceCache {
&self,
nonce_account: Option<Pubkey>,
current_nonce: Option<Hash>,
next_buy_time: Option<i64>,
used: Option<bool>,
) {
let mut current = self.nonce_info.lock();
@@ -83,10 +78,6 @@ impl NonceCache {
current.current_nonce = nonce;
}
if let Some(time) = next_buy_time {
current.next_buy_time = time;
}
if let Some(u) = used {
current.used = u;
}
@@ -94,7 +85,7 @@ impl NonceCache {
/// Mark nonce as used
pub fn mark_used(&self) {
self.update_nonce_info_partial(None, None, None, Some(true));
self.update_nonce_info_partial(None, None, Some(true));
}
/// Fetch nonce information using RPC
@@ -109,12 +100,7 @@ impl NonceCache {
let blockhash = data.durable_nonce.as_hash();
let old_nonce_info = self.get_nonce_info();
if old_nonce_info.current_nonce != *blockhash {
self.update_nonce_info_partial(
None,
Some(*blockhash),
None,
Some(false),
);
self.update_nonce_info_partial(None, Some(*blockhash), Some(false));
}
}
}
+3 -61
View File
@@ -1,21 +1,10 @@
use std::sync::Arc;
use crate::{
constants::trade::trade::{
DEFAULT_BUY_TIP_FEE, DEFAULT_RPC_UNIT_LIMIT, DEFAULT_RPC_UNIT_PRICE, DEFAULT_SELL_TIP_FEE,
DEFAULT_TIP_UNIT_LIMIT, DEFAULT_TIP_UNIT_PRICE,
},
swqos::{SwqosClient, SwqosConfig},
};
use serde::Deserialize;
use solana_client::rpc_client::RpcClient;
use solana_sdk::{commitment_config::CommitmentConfig, signature::Keypair};
use crate::swqos::SwqosConfig;
use solana_sdk::commitment_config::CommitmentConfig;
#[derive(Debug, Clone)]
pub struct TradeConfig {
pub rpc_url: String,
pub swqos_configs: Vec<SwqosConfig>,
pub priority_fee: PriorityFee,
pub commitment: CommitmentConfig,
}
@@ -23,58 +12,11 @@ impl TradeConfig {
pub fn new(
rpc_url: String,
swqos_configs: Vec<SwqosConfig>,
priority_fee: PriorityFee,
commitment: CommitmentConfig,
) -> Self {
Self { rpc_url, swqos_configs, priority_fee, commitment }
}
}
#[derive(Debug, Deserialize, Clone, PartialEq)]
pub struct PriorityFee {
pub tip_unit_limit: u32,
pub tip_unit_price: u64,
pub rpc_unit_limit: u32,
pub rpc_unit_price: u64,
// Matches the order of swqos
pub buy_tip_fees: Vec<f64>,
// Matches the order of swqos
pub sell_tip_fees: Vec<f64>,
}
impl Default for PriorityFee {
fn default() -> Self {
Self {
tip_unit_limit: DEFAULT_TIP_UNIT_LIMIT,
tip_unit_price: DEFAULT_TIP_UNIT_PRICE,
rpc_unit_limit: DEFAULT_RPC_UNIT_LIMIT,
rpc_unit_price: DEFAULT_RPC_UNIT_PRICE,
// Matches the order of swqos
buy_tip_fees: vec![DEFAULT_BUY_TIP_FEE],
// Matches the order of swqos
sell_tip_fees: vec![DEFAULT_SELL_TIP_FEE],
}
Self { rpc_url, swqos_configs, commitment }
}
}
pub type SolanaRpcClient = solana_client::nonblocking::rpc_client::RpcClient;
pub struct MethodArgs {
pub payer: Arc<Keypair>,
pub rpc: Arc<RpcClient>,
pub nonblocking_rpc: Arc<SolanaRpcClient>,
pub jito_client: Arc<SwqosClient>,
}
impl MethodArgs {
pub fn new(
payer: Arc<Keypair>,
rpc: Arc<RpcClient>,
nonblocking_rpc: Arc<SolanaRpcClient>,
jito_client: Arc<SwqosClient>,
) -> Self {
Self { payer, rpc, nonblocking_rpc, jito_client }
}
}
pub type AnyResult<T> = anyhow::Result<T>;
+203 -182
View File
@@ -5,10 +5,9 @@ pub mod protos;
pub mod swqos;
pub mod trading;
pub mod utils;
use solana_sdk::signer::Signer;
pub use solana_streamer_sdk;
use crate::common::TradeConfig;
use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
use crate::swqos::SwqosClient;
use crate::swqos::SwqosConfig;
use crate::trading::core::params::BonkParams;
use crate::trading::core::params::PumpFunParams;
@@ -18,23 +17,31 @@ use crate::trading::core::params::RaydiumCpmmParams;
use crate::trading::core::traits::ProtocolParams;
use crate::trading::factory::DexType;
use crate::trading::BuyParams;
use crate::trading::MiddlewareManager;
use crate::trading::SellParams;
use crate::trading::MiddlewareManager;
use crate::trading::TradeFactory;
use common::{PriorityFee, SolanaRpcClient, TradeConfig};
use common::SolanaRpcClient;
use parking_lot::Mutex;
use rustls::crypto::{ring::default_provider, CryptoProvider};
use solana_sdk::hash::Hash;
use solana_sdk::signer::Signer;
use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature};
pub use solana_streamer_sdk;
use std::sync::Arc;
use swqos::SwqosClient;
/// Main trading client for Solana DeFi protocols
///
/// `SolanaTrade` provides a unified interface for trading across multiple Solana DEXs
/// including PumpFun, PumpSwap, Bonk, Raydium AMM V4, and Raydium CPMM.
/// It manages RPC connections, transaction signing, and SWQOS (Solana Web Quality of Service) settings.
pub struct SolanaTrade {
/// The keypair used for signing all transactions
pub payer: Arc<Keypair>,
/// RPC client for blockchain interactions
pub rpc: Arc<SolanaRpcClient>,
pub rpc_client: Vec<Arc<SwqosClient>>,
/// SWQOS clients for transaction priority and routing
pub swqos_clients: Vec<Arc<SwqosClient>>,
pub priority_fee: Arc<PriorityFee>,
/// Optional middleware manager for custom transaction processing
pub middleware_manager: Option<Arc<MiddlewareManager>>,
}
@@ -45,15 +52,94 @@ impl Clone for SolanaTrade {
Self {
payer: self.payer.clone(),
rpc: self.rpc.clone(),
rpc_client: self.rpc_client.clone(),
swqos_clients: self.swqos_clients.clone(),
priority_fee: self.priority_fee.clone(),
middleware_manager: self.middleware_manager.clone(),
}
}
}
/// Parameters for executing buy orders across different DEX protocols
///
/// Contains all necessary configuration for purchasing tokens, including
/// protocol-specific settings, account management options, and transaction preferences.
#[derive(Clone)]
pub struct TradeBuyParams {
// Trading configuration
/// The DEX protocol to use for the trade
pub dex_type: DexType,
/// Public key of the token to purchase
pub mint: Pubkey,
/// Amount of SOL to spend (in lamports)
pub sol_amount: u64,
/// Optional slippage tolerance in basis points (e.g., 100 = 1%)
pub slippage_basis_points: Option<u64>,
/// Recent blockhash for transaction validity
pub recent_blockhash: Hash,
/// Protocol-specific parameters (PumpFun, Raydium, etc.)
pub extension_params: Box<dyn ProtocolParams>,
// Extended configuration
/// Optional address lookup table for transaction size optimization
pub lookup_table_key: Option<Pubkey>,
/// Whether to wait for transaction confirmation before returning
pub wait_transaction_confirmed: bool,
/// Whether to create wrapped SOL associated token account
pub create_wsol_ata: bool,
/// Whether to close wrapped SOL associated token account after trade
pub close_wsol_ata: bool,
/// Whether to create token mint associated token account
pub create_mint_ata: bool,
/// Whether to enable seed-based optimization for account creation
pub open_seed_optimize: bool,
}
/// Parameters for executing sell orders across different DEX protocols
///
/// Contains all necessary configuration for selling tokens, including
/// protocol-specific settings, tip preferences, account management options, and transaction preferences.
#[derive(Clone)]
pub struct TradeSellParams {
// Trading configuration
/// The DEX protocol to use for the trade
pub dex_type: DexType,
/// Public key of the token to sell
pub mint: Pubkey,
/// Amount of tokens to sell (in smallest token units)
pub token_amount: u64,
/// Optional slippage tolerance in basis points (e.g., 100 = 1%)
pub slippage_basis_points: Option<u64>,
/// Recent blockhash for transaction validity
pub recent_blockhash: Hash,
/// Whether to include tip for transaction priority
pub with_tip: bool,
/// Protocol-specific parameters (PumpFun, Raydium, etc.)
pub extension_params: Box<dyn ProtocolParams>,
// Extended configuration
/// Optional address lookup table for transaction size optimization
pub lookup_table_key: Option<Pubkey>,
/// Whether to wait for transaction confirmation before returning
pub wait_transaction_confirmed: bool,
/// Whether to create wrapped SOL associated token account
pub create_wsol_ata: bool,
/// Whether to close wrapped SOL associated token account after trade
pub close_wsol_ata: bool,
/// Whether to enable seed-based optimization for account creation
pub open_seed_optimize: bool,
}
impl SolanaTrade {
/// Creates a new SolanaTrade instance with the specified configuration
///
/// This function initializes the trading system with RPC connection, SWQOS settings,
/// and sets up necessary components for trading operations.
///
/// # Arguments
/// * `payer` - The keypair used for signing transactions
/// * `rpc_url` - Solana RPC endpoint URL
/// * `commitment` - Transaction commitment level for RPC calls
/// * `swqos_settings` - List of SWQOS (Solana Web Quality of Service) configurations
///
/// # Returns
/// Returns a configured `SolanaTrade` instance ready for trading operations
#[inline]
pub async fn new(payer: Arc<Keypair>, trade_config: TradeConfig) -> Self {
crate::common::fast_fn::fast_init(&payer.try_pubkey().unwrap());
@@ -66,7 +152,6 @@ impl SolanaTrade {
let rpc_url = trade_config.rpc_url.clone();
let swqos_configs = trade_config.swqos_configs.clone();
let priority_fee = Arc::new(trade_config.priority_fee.clone());
let commitment = trade_config.commitment.clone();
let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
@@ -76,24 +161,12 @@ impl SolanaTrade {
swqos_clients.push(swqos_client);
}
let rpc = Arc::new(SolanaRpcClient::new_with_commitment(rpc_url.clone(), commitment));
let rpc =
Arc::new(SolanaRpcClient::new_with_commitment(rpc_url.clone(), commitment.clone()));
common::seed::update_rents(&rpc).await.unwrap();
common::seed::start_rent_updater(rpc.clone());
let rpc_client = SwqosConfig::get_swqos_client(
rpc_url.clone(),
commitment,
SwqosConfig::Default(rpc_url),
);
let instance = Self {
payer,
rpc,
rpc_client: vec![rpc_client],
swqos_clients,
priority_fee,
middleware_manager: None,
};
let instance = Self { payer, rpc, swqos_clients, middleware_manager: None };
let mut current = INSTANCE.lock();
*current = Some(Arc::new(instance.clone()));
@@ -101,22 +174,47 @@ impl SolanaTrade {
instance
}
/// Adds a middleware manager to the SolanaTrade instance
///
/// Middleware managers can be used to implement custom logic that runs before or after trading operations,
/// such as logging, monitoring, or custom validation.
///
/// # Arguments
/// * `middleware_manager` - The middleware manager to attach
///
/// # Returns
/// Returns the modified SolanaTrade instance with middleware manager attached
pub fn with_middleware_manager(mut self, middleware_manager: MiddlewareManager) -> Self {
self.middleware_manager = Some(Arc::new(middleware_manager));
self
}
/// Get the RPC client instance
/// Gets the RPC client instance for direct Solana blockchain interactions
///
/// This provides access to the underlying Solana RPC client that can be used
/// for custom blockchain operations outside of the trading framework.
///
/// # Returns
/// Returns a reference to the Arc-wrapped SolanaRpcClient instance
pub fn get_rpc(&self) -> &Arc<SolanaRpcClient> {
&self.rpc
}
/// Get the current instance
/// Gets the current globally shared SolanaTrade instance
///
/// This provides access to the singleton instance that was created with `new()`.
/// Useful for accessing the trading instance from different parts of the application.
///
/// # Returns
/// Returns the Arc-wrapped SolanaTrade instance
///
/// # Panics
/// Panics if no instance has been initialized yet. Make sure to call `new()` first.
pub fn get_instance() -> Arc<Self> {
let instance = INSTANCE.lock();
instance
.as_ref()
.expect("PumpFun instance not initialized. Please call new() first.")
.expect("SolanaTrade instance not initialized. Please call new() first.")
.clone()
}
@@ -124,80 +222,52 @@ impl SolanaTrade {
///
/// # Arguments
///
/// * `dex_type` - The trading protocol to use (PumpFun, PumpSwap, or Bonk)
/// * `mint` - The public key of the token mint to buy
/// * `sol_amount` - Amount of SOL to spend on the purchase (in lamports)
/// * `slippage_basis_points` - Optional slippage tolerance in basis points (e.g., 100 = 1%)
/// * `recent_blockhash` - Recent blockhash for transaction validity
/// * `custom_priority_fee` - Optional custom priority fee for priority processing
/// * `extension_params` - Optional protocol-specific parameters (uses defaults if None)
/// * `lookup_table_key` - Optional address lookup table key for transaction optimization
/// * `wait_transaction_confirmed` - Whether to wait for the transaction to be confirmed
/// * `create_wsol_ata` - Whether to create wSOL ATA account
/// * `close_wsol_ata` - Whether to close wSOL ATA account
/// * `open_seed_optimize` - Whether to open seed optimize
/// * `params` - Buy trade parameters containing all necessary trading configuration
///
/// # Returns
///
/// Returns `Ok(())` if the buy order is successfully executed, or an error if the transaction fails.
/// Returns `Ok(Signature)` with the transaction signature if the buy order is successfully executed,
/// or an error if the transaction fails.
///
/// # Errors
///
/// This function will return an error if:
/// - Invalid protocol parameters are provided
/// - Invalid protocol parameters are provided for the specified DEX type
/// - The transaction fails to execute
/// - Network or RPC errors occur
/// - Insufficient SOL balance for the purchase
pub async fn buy(
&self,
dex_type: DexType,
mint: Pubkey,
sol_amount: u64,
slippage_basis_points: Option<u64>,
recent_blockhash: Hash,
custom_priority_fee: Option<PriorityFee>,
extension_params: Box<dyn ProtocolParams>,
lookup_table_key: Option<Pubkey>,
wait_transaction_confirmed: bool,
create_wsol_ata: bool,
close_wsol_ata: bool,
create_mint_ata: bool,
open_seed_optimize: bool,
) -> Result<Signature, anyhow::Error> {
if slippage_basis_points.is_none() {
/// - Required accounts cannot be created or accessed
pub async fn buy(&self, params: TradeBuyParams) -> Result<Signature, anyhow::Error> {
if params.slippage_basis_points.is_none() {
println!(
"slippage_basis_points is none, use default slippage basis points: {}",
DEFAULT_SLIPPAGE
);
}
let executor = TradeFactory::create_executor(dex_type.clone());
let protocol_params = extension_params;
let executor = TradeFactory::create_executor(params.dex_type.clone());
let protocol_params = params.extension_params;
let mut buy_params = BuyParams {
let buy_params = BuyParams {
rpc: Some(self.rpc.clone()),
payer: self.payer.clone(),
mint: mint,
sol_amount: sol_amount,
slippage_basis_points: slippage_basis_points,
priority_fee: self.priority_fee.clone(),
lookup_table_key,
recent_blockhash,
mint: params.mint,
sol_amount: params.sol_amount,
slippage_basis_points: params.slippage_basis_points,
lookup_table_key: params.lookup_table_key,
recent_blockhash: params.recent_blockhash,
data_size_limit: 256 * 1024,
wait_transaction_confirmed: wait_transaction_confirmed,
wait_transaction_confirmed: params.wait_transaction_confirmed,
protocol_params: protocol_params.clone(),
open_seed_optimize,
create_wsol_ata,
close_wsol_ata,
create_mint_ata,
open_seed_optimize: params.open_seed_optimize,
create_wsol_ata: params.create_wsol_ata,
close_wsol_ata: params.close_wsol_ata,
create_mint_ata: params.create_mint_ata,
swqos_clients: self.swqos_clients.clone(),
middleware_manager: self.middleware_manager.clone(),
};
if custom_priority_fee.is_some() {
buy_params.priority_fee = Arc::new(custom_priority_fee.unwrap());
}
// Validate protocol params
let is_valid_params = match dex_type {
let is_valid_params = match params.dex_type {
DexType::PumpFun => protocol_params.as_any().downcast_ref::<PumpFunParams>().is_some(),
DexType::PumpSwap => {
protocol_params.as_any().downcast_ref::<PumpSwapParams>().is_some()
@@ -222,86 +292,52 @@ impl SolanaTrade {
///
/// # Arguments
///
/// * `dex_type` - The trading protocol to use (PumpFun, PumpSwap, or Bonk)
/// * `mint` - The public key of the token mint to sell
/// * `token_amount` - Amount of tokens to sell (in smallest token units)
/// * `slippage_basis_points` - Optional slippage tolerance in basis points (e.g., 100 = 1%)
/// * `recent_blockhash` - Recent blockhash for transaction validity
/// * `custom_priority_fee` - Optional custom priority fee for priority processing
/// * `with_tip` - Optional boolean to indicate if the transaction should be sent with tip
/// * `extension_params` - Optional protocol-specific parameters (uses defaults if None)
/// * `lookup_table_key` - Optional address lookup table key for transaction optimization
/// * `wait_transaction_confirmed` - Whether to wait for the transaction to be confirmed
/// * `create_wsol_ata` - Whether to create wSOL ATA account
/// * `close_wsol_ata` - Whether to close wSOL ATA account
/// * `open_seed_optimize` - Whether to open seed optimize
/// * `params` - Sell trade parameters containing all necessary trading configuration
///
/// # Returns
///
/// Returns `Ok(())` if the sell order is successfully executed, or an error if the transaction fails.
/// Returns `Ok(Signature)` with the transaction signature if the sell order is successfully executed,
/// or an error if the transaction fails.
///
/// # Errors
///
/// This function will return an error if:
/// - Invalid protocol parameters are provided
/// - Invalid protocol parameters are provided for the specified DEX type
/// - The transaction fails to execute
/// - Network or RPC errors occur
/// - Insufficient token balance for the sale
/// - Token account doesn't exist or is not properly initialized
pub async fn sell(
&self,
dex_type: DexType,
mint: Pubkey,
token_amount: u64,
slippage_basis_points: Option<u64>,
recent_blockhash: Hash,
custom_priority_fee: Option<PriorityFee>,
with_tip: bool,
extension_params: Box<dyn ProtocolParams>,
lookup_table_key: Option<Pubkey>,
wait_transaction_confirmed: bool,
create_wsol_ata: bool,
close_wsol_ata: bool,
open_seed_optimize: bool,
) -> Result<Signature, anyhow::Error> {
if slippage_basis_points.is_none() {
/// - Required accounts cannot be created or accessed
pub async fn sell(&self, params: TradeSellParams) -> Result<Signature, anyhow::Error> {
if params.slippage_basis_points.is_none() {
println!(
"slippage_basis_points is none, use default slippage basis points: {}",
DEFAULT_SLIPPAGE
);
}
let executor = TradeFactory::create_executor(dex_type.clone());
let protocol_params = extension_params;
let executor = TradeFactory::create_executor(params.dex_type.clone());
let protocol_params = params.extension_params;
let mut sell_params = SellParams {
let sell_params = SellParams {
rpc: Some(self.rpc.clone()),
payer: self.payer.clone(),
mint: mint,
token_amount: Some(token_amount),
slippage_basis_points: slippage_basis_points,
priority_fee: self.priority_fee.clone(),
lookup_table_key,
recent_blockhash,
wait_transaction_confirmed: wait_transaction_confirmed,
mint: params.mint,
token_amount: Some(params.token_amount),
slippage_basis_points: params.slippage_basis_points,
lookup_table_key: params.lookup_table_key,
recent_blockhash: params.recent_blockhash,
wait_transaction_confirmed: params.wait_transaction_confirmed,
protocol_params: protocol_params.clone(),
with_tip: with_tip,
open_seed_optimize,
swqos_clients: if !with_tip {
self.rpc_client.clone()
} else {
self.swqos_clients.clone()
},
with_tip: params.with_tip,
open_seed_optimize: params.open_seed_optimize,
swqos_clients: self.swqos_clients.clone(),
middleware_manager: self.middleware_manager.clone(),
create_wsol_ata,
close_wsol_ata,
create_wsol_ata: params.create_wsol_ata,
close_wsol_ata: params.close_wsol_ata,
};
if custom_priority_fee.is_some() {
sell_params.priority_fee = Arc::new(custom_priority_fee.unwrap());
}
// Validate protocol params
let is_valid_params = match dex_type {
let is_valid_params = match params.dex_type {
DexType::PumpFun => protocol_params.as_any().downcast_ref::<PumpFunParams>().is_some(),
DexType::PumpSwap => {
protocol_params.as_any().downcast_ref::<PumpSwapParams>().is_some()
@@ -330,82 +366,59 @@ impl SolanaTrade {
///
/// # Arguments
///
/// * `dex_type` - The trading protocol to use (PumpFun, PumpSwap, or Bonk)
/// * `mint` - The public key of the token mint to sell
/// * `params` - Sell trade parameters (will be modified with calculated token amount)
/// * `amount_token` - Total amount of tokens available (in smallest token units)
/// * `percent` - Percentage of tokens to sell (1-100, where 100 = 100%)
/// * `slippage_basis_points` - Optional slippage tolerance in basis points (e.g., 100 = 1%)
/// * `recent_blockhash` - Recent blockhash for transaction validity
/// * `custom_priority_fee` - Optional custom priority fee for priority processing
/// * `with_tip` - Whether to use tip for priority processing
/// * `extension_params` - Optional protocol-specific parameters (uses defaults if None)
/// * `lookup_table_key` - Optional lookup table key for address lookup optimization
/// * `wait_transaction_confirmed` - Whether to wait for the transaction to be confirmed
///
/// # Returns
///
/// Returns `Ok(())` if the sell order is successfully executed, or an error if the transaction fails.
/// Returns `Ok(Signature)` with the transaction signature if the sell order is successfully executed,
/// or an error if the transaction fails.
///
/// # Errors
///
/// This function will return an error if:
/// - `percent` is 0 or greater than 100
/// - Invalid protocol parameters are provided
/// - Invalid protocol parameters are provided for the specified DEX type
/// - The transaction fails to execute
/// - Network or RPC errors occur
/// - Insufficient token balance for the calculated sale amount
/// - Token account doesn't exist or is not properly initialized
/// - Required accounts cannot be created or accessed
pub async fn sell_by_percent(
&self,
dex_type: DexType,
mint: Pubkey,
mut params: TradeSellParams,
amount_token: u64,
percent: u64,
slippage_basis_points: Option<u64>,
recent_blockhash: Hash,
custom_priority_fee: Option<PriorityFee>,
with_tip: bool,
extension_params: Box<dyn ProtocolParams>,
lookup_table_key: Option<Pubkey>,
wait_transaction_confirmed: bool,
create_wsol_ata: bool,
close_wsol_ata: bool,
open_seed_optimize: bool,
) -> Result<Signature, anyhow::Error> {
if percent == 0 || percent > 100 {
return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
}
let amount = amount_token * percent / 100;
self.sell(
dex_type,
mint,
amount,
slippage_basis_points,
recent_blockhash,
custom_priority_fee,
with_tip,
extension_params,
lookup_table_key,
wait_transaction_confirmed,
create_wsol_ata,
close_wsol_ata,
open_seed_optimize,
)
.await
params.token_amount = amount;
self.sell(params).await
}
/// Wraps SOL into wSOL (Wrapped SOL)
/// Wraps native SOL into wSOL (Wrapped SOL) for use in SPL token operations
///
/// This function creates a wSOL associated token account (if it doesn't exist),
/// transfers the specified amount of SOL to that account, and then syncs the native
/// token balance to make SOL usable as an SPL token.
/// token balance to make SOL usable as an SPL token in trading operations.
///
/// # Arguments
/// - `amount`: The amount of SOL to wrap (in lamports)
/// * `amount` - The amount of SOL to wrap (in lamports)
///
/// # Returns
/// - `Ok(String)`: Transaction signature
/// - `Err(anyhow::Error)`: If the transaction fails
/// * `Ok(String)` - Transaction signature if successful
/// * `Err(anyhow::Error)` - If the transaction fails to execute
///
/// # Errors
///
/// This function will return an error if:
/// - Insufficient SOL balance for the wrap operation
/// - wSOL associated token account creation fails
/// - Transaction fails to execute or confirm
/// - Network or RPC errors occur
pub async fn wrap_sol_to_wsol(&self, amount: u64) -> Result<String, anyhow::Error> {
use crate::trading::common::wsol_manager::handle_wsol;
use solana_sdk::transaction::Transaction;
@@ -417,15 +430,23 @@ impl SolanaTrade {
let signature = self.rpc.send_and_confirm_transaction(&transaction).await?;
Ok(signature.to_string())
}
/// Closes the wSOL account and unwraps SOL back to native SOL
/// Closes the wSOL associated token account and unwraps remaining balance to native SOL
///
/// This function closes the wSOL associated token account, which automatically
/// transfers any remaining wSOL balance back to the account owner as native SOL.
/// This is useful for cleaning up wSOL accounts and recovering wrapped SOL.
/// This is useful for cleaning up wSOL accounts and recovering wrapped SOL after trading operations.
///
/// # Returns
/// - `Ok(String)`: Transaction signature
/// - `Err(anyhow::Error)`: If the transaction fails
/// * `Ok(String)` - Transaction signature if successful
/// * `Err(anyhow::Error)` - If the transaction fails to execute
///
/// # Errors
///
/// This function will return an error if:
/// - wSOL associated token account doesn't exist
/// - Account closure fails due to insufficient permissions
/// - Transaction fails to execute or confirm
/// - Network or RPC errors occur
pub async fn close_wsol(&self) -> Result<String, anyhow::Error> {
use crate::trading::common::wsol_manager::close_wsol;
use solana_sdk::transaction::Transaction;
+5 -6
View File
@@ -149,7 +149,6 @@ impl AstralaneClient {
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",
@@ -175,12 +174,12 @@ impl AstralaneClient {
// Parse JSON response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" astralane {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [astralane] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" astralane {} submission failed: {:?}", trade_type, _error);
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" astralane {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [astralane] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -188,12 +187,12 @@ impl AstralaneClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" astralane {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [astralane] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" astralane {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [astralane] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+5 -6
View File
@@ -156,7 +156,6 @@ impl BlockRazorClient {
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!({
@@ -177,12 +176,12 @@ impl BlockRazorClient {
// Parse JSON response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() || response_json.get("signature").is_some() {
println!(" blockrazor {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [blockrazor] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" blockrazor {} submission failed: {:?}", trade_type, _error);
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" blockrazor {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [blockrazor] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -190,12 +189,12 @@ impl BlockRazorClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" blockrazor {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [blockrazor] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" blockrazor {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [blockrazor] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+5 -7
View File
@@ -60,7 +60,6 @@ impl BloxrouteClient {
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 body = serde_json::json!({
"transaction": {
@@ -83,12 +82,12 @@ impl BloxrouteClient {
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" bloxroute {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [bloxroute] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" bloxroute {} submission failed: {:?}", trade_type, _error);
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" bloxroute {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [bloxroute] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -96,19 +95,18 @@ impl BloxrouteClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" bloxroute {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [bloxroute] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" bloxroute {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [bloxroute] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
pub async fn send_transactions(&self, trade_type: TradeType, transactions: &Vec<VersionedTransaction>) -> Result<()> {
let start_time = Instant::now();
println!(" Transaction encoded to base64: {:?}", start_time.elapsed());
let body = serde_json::json!({
"entries": transactions
+5 -6
View File
@@ -61,7 +61,6 @@ impl FlashBlockClient {
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());
// FlashBlock API format
let request_body = serde_json::to_string(&json!({
@@ -85,12 +84,12 @@ impl FlashBlockClient {
// Parse response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("success").is_some() || response_json.get("result").is_some() {
println!(" FlashBlock {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [FlashBlock] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" FlashBlock {} submission failed: {:?}", trade_type, _error);
eprintln!(" [FlashBlock] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" FlashBlock {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [FlashBlock] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -98,12 +97,12 @@ impl FlashBlockClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" FlashBlock {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [FlashBlock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" FlashBlock {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [FlashBlock] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+5 -6
View File
@@ -64,7 +64,6 @@ impl JitoClient {
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!({
"id": 1,
@@ -99,12 +98,12 @@ impl JitoClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" jito {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [jito] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" jito {} submission failed: {:?}", trade_type, _error);
eprintln!(" [jito] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" jito {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [jito] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -112,12 +111,12 @@ impl JitoClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" jito {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [jito] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" jito {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [jito] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+28 -2
View File
@@ -48,7 +48,7 @@ lazy_static::lazy_static! {
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TradeType {
Create,
CreateAndBuy,
@@ -68,7 +68,7 @@ impl std::fmt::Display for TradeType {
}
}
#[derive(Debug, Clone, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SwqosType {
Jito,
NextBlock,
@@ -82,6 +82,23 @@ pub enum SwqosType {
Default,
}
impl SwqosType {
pub fn values() -> Vec<Self> {
vec![
Self::Jito,
Self::NextBlock,
Self::ZeroSlot,
Self::Temporal,
Self::Bloxroute,
Self::Node1,
Self::FlashBlock,
Self::BlockRazor,
Self::Astralane,
Self::Default,
]
}
}
pub type SwqosClient = dyn SwqosClientTrait + Send + Sync + 'static;
#[async_trait::async_trait]
@@ -107,14 +124,23 @@ pub enum SwqosRegion {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SwqosConfig {
Default(String),
/// Jito(uuid, region, custom_url)
Jito(String, SwqosRegion, Option<String>),
/// NextBlock(api_token, region, custom_url)
NextBlock(String, SwqosRegion, Option<String>),
/// Bloxroute(api_token, region, custom_url)
Bloxroute(String, SwqosRegion, Option<String>),
/// Temporal(api_token, region, custom_url)
Temporal(String, SwqosRegion, Option<String>),
/// ZeroSlot(api_token, region, custom_url)
ZeroSlot(String, SwqosRegion, Option<String>),
/// Node1(api_token, region, custom_url)
Node1(String, SwqosRegion, Option<String>),
/// FlashBlock(api_token, region, custom_url)
FlashBlock(String, SwqosRegion, Option<String>),
/// BlockRazor(api_token, region, custom_url)
BlockRazor(String, SwqosRegion, Option<String>),
/// Astralane(api_token, region, custom_url)
Astralane(String, SwqosRegion, Option<String>),
}
+5 -6
View File
@@ -66,7 +66,6 @@ impl NextBlockClient {
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!({
"transaction": {
@@ -86,12 +85,12 @@ impl NextBlockClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" nextblock {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [nextblock] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" nextblock {} submission failed: {:?}", trade_type, _error);
eprintln!(" [nextblock] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" nextblock {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [nextblock] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -99,12 +98,12 @@ impl NextBlockClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" nextblock {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [nextblock] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" nextblock {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [nextblock] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+5 -6
View File
@@ -143,7 +143,6 @@ impl Node1Client {
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",
@@ -168,12 +167,12 @@ impl Node1Client {
// Parse JSON response
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" node1 {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [node1] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" node1 {} submission failed: {:?}", trade_type, _error);
eprintln!(" [node1] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" node1 {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [node1] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -181,12 +180,12 @@ impl Node1Client {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" node1 {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [node1] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" node1 {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [node1] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+2 -2
View File
@@ -42,12 +42,12 @@ impl SwqosClientTrait for SolRpcClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" rpc {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [rpc] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
}
}
println!(" signature: {:?}", signature);
println!(" rpc {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [rpc] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+4 -5
View File
@@ -147,7 +147,6 @@ impl TemporalClient {
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());
// Build request body according to Nozomi documentation requirements
let request_body = serde_json::to_string(&json!({
@@ -175,12 +174,12 @@ impl TemporalClient {
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" nozomi {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [nozomi] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
// eprintln!("nozomi transaction submission failed: {:?}", _error);
}
} else {
eprintln!(" nozomi {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [nozomi] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -188,12 +187,12 @@ impl TemporalClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" nozomi {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [nozomi] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" nozomi {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [nozomi] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+5 -6
View File
@@ -61,7 +61,6 @@ impl ZeroSlotClient {
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",
@@ -90,12 +89,12 @@ impl ZeroSlotClient {
// 5. Use `serde_json::from_str()` to parse JSON, reducing extra wait from `.json().await?`
if let Ok(response_json) = serde_json::from_str::<serde_json::Value>(&response_text) {
if response_json.get("result").is_some() {
println!(" 0slot {} submitted: {:?}", trade_type, start_time.elapsed());
println!(" [0slot] {} submitted: {:?}", trade_type, start_time.elapsed());
} else if let Some(_error) = response_json.get("error") {
eprintln!(" 0slot {} submission failed: {:?}", trade_type, _error);
eprintln!(" [0slot] {} submission failed: {:?}", trade_type, _error);
}
} else {
eprintln!(" 0slot {} submission failed: {:?}", trade_type, response_text);
eprintln!(" [0slot] {} submission failed: {:?}", trade_type, response_text);
}
let start_time: Instant = Instant::now();
@@ -103,12 +102,12 @@ impl ZeroSlotClient {
Ok(_) => (),
Err(e) => {
println!(" signature: {:?}", signature);
println!(" 0slot {} confirmation failed: {:?}", trade_type, start_time.elapsed());
println!(" [0slot] {} confirmation failed: {:?}", trade_type, start_time.elapsed());
return Err(e);
},
}
println!(" signature: {:?}", signature);
println!(" 0slot {} confirmed: {:?}", trade_type, start_time.elapsed());
println!(" [0slot] {} confirmed: {:?}", trade_type, start_time.elapsed());
Ok(())
}
+8 -10
View File
@@ -1,4 +1,3 @@
use crate::common::PriorityFee;
use dashmap::DashMap;
use once_cell::sync::Lazy;
use smallvec::SmallVec;
@@ -20,19 +19,18 @@ static COMPUTE_BUDGET_CACHE: Lazy<DashMap<ComputeBudgetCacheKey, SmallVec<[Instr
#[inline(always)]
pub fn compute_budget_instructions(
priority_fee: &PriorityFee,
unit_price: u64,
unit_limit: u32,
data_size_limit: u32,
is_rpc: bool,
is_buy: bool,
) -> SmallVec<[Instruction; 3]> {
let (unit_price, unit_limit) = if is_rpc {
(priority_fee.rpc_unit_price, priority_fee.rpc_unit_limit)
} else {
(priority_fee.tip_unit_price, priority_fee.tip_unit_limit)
};
// Create cache key
let cache_key = ComputeBudgetCacheKey { data_size_limit, unit_price, unit_limit, is_buy };
let cache_key = ComputeBudgetCacheKey {
data_size_limit,
unit_price: unit_price,
unit_limit: unit_limit,
is_buy,
};
// Try to get from cache first
if let Some(cached_insts) = COMPUTE_BUDGET_CACHE.get(&cache_key) {
+7 -8
View File
@@ -16,12 +16,13 @@ use super::{
compute_budget_manager::compute_budget_instructions,
nonce_manager::{add_nonce_instruction, get_transaction_blockhash},
};
use crate::{common::PriorityFee, trading::MiddlewareManager};
use crate::trading::MiddlewareManager;
/// Build standard RPC transaction
pub async fn build_transaction(
payer: Arc<Keypair>,
priority_fee: &PriorityFee,
unit_limit: u32,
unit_price: u64,
business_instructions: Vec<Instruction>,
lookup_table_key: Option<Pubkey>,
recent_blockhash: Hash,
@@ -36,10 +37,8 @@ pub async fn build_transaction(
let mut instructions = Vec::with_capacity(business_instructions.len() + 5);
// Add nonce instruction
if is_buy {
if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) {
return Err(e);
}
if let Err(e) = add_nonce_instruction(&mut instructions, payer.as_ref()) {
return Err(e);
}
// Add tip transfer instruction
@@ -53,9 +52,9 @@ pub async fn build_transaction(
// Add compute budget instructions
instructions.extend(compute_budget_instructions(
priority_fee,
unit_price,
unit_limit,
data_size_limit,
!with_tip,
is_buy,
));
+55 -53
View File
@@ -8,9 +8,11 @@ use tokio::sync::mpsc;
use tokio::task::JoinHandle;
use crate::{
common::PriorityFee,
common::GasFeeStrategy,
swqos::{SwqosClient, SwqosType, TradeType},
trading::{common::build_transaction, BuyParams, MiddlewareManager, SellParams},
trading::{
common::build_transaction, BuyParams, SellParams, MiddlewareManager,
},
};
pub async fn buy_parallel_execute(
@@ -22,7 +24,6 @@ pub async fn buy_parallel_execute(
params.swqos_clients,
params.payer,
instructions,
params.priority_fee,
params.lookup_table_key,
params.recent_blockhash,
params.data_size_limit,
@@ -44,7 +45,6 @@ pub async fn sell_parallel_execute(
params.swqos_clients,
params.payer,
instructions,
params.priority_fee,
params.lookup_table_key,
params.recent_blockhash,
0,
@@ -62,7 +62,6 @@ async fn parallel_execute(
swqos_clients: Vec<Arc<SwqosClient>>,
payer: Arc<Keypair>,
instructions: Vec<Instruction>,
priority_fee: Arc<PriorityFee>,
lookup_table_key: Option<Pubkey>,
recent_blockhash: Hash,
data_size_limit: u32,
@@ -72,68 +71,69 @@ async fn parallel_execute(
wait_transaction_confirmed: bool,
with_tip: bool,
) -> Result<Signature> {
if swqos_clients.is_empty() {
return Err(anyhow!("swqos_clients is empty"));
}
if !with_tip
&& swqos_clients
.iter()
.find(|swqos| matches!(swqos.get_swqos_type(), SwqosType::Default))
.is_none()
{
return Err(anyhow!("No Rpc Default Swqos configured"));
}
let cores = core_affinity::get_core_ids().unwrap();
let mut handles: Vec<JoinHandle<Result<Signature>>> = Vec::with_capacity(swqos_clients.len());
if is_buy && with_tip && priority_fee.buy_tip_fees.is_empty() {
return Err(anyhow!("buy_tip_fees is empty"));
}
if !is_buy && with_tip && priority_fee.sell_tip_fees.is_empty() {
return Err(anyhow!("sell_tip_fees is empty"));
}
let instructions = Arc::new(instructions);
for i in 0..swqos_clients.len() {
let swqos_client = swqos_clients[i].clone();
if !with_tip && !matches!(swqos_client.get_swqos_type(), SwqosType::Default) {
continue;
}
// 预先计算所有有效的组合
let task_configs: Vec<_> = swqos_clients
.iter()
.enumerate()
.filter(|(_, swqos_client)| {
with_tip || matches!(swqos_client.get_swqos_type(), SwqosType::Default)
})
.flat_map(|(i, swqos_client)| {
let gas_fee_strategy_configs = GasFeeStrategy::instance()
.get_available_strategies(if is_buy { TradeType::Buy } else { TradeType::Sell });
gas_fee_strategy_configs
.into_iter()
.filter(|config| config.0.eq(&swqos_client.get_swqos_type()))
.map(move |config| (i, swqos_client.clone(), config))
})
.collect();
if task_configs.is_empty() {
return Err(anyhow!("No available gas fee strategy configs"));
}
for (i, swqos_client, gas_fee_strategy_config) in task_configs {
let core_id = cores[i % cores.len()];
let payer = payer.clone();
let instructions = instructions.clone();
let priority_fee = priority_fee.clone();
let core_id = cores[i % cores.len()];
let middleware_manager = middleware_manager.clone();
let swqos_type = swqos_client.get_swqos_type();
let tip_account_str = swqos_client.get_tip_account()?;
let tip_account = Arc::new(Pubkey::from_str(&tip_account_str).unwrap_or_default());
let tip = gas_fee_strategy_config.2.tip;
let unit_limit = gas_fee_strategy_config.2.cu_limit;
let unit_price = gas_fee_strategy_config.2.cu_price;
let swqos_type = swqos_type.clone();
let tip_account = tip_account.clone();
let handle = tokio::spawn(async move {
core_affinity::set_for_current(core_id);
let swqos_type = swqos_client.get_swqos_type();
let mut start = Instant::now();
let tip_account_str = swqos_client.get_tip_account()?;
let tip_account = Arc::new(Pubkey::from_str(&tip_account_str).unwrap_or_default());
let tip_amount = if with_tip {
if is_buy {
if priority_fee.buy_tip_fees.len() > i {
priority_fee.buy_tip_fees[i]
} else {
println!(
"❗️❗️❗️[{:?}] - Using buy_tip_fees[0]: {:?}",
swqos_type, priority_fee.buy_tip_fees[0]
);
priority_fee.buy_tip_fees[0]
}
} else {
if priority_fee.sell_tip_fees.len() > i {
priority_fee.sell_tip_fees[i]
} else {
println!(
"❗️❗️❗️[{:?}] - Using sell_tip_fees[0]: {:?}",
swqos_type, priority_fee.sell_tip_fees[0]
);
priority_fee.sell_tip_fees[0]
}
}
} else {
0.0
};
let tip_amount = if with_tip { tip } else { 0.0 };
let transaction = build_transaction(
payer,
&priority_fee,
unit_limit,
unit_price,
instructions.as_ref().clone(),
lookup_table_key,
recent_blockhash,
@@ -148,8 +148,9 @@ async fn parallel_execute(
.await?;
println!(
"[{:?}] - Building transaction instructions: {:?}",
"[{:?}] - [{:?}] - Building transaction instructions: {:?}",
swqos_type,
gas_fee_strategy_config.1,
start.elapsed()
);
@@ -163,8 +164,9 @@ async fn parallel_execute(
.await?;
println!(
"[{:?}] - Submitting transaction instructions: {:?}",
"[{:?}] - [{:?}] - Submitting transaction instructions: {:?}",
swqos_type,
gas_fee_strategy_config.1,
start.elapsed()
);
@@ -178,7 +180,7 @@ async fn parallel_execute(
handles.push(handle);
}
// Return as soon as any one succeeds
let (tx, mut rx) = mpsc::channel(swqos_clients.len());
let (tx, mut rx) = mpsc::channel(handles.len());
// Start monitoring tasks
for handle in handles {
+1 -3
View File
@@ -1,6 +1,6 @@
use super::traits::ProtocolParams;
use crate::common::bonding_curve::BondingCurveAccount;
use crate::common::{PriorityFee, SolanaRpcClient};
use crate::common::SolanaRpcClient;
use crate::solana_streamer_sdk::streaming::event_parser::common::EventType;
use crate::solana_streamer_sdk::streaming::event_parser::protocols::bonk::BonkTradeEvent;
use crate::swqos::SwqosClient;
@@ -24,7 +24,6 @@ pub struct BuyParams {
pub mint: Pubkey,
pub sol_amount: u64,
pub slippage_basis_points: Option<u64>,
pub priority_fee: Arc<PriorityFee>,
pub lookup_table_key: Option<Pubkey>,
pub recent_blockhash: Hash,
pub data_size_limit: u32,
@@ -46,7 +45,6 @@ pub struct SellParams {
pub mint: Pubkey,
pub token_amount: Option<u64>,
pub slippage_basis_points: Option<u64>,
pub priority_fee: Arc<PriorityFee>,
pub lookup_table_key: Option<Pubkey>,
pub recent_blockhash: Hash,
pub wait_transaction_confirmed: bool,
-105
View File
@@ -1,7 +1,5 @@
pub mod calc;
pub mod price;
use crate::solana_streamer_sdk::streaming::event_parser::protocols::pumpfun::PumpFunTradeEvent;
use crate::trading;
use crate::SolanaTrade;
use solana_sdk::pubkey::Pubkey;
@@ -57,107 +55,4 @@ impl SolanaTrade {
pub async fn close_token_account(&self, mint: &Pubkey) -> Result<(), anyhow::Error> {
trading::common::utils::close_token_account(&self.rpc, self.payer.as_ref(), mint).await
}
// -------------------------------- PumpFun --------------------------------
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub fn get_pumpfun_token_buy_price(&self, amount: u64, trade_info: &PumpFunTradeEvent) -> u64 {
crate::instruction::utils::pumpfun::get_buy_price(amount, trade_info)
}
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub async fn get_pumpfun_token_current_price(
&self,
mint: &Pubkey,
) -> Result<f64, anyhow::Error> {
let (bonding_curve, _) =
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(&self.rpc, mint)
.await?;
let virtual_sol_reserves = bonding_curve.virtual_sol_reserves;
let virtual_token_reserves = bonding_curve.virtual_token_reserves;
Ok(price::pumpfun::price_token_in_sol(virtual_sol_reserves, virtual_token_reserves))
}
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub async fn get_pumpfun_token_real_sol_reserves(
&self,
mint: &Pubkey,
) -> Result<u64, anyhow::Error> {
let (bonding_curve, _) =
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(&self.rpc, mint)
.await?;
let actual_sol_reserves = bonding_curve.real_sol_reserves;
Ok(actual_sol_reserves)
}
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub async fn get_pumpfun_token_creator(&self, mint: &Pubkey) -> Result<Pubkey, anyhow::Error> {
let (bonding_curve, _) =
crate::instruction::utils::pumpfun::fetch_bonding_curve_account(&self.rpc, mint)
.await?;
let creator = bonding_curve.creator;
Ok(creator)
}
// -------------------------------- PumpSwap --------------------------------
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub async fn get_pumpswap_token_current_price(
&self,
pool_address: &Pubkey,
) -> Result<f64, anyhow::Error> {
let pool = crate::instruction::utils::pumpswap::fetch_pool(&self.rpc, pool_address).await?;
let (base_amount, quote_amount) =
crate::instruction::utils::pumpswap::get_token_balances(&pool, &self.rpc).await?;
// Calculate price using constant product formula (x * y = k)
// Price = quote_amount / base_amount
if base_amount == 0 {
return Err(anyhow::anyhow!("Base amount is zero, cannot calculate price"));
}
let price = quote_amount as f64 / base_amount as f64;
Ok(price)
}
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub async fn get_pumpswap_token_real_sol_reserves(
&self,
pool_address: &Pubkey,
) -> Result<u64, anyhow::Error> {
let pool = crate::instruction::utils::pumpswap::fetch_pool(&self.rpc, pool_address).await?;
let (_, quote_amount) =
crate::instruction::utils::pumpswap::get_token_balances(&pool, &self.rpc).await?;
Ok(quote_amount)
}
#[deprecated(since = "0.6.7", note = "This function is deprecated and will be removed in a future version")]
#[inline]
pub async fn get_pumpswap_payer_token_balance(
&self,
pool_address: &Pubkey,
) -> Result<u64, anyhow::Error> {
let pool = crate::instruction::utils::pumpswap::fetch_pool(&self.rpc, pool_address).await?;
let (base_amount, _) =
crate::instruction::utils::pumpswap::get_token_balances(&pool, &self.rpc).await?;
Ok(base_amount)
}
}