sol_trade_sdk/
lib.rs

1pub mod common;
2pub mod constants;
3pub mod instruction;
4pub mod perf;
5pub mod swqos;
6pub mod trading;
7pub mod utils;
8use crate::common::nonce_cache::DurableNonceInfo;
9use crate::common::GasFeeStrategy;
10use crate::common::TradeConfig;
11use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
12use crate::constants::SOL_TOKEN_ACCOUNT;
13use crate::constants::USD1_TOKEN_ACCOUNT;
14use crate::constants::WSOL_TOKEN_ACCOUNT;
15use crate::constants::USDC_TOKEN_ACCOUNT;
16use crate::swqos::SwqosClient;
17use crate::swqos::SwqosConfig;
18use crate::swqos::TradeType;
19use crate::trading::core::params::BonkParams;
20use crate::trading::core::params::MeteoraDammV2Params;
21use crate::trading::core::params::PumpFunParams;
22use crate::trading::core::params::PumpSwapParams;
23use crate::trading::core::params::RaydiumAmmV4Params;
24use crate::trading::core::params::RaydiumCpmmParams;
25use crate::trading::core::traits::ProtocolParams;
26use crate::trading::factory::DexType;
27use crate::trading::MiddlewareManager;
28use crate::trading::SwapParams;
29use crate::trading::TradeFactory;
30use common::SolanaRpcClient;
31use parking_lot::Mutex;
32use rustls::crypto::{ring::default_provider, CryptoProvider};
33use solana_sdk::hash::Hash;
34use solana_sdk::message::AddressLookupTableAccount;
35use solana_sdk::signer::Signer;
36use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature};
37use std::sync::Arc;
38
39/// Type of the token to buy
40#[derive(Clone, PartialEq)]
41pub enum TradeTokenType {
42    SOL,
43    WSOL,
44    USD1,
45    USDC,
46}
47
48/// Main trading client for Solana DeFi protocols
49///
50/// `SolanaTrade` provides a unified interface for trading across multiple Solana DEXs
51/// including PumpFun, PumpSwap, Bonk, Raydium AMM V4, and Raydium CPMM.
52/// It manages RPC connections, transaction signing, and SWQOS (Solana Web Quality of Service) settings.
53pub struct SolanaTrade {
54    /// The keypair used for signing all transactions
55    pub payer: Arc<Keypair>,
56    /// RPC client for blockchain interactions
57    pub rpc: Arc<SolanaRpcClient>,
58    /// SWQOS clients for transaction priority and routing
59    pub swqos_clients: Vec<Arc<SwqosClient>>,
60    /// Optional middleware manager for custom transaction processing
61    pub middleware_manager: Option<Arc<MiddlewareManager>>,
62}
63
64static INSTANCE: Mutex<Option<Arc<SolanaTrade>>> = Mutex::new(None);
65
66impl Clone for SolanaTrade {
67    fn clone(&self) -> Self {
68        Self {
69            payer: self.payer.clone(),
70            rpc: self.rpc.clone(),
71            swqos_clients: self.swqos_clients.clone(),
72            middleware_manager: self.middleware_manager.clone(),
73        }
74    }
75}
76
77/// Parameters for executing buy orders across different DEX protocols
78///
79/// Contains all necessary configuration for purchasing tokens, including
80/// protocol-specific settings, account management options, and transaction preferences.
81#[derive(Clone)]
82pub struct TradeBuyParams {
83    // Trading configuration
84    /// The DEX protocol to use for the trade
85    pub dex_type: DexType,
86    /// Type of the token to buy
87    pub input_token_type: TradeTokenType,
88    /// Public key of the token to purchase
89    pub mint: Pubkey,
90    /// Amount of tokens to buy (in smallest token units)
91    pub input_token_amount: u64,
92    /// Optional slippage tolerance in basis points (e.g., 100 = 1%)
93    pub slippage_basis_points: Option<u64>,
94    /// Recent blockhash for transaction validity
95    pub recent_blockhash: Option<Hash>,
96    /// Protocol-specific parameters (PumpFun, Raydium, etc.)
97    pub extension_params: Box<dyn ProtocolParams>,
98    // Extended configuration
99    /// Optional address lookup table for transaction size optimization
100    pub address_lookup_table_account: Option<AddressLookupTableAccount>,
101    /// Whether to wait for transaction confirmation before returning
102    pub wait_transaction_confirmed: bool,
103    /// Whether to create input token associated token account
104    pub create_input_token_ata: bool,
105    /// Whether to close input token associated token account after trade
106    pub close_input_token_ata: bool,
107    /// Whether to create token mint associated token account
108    pub create_mint_ata: bool,
109    /// Whether to enable seed-based optimization for account creation
110    pub open_seed_optimize: bool,
111    /// Durable nonce information
112    pub durable_nonce: Option<DurableNonceInfo>,
113    /// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
114    pub fixed_output_token_amount: Option<u64>,
115    /// Gas fee strategy
116    pub gas_fee_strategy: GasFeeStrategy,
117    /// Whether to simulate the transaction instead of executing it
118    pub simulate: bool,
119}
120
121/// Parameters for executing sell orders across different DEX protocols
122///
123/// Contains all necessary configuration for selling tokens, including
124/// protocol-specific settings, tip preferences, account management options, and transaction preferences.
125#[derive(Clone)]
126pub struct TradeSellParams {
127    // Trading configuration
128    /// The DEX protocol to use for the trade
129    pub dex_type: DexType,
130    /// Type of the token to sell
131    pub output_token_type: TradeTokenType,
132    /// Public key of the token to sell
133    pub mint: Pubkey,
134    /// Amount of tokens to sell (in smallest token units)
135    pub input_token_amount: u64,
136    /// Optional slippage tolerance in basis points (e.g., 100 = 1%)
137    pub slippage_basis_points: Option<u64>,
138    /// Recent blockhash for transaction validity
139    pub recent_blockhash: Option<Hash>,
140    /// Whether to include tip for transaction priority
141    pub with_tip: bool,
142    /// Protocol-specific parameters (PumpFun, Raydium, etc.)
143    pub extension_params: Box<dyn ProtocolParams>,
144    // Extended configuration
145    /// Optional address lookup table for transaction size optimization
146    pub address_lookup_table_account: Option<AddressLookupTableAccount>,
147    /// Whether to wait for transaction confirmation before returning
148    pub wait_transaction_confirmed: bool,
149    /// Whether to create output token associated token account
150    pub create_output_token_ata: bool,
151    /// Whether to close output token associated token account after trade
152    pub close_output_token_ata: bool,
153    /// Whether to close mint token associated token account after trade
154    pub close_mint_token_ata: bool,
155    /// Whether to enable seed-based optimization for account creation
156    pub open_seed_optimize: bool,
157    /// Durable nonce information
158    pub durable_nonce: Option<DurableNonceInfo>,
159    /// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated)
160    pub fixed_output_token_amount: Option<u64>,
161    /// Gas fee strategy
162    pub gas_fee_strategy: GasFeeStrategy,
163    /// Whether to simulate the transaction instead of executing it
164    pub simulate: bool,
165}
166
167impl SolanaTrade {
168    /// Creates a new SolanaTrade instance with the specified configuration
169    ///
170    /// This function initializes the trading system with RPC connection, SWQOS settings,
171    /// and sets up necessary components for trading operations.
172    ///
173    /// # Arguments
174    /// * `payer` - The keypair used for signing transactions
175    /// * `rpc_url` - Solana RPC endpoint URL
176    /// * `commitment` - Transaction commitment level for RPC calls
177    /// * `swqos_settings` - List of SWQOS (Solana Web Quality of Service) configurations
178    ///
179    /// # Returns
180    /// Returns a configured `SolanaTrade` instance ready for trading operations
181    #[inline]
182    pub async fn new(payer: Arc<Keypair>, trade_config: TradeConfig) -> Self {
183        crate::common::fast_fn::fast_init(&payer.try_pubkey().unwrap());
184
185        if CryptoProvider::get_default().is_none() {
186            let _ = default_provider()
187                .install_default()
188                .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
189        }
190
191        let rpc_url = trade_config.rpc_url.clone();
192        let swqos_configs = trade_config.swqos_configs.clone();
193        let commitment = trade_config.commitment.clone();
194        let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
195
196        for swqos in swqos_configs {
197            let swqos_client =
198                SwqosConfig::get_swqos_client(rpc_url.clone(), commitment.clone(), swqos.clone());
199            swqos_clients.push(swqos_client);
200        }
201
202        let rpc =
203            Arc::new(SolanaRpcClient::new_with_commitment(rpc_url.clone(), commitment.clone()));
204        common::seed::update_rents(&rpc).await.unwrap();
205        common::seed::start_rent_updater(rpc.clone());
206
207        let instance = Self { payer, rpc, swqos_clients, middleware_manager: None };
208
209        let mut current = INSTANCE.lock();
210        *current = Some(Arc::new(instance.clone()));
211
212        instance
213    }
214
215    /// Adds a middleware manager to the SolanaTrade instance
216    ///
217    /// Middleware managers can be used to implement custom logic that runs before or after trading operations,
218    /// such as logging, monitoring, or custom validation.
219    ///
220    /// # Arguments
221    /// * `middleware_manager` - The middleware manager to attach
222    ///
223    /// # Returns
224    /// Returns the modified SolanaTrade instance with middleware manager attached
225    pub fn with_middleware_manager(mut self, middleware_manager: MiddlewareManager) -> Self {
226        self.middleware_manager = Some(Arc::new(middleware_manager));
227        self
228    }
229
230    /// Gets the RPC client instance for direct Solana blockchain interactions
231    ///
232    /// This provides access to the underlying Solana RPC client that can be used
233    /// for custom blockchain operations outside of the trading framework.
234    ///
235    /// # Returns
236    /// Returns a reference to the Arc-wrapped SolanaRpcClient instance
237    pub fn get_rpc(&self) -> &Arc<SolanaRpcClient> {
238        &self.rpc
239    }
240
241    /// Gets the current globally shared SolanaTrade instance
242    ///
243    /// This provides access to the singleton instance that was created with `new()`.
244    /// Useful for accessing the trading instance from different parts of the application.
245    ///
246    /// # Returns
247    /// Returns the Arc-wrapped SolanaTrade instance
248    ///
249    /// # Panics
250    /// Panics if no instance has been initialized yet. Make sure to call `new()` first.
251    pub fn get_instance() -> Arc<Self> {
252        let instance = INSTANCE.lock();
253        instance
254            .as_ref()
255            .expect("SolanaTrade instance not initialized. Please call new() first.")
256            .clone()
257    }
258
259    /// Execute a buy order for a specified token
260    ///
261    /// # Arguments
262    ///
263    /// * `params` - Buy trade parameters containing all necessary trading configuration
264    ///
265    /// # Returns
266    ///
267    /// Returns `Ok(Signature)` with the transaction signature if the buy order is successfully executed,
268    /// or an error if the transaction fails.
269    ///
270    /// # Errors
271    ///
272    /// This function will return an error if:
273    /// - Invalid protocol parameters are provided for the specified DEX type
274    /// - The transaction fails to execute
275    /// - Network or RPC errors occur
276    /// - Insufficient SOL balance for the purchase
277    /// - Required accounts cannot be created or accessed
278    pub async fn buy(&self, params: TradeBuyParams) -> Result<(bool, Signature), anyhow::Error> {
279        if params.slippage_basis_points.is_none() {
280            println!(
281                "slippage_basis_points is none, use default slippage basis points: {}",
282                DEFAULT_SLIPPAGE
283            );
284        }
285        if params.input_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk {
286            return Err(anyhow::anyhow!(
287                " Current version only support USD1 trading on Bonk protocols"
288            ));
289        }
290        let input_token_mint = if params.input_token_type == TradeTokenType::SOL {
291            SOL_TOKEN_ACCOUNT
292        } else if params.input_token_type == TradeTokenType::WSOL {
293            WSOL_TOKEN_ACCOUNT
294        } else if params.input_token_type == TradeTokenType::USDC {
295            USDC_TOKEN_ACCOUNT
296        } else {
297            USD1_TOKEN_ACCOUNT
298        };
299        let executor = TradeFactory::create_executor(params.dex_type.clone());
300        let protocol_params = params.extension_params;
301        let buy_params = SwapParams {
302            rpc: Some(self.rpc.clone()),
303            payer: self.payer.clone(),
304            trade_type: TradeType::Buy,
305            input_mint: input_token_mint,
306            output_mint: params.mint,
307            input_token_program: None,
308            output_token_program: None,
309            input_amount: Some(params.input_token_amount),
310            slippage_basis_points: params.slippage_basis_points,
311            address_lookup_table_account: params.address_lookup_table_account,
312            recent_blockhash: params.recent_blockhash,
313            data_size_limit: 256 * 1024,
314            wait_transaction_confirmed: params.wait_transaction_confirmed,
315            protocol_params: protocol_params.clone(),
316            open_seed_optimize: params.open_seed_optimize,
317            swqos_clients: self.swqos_clients.clone(),
318            middleware_manager: self.middleware_manager.clone(),
319            durable_nonce: params.durable_nonce,
320            with_tip: true,
321            create_input_mint_ata: params.create_input_token_ata,
322            close_input_mint_ata: params.close_input_token_ata,
323            create_output_mint_ata: params.create_mint_ata,
324            close_output_mint_ata: false,
325            fixed_output_amount: params.fixed_output_token_amount,
326            gas_fee_strategy: params.gas_fee_strategy,
327            simulate: params.simulate,
328        };
329
330        // Validate protocol params
331        let is_valid_params = match params.dex_type {
332            DexType::PumpFun => protocol_params.as_any().downcast_ref::<PumpFunParams>().is_some(),
333            DexType::PumpSwap => {
334                protocol_params.as_any().downcast_ref::<PumpSwapParams>().is_some()
335            }
336            DexType::Bonk => protocol_params.as_any().downcast_ref::<BonkParams>().is_some(),
337            DexType::RaydiumCpmm => {
338                protocol_params.as_any().downcast_ref::<RaydiumCpmmParams>().is_some()
339            }
340            DexType::RaydiumAmmV4 => {
341                protocol_params.as_any().downcast_ref::<RaydiumAmmV4Params>().is_some()
342            }
343            DexType::MeteoraDammV2 => {
344                protocol_params.as_any().downcast_ref::<MeteoraDammV2Params>().is_some()
345            }
346        };
347
348        if !is_valid_params {
349            return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
350        }
351
352        executor.swap(buy_params).await
353    }
354
355    /// Execute a sell order for a specified token
356    ///
357    /// # Arguments
358    ///
359    /// * `params` - Sell trade parameters containing all necessary trading configuration
360    ///
361    /// # Returns
362    ///
363    /// Returns `Ok(Signature)` with the transaction signature if the sell order is successfully executed,
364    /// or an error if the transaction fails.
365    ///
366    /// # Errors
367    ///
368    /// This function will return an error if:
369    /// - Invalid protocol parameters are provided for the specified DEX type
370    /// - The transaction fails to execute
371    /// - Network or RPC errors occur
372    /// - Insufficient token balance for the sale
373    /// - Token account doesn't exist or is not properly initialized
374    /// - Required accounts cannot be created or accessed
375    pub async fn sell(&self, params: TradeSellParams) -> Result<(bool, Signature), anyhow::Error> {
376        if params.slippage_basis_points.is_none() {
377            println!(
378                "slippage_basis_points is none, use default slippage basis points: {}",
379                DEFAULT_SLIPPAGE
380            );
381        }
382        if params.output_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk {
383            return Err(anyhow::anyhow!(
384                " Current version only support USD1 trading on Bonk protocols"
385            ));
386        }
387        let executor = TradeFactory::create_executor(params.dex_type.clone());
388        let protocol_params = params.extension_params;
389        let output_token_mint = if params.output_token_type == TradeTokenType::SOL {
390            SOL_TOKEN_ACCOUNT
391        } else if params.output_token_type == TradeTokenType::WSOL {
392            WSOL_TOKEN_ACCOUNT
393        } else if params.output_token_type == TradeTokenType::USDC {
394            USDC_TOKEN_ACCOUNT
395        } else {
396            USD1_TOKEN_ACCOUNT
397        };
398        let sell_params = SwapParams {
399            rpc: Some(self.rpc.clone()),
400            payer: self.payer.clone(),
401            trade_type: TradeType::Sell,
402            input_mint: params.mint,
403            output_mint: output_token_mint,
404            input_token_program: None,
405            output_token_program: None,
406            input_amount: Some(params.input_token_amount),
407            slippage_basis_points: params.slippage_basis_points,
408            address_lookup_table_account: params.address_lookup_table_account,
409            recent_blockhash: params.recent_blockhash,
410            wait_transaction_confirmed: params.wait_transaction_confirmed,
411            protocol_params: protocol_params.clone(),
412            with_tip: params.with_tip,
413            open_seed_optimize: params.open_seed_optimize,
414            swqos_clients: self.swqos_clients.clone(),
415            middleware_manager: self.middleware_manager.clone(),
416            durable_nonce: params.durable_nonce,
417            data_size_limit: 0,
418            create_input_mint_ata: false,
419            close_input_mint_ata: params.close_mint_token_ata,
420            create_output_mint_ata: params.create_output_token_ata,
421            close_output_mint_ata: params.close_output_token_ata,
422            fixed_output_amount: params.fixed_output_token_amount,
423            gas_fee_strategy: params.gas_fee_strategy,
424            simulate: params.simulate,
425        };
426
427        // Validate protocol params
428        let is_valid_params = match params.dex_type {
429            DexType::PumpFun => protocol_params.as_any().downcast_ref::<PumpFunParams>().is_some(),
430            DexType::PumpSwap => {
431                protocol_params.as_any().downcast_ref::<PumpSwapParams>().is_some()
432            }
433            DexType::Bonk => protocol_params.as_any().downcast_ref::<BonkParams>().is_some(),
434            DexType::RaydiumCpmm => {
435                protocol_params.as_any().downcast_ref::<RaydiumCpmmParams>().is_some()
436            }
437            DexType::RaydiumAmmV4 => {
438                protocol_params.as_any().downcast_ref::<RaydiumAmmV4Params>().is_some()
439            }
440            DexType::MeteoraDammV2 => {
441                protocol_params.as_any().downcast_ref::<MeteoraDammV2Params>().is_some()
442            }
443        };
444
445        if !is_valid_params {
446            return Err(anyhow::anyhow!("Invalid protocol params for Trade"));
447        }
448
449        // Execute sell based on tip preference
450        executor.swap(sell_params).await
451    }
452
453    /// Execute a sell order for a percentage of the specified token amount
454    ///
455    /// This is a convenience function that calculates the exact amount to sell based on
456    /// a percentage of the total token amount and then calls the `sell` function.
457    ///
458    /// # Arguments
459    ///
460    /// * `params` - Sell trade parameters (will be modified with calculated token amount)
461    /// * `amount_token` - Total amount of tokens available (in smallest token units)
462    /// * `percent` - Percentage of tokens to sell (1-100, where 100 = 100%)
463    ///
464    /// # Returns
465    ///
466    /// Returns `Ok(Signature)` with the transaction signature if the sell order is successfully executed,
467    /// or an error if the transaction fails.
468    ///
469    /// # Errors
470    ///
471    /// This function will return an error if:
472    /// - `percent` is 0 or greater than 100
473    /// - Invalid protocol parameters are provided for the specified DEX type
474    /// - The transaction fails to execute
475    /// - Network or RPC errors occur
476    /// - Insufficient token balance for the calculated sale amount
477    /// - Token account doesn't exist or is not properly initialized
478    /// - Required accounts cannot be created or accessed
479    pub async fn sell_by_percent(
480        &self,
481        mut params: TradeSellParams,
482        amount_token: u64,
483        percent: u64,
484    ) -> Result<(bool, Signature), anyhow::Error> {
485        if percent == 0 || percent > 100 {
486            return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
487        }
488        let amount = amount_token * percent / 100;
489        params.input_token_amount = amount;
490        self.sell(params).await
491    }
492
493    /// Wraps native SOL into wSOL (Wrapped SOL) for use in SPL token operations
494    ///
495    /// This function creates a wSOL associated token account (if it doesn't exist),
496    /// transfers the specified amount of SOL to that account, and then syncs the native
497    /// token balance to make SOL usable as an SPL token in trading operations.
498    ///
499    /// # Arguments
500    /// * `amount` - The amount of SOL to wrap (in lamports)
501    ///
502    /// # Returns
503    /// * `Ok(String)` - Transaction signature if successful
504    /// * `Err(anyhow::Error)` - If the transaction fails to execute
505    ///
506    /// # Errors
507    ///
508    /// This function will return an error if:
509    /// - Insufficient SOL balance for the wrap operation
510    /// - wSOL associated token account creation fails
511    /// - Transaction fails to execute or confirm
512    /// - Network or RPC errors occur
513    pub async fn wrap_sol_to_wsol(&self, amount: u64) -> Result<String, anyhow::Error> {
514        use crate::trading::common::wsol_manager::handle_wsol;
515        use solana_sdk::transaction::Transaction;
516        let recent_blockhash = self.rpc.get_latest_blockhash().await?;
517        let instructions = handle_wsol(&self.payer.pubkey(), amount);
518        let mut transaction =
519            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
520        transaction.sign(&[&*self.payer], recent_blockhash);
521        let signature = self.rpc.send_and_confirm_transaction(&transaction).await?;
522        Ok(signature.to_string())
523    }
524    /// Closes the wSOL associated token account and unwraps remaining balance to native SOL
525    ///
526    /// This function closes the wSOL associated token account, which automatically
527    /// transfers any remaining wSOL balance back to the account owner as native SOL.
528    /// This is useful for cleaning up wSOL accounts and recovering wrapped SOL after trading operations.
529    ///
530    /// # Returns
531    /// * `Ok(String)` - Transaction signature if successful
532    /// * `Err(anyhow::Error)` - If the transaction fails to execute
533    ///
534    /// # Errors
535    ///
536    /// This function will return an error if:
537    /// - wSOL associated token account doesn't exist
538    /// - Account closure fails due to insufficient permissions
539    /// - Transaction fails to execute or confirm
540    /// - Network or RPC errors occur
541    pub async fn close_wsol(&self) -> Result<String, anyhow::Error> {
542        use crate::trading::common::wsol_manager::close_wsol;
543        use solana_sdk::transaction::Transaction;
544        let recent_blockhash = self.rpc.get_latest_blockhash().await?;
545        let instructions = close_wsol(&self.payer.pubkey());
546        let mut transaction =
547            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
548        transaction.sign(&[&*self.payer], recent_blockhash);
549        let signature = self.rpc.send_and_confirm_transaction(&transaction).await?;
550        Ok(signature.to_string())
551    }
552}