From c3630e4c9945cddec11c4df1506026e506033362 Mon Sep 17 00:00:00 2001 From: ysq Date: Mon, 24 Nov 2025 22:52:56 +0800 Subject: [PATCH] feat: upgrade to v3.3.4 with improved error handling and transaction details - Add TradeError struct with code, message, and instruction fields - Enhance poll_transaction_confirmation to extract detailed error info from logs - Change buy/sell return type from Option to Option - Add solana-transaction-status-client-types dependency - Remove unused parallel.rs module --- Cargo.toml | 3 +- README.md | 4 +- README_CN.md | 4 +- src/lib.rs | 40 ++++-- src/swqos/common.rs | 120 ++++++++++++++--- src/trading/core/parallel.rs | 255 ----------------------------------- 6 files changed, 138 insertions(+), 288 deletions(-) delete mode 100755 src/trading/core/parallel.rs diff --git a/Cargo.toml b/Cargo.toml index b7766ab..54388f4 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sol-trade-sdk" -version = "3.3.3" +version = "3.3.4" edition = "2021" authors = [ "William ", @@ -55,6 +55,7 @@ solana-nonce = "3.0.0" solana-address-lookup-table-interface = "3.0.0" solana-compute-budget-interface = "3.0.0" solana-commitment-config = { version = "3.0.0", features = ["serde"] } +solana-transaction-status-client-types = "3.0.0" borsh = { version = "1.5.3", features = ["derive"] } isahc = "1.7.2" diff --git a/README.md b/README.md index 03da86a..0b56130 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.3" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.3.4" } ``` ### Use crates.io ```toml # Add to your Cargo.toml -sol-trade-sdk = "3.3.3" +sol-trade-sdk = "3.3.4" ``` ## 🛠️ Usage Examples diff --git a/README_CN.md b/README_CN.md index be8fb6b..126a276 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.3" } +sol-trade-sdk = { path = "./sol-trade-sdk", version = "3.3.4" } ``` ### 使用 crates.io ```toml # 添加到您的 Cargo.toml -sol-trade-sdk = "3.3.3" +sol-trade-sdk = "3.3.4" ``` ## 🛠️ 使用示例 diff --git a/src/lib.rs b/src/lib.rs index 7a2b9b1..2276dc1 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,8 +11,9 @@ use crate::common::TradeConfig; use crate::constants::trade::trade::DEFAULT_SLIPPAGE; use crate::constants::SOL_TOKEN_ACCOUNT; use crate::constants::USD1_TOKEN_ACCOUNT; -use crate::constants::WSOL_TOKEN_ACCOUNT; use crate::constants::USDC_TOKEN_ACCOUNT; +use crate::constants::WSOL_TOKEN_ACCOUNT; +use crate::swqos::common::TradeError; use crate::swqos::SwqosClient; use crate::swqos::SwqosConfig; use crate::swqos::TradeType; @@ -345,7 +346,10 @@ impl SolanaTrade { /// - Network or RPC errors occur /// - Insufficient SOL balance for the purchase /// - Required accounts cannot be created or accessed - pub async fn buy(&self, params: TradeBuyParams) -> Result<(bool, Signature, Option), anyhow::Error> { + pub async fn buy( + &self, + params: TradeBuyParams, + ) -> Result<(bool, Signature, Option), anyhow::Error> { if params.slippage_basis_points.is_none() { println!( "slippage_basis_points is none, use default slippage basis points: {}", @@ -380,7 +384,9 @@ impl SolanaTrade { slippage_basis_points: params.slippage_basis_points, address_lookup_table_account: params.address_lookup_table_account, recent_blockhash: params.recent_blockhash, - data_size_limit: params.gas_fee_strategy.get_strategies(TradeType::Buy) + data_size_limit: params + .gas_fee_strategy + .get_strategies(TradeType::Buy) .get(0) .map(|(_, _, v)| v.data_size_limit) .unwrap_or(256 * 1024), @@ -422,7 +428,10 @@ impl SolanaTrade { return Err(anyhow::anyhow!("Invalid protocol params for Trade")); } - executor.swap(buy_params).await + let swap_result = executor.swap(buy_params).await; + let result = + swap_result.map(|(success, sig, err)| (success, sig, err.map(TradeError::from))); + return result; } /// Execute a sell order for a specified token @@ -445,7 +454,10 @@ impl SolanaTrade { /// - Insufficient token balance for the sale /// - Token account doesn't exist or is not properly initialized /// - Required accounts cannot be created or accessed - pub async fn sell(&self, params: TradeSellParams) -> Result<(bool, Signature, Option), anyhow::Error> { + pub async fn sell( + &self, + params: TradeSellParams, + ) -> Result<(bool, Signature, Option), anyhow::Error> { if params.slippage_basis_points.is_none() { println!( "slippage_basis_points is none, use default slippage basis points: {}", @@ -487,7 +499,9 @@ impl SolanaTrade { swqos_clients: self.swqos_clients.clone(), middleware_manager: self.middleware_manager.clone(), durable_nonce: params.durable_nonce, - data_size_limit: params.gas_fee_strategy.get_strategies(TradeType::Sell) + data_size_limit: params + .gas_fee_strategy + .get_strategies(TradeType::Sell) .get(0) .map(|(_, _, v)| v.data_size_limit) .unwrap_or(0), @@ -523,7 +537,10 @@ impl SolanaTrade { } // Execute sell based on tip preference - executor.swap(sell_params).await + let swap_result = executor.swap(sell_params).await; + let result = + swap_result.map(|(success, sig, err)| (success, sig, err.map(TradeError::from))); + return result; } /// Execute a sell order for a percentage of the specified token amount @@ -557,7 +574,7 @@ impl SolanaTrade { mut params: TradeSellParams, amount_token: u64, percent: u64, - ) -> Result<(bool, Signature, Option), anyhow::Error> { + ) -> Result<(bool, Signature, Option), anyhow::Error> { if percent == 0 || percent > 100 { return Err(anyhow::anyhow!("Percentage must be between 1 and 100")); } @@ -652,9 +669,7 @@ impl SolanaTrade { // If instructions are empty, ATA already exists if instructions.is_empty() { - return Err(anyhow::anyhow!( - "wSOL ATA already exists or no instructions needed" - )); + return Err(anyhow::anyhow!("wSOL ATA already exists or no instructions needed")); } let mut transaction = @@ -694,7 +709,8 @@ impl SolanaTrade { let recent_blockhash = self.rpc.get_latest_blockhash().await?; let instructions = wrap_wsol_to_sol_internal(&self.payer.pubkey(), amount)?; - let mut transaction = Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); transaction.sign(&[&*self.payer], recent_blockhash); let signature = self.rpc.send_and_confirm_transaction(&transaction).await?; Ok(signature.to_string()) diff --git a/src/swqos/common.rs b/src/swqos/common.rs index 882bf79..30a7e88 100755 --- a/src/swqos/common.rs +++ b/src/swqos/common.rs @@ -1,18 +1,44 @@ +use crate::common::types::SolanaRpcClient; +use anyhow::Result; +use base64::engine::general_purpose::{self, STANDARD}; +use base64::Engine; use bincode::serialize; +use reqwest::Client; +use serde_json; use serde_json::json; use solana_client::rpc_client::SerializableTransaction; +use solana_client::rpc_config::RpcTransactionConfig; use solana_sdk::signature::Signature; -use solana_sdk::transaction::Transaction; +use solana_sdk::transaction::VersionedTransaction; +use solana_sdk::transaction::{Transaction, TransactionError}; use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding}; use std::str::FromStr; use std::time::{Duration, Instant}; use tokio::time::sleep; -use crate::common::types::SolanaRpcClient; -use anyhow::Result; -use base64::Engine; -use base64::engine::general_purpose::{self, STANDARD}; -use reqwest::Client; -use solana_sdk::transaction::VersionedTransaction; + +#[derive(Debug, Clone)] +pub struct TradeError { + pub code: u32, + pub message: String, + pub instruction: Option, +} + +impl std::fmt::Display for TradeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +impl std::error::Error for TradeError {} + +impl From for TradeError { + fn from(e: anyhow::Error) -> Self { + if let Some(te) = e.downcast_ref::() { + return te.clone(); + } + TradeError { code: 500, message: format!("{}", e), instruction: None } + } +} // 使用高性能序列化 @@ -27,8 +53,11 @@ impl FormatBase64VersionedTransaction for VersionedTransaction { } } -pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signature) -> Result { - let timeout: Duration = Duration::from_secs(15); // 🔧 增加到15秒,避免网络拥堵时超时 +pub async fn poll_transaction_confirmation( + rpc: &SolanaRpcClient, + txt_sig: Signature, +) -> Result { + let timeout: Duration = Duration::from_secs(15); // 🔧 增加到15秒,避免网络拥堵时超时 let interval: Duration = Duration::from_millis(1000); let start: Instant = Instant::now(); @@ -38,21 +67,80 @@ pub async fn poll_transaction_confirmation(rpc: &SolanaRpcClient, txt_sig: Signa } let status = rpc.get_signature_statuses(&[txt_sig]).await?; - match status.value[0].clone() { Some(status) => { if status.err.is_none() - && (status.confirmation_status == Some(TransactionConfirmationStatus::Confirmed) - || status.confirmation_status == Some(TransactionConfirmationStatus::Finalized)) + && (status.confirmation_status + == Some(TransactionConfirmationStatus::Confirmed) + || status.confirmation_status + == Some(TransactionConfirmationStatus::Finalized)) { return Ok(txt_sig); } - if status.err.is_some() { - return Err(anyhow::anyhow!(status.err.unwrap())); - } } - None => { + None => {} + } + + let tx_details = match rpc + .get_transaction_with_config( + &txt_sig, + RpcTransactionConfig { + encoding: Some(UiTransactionEncoding::JsonParsed), + max_supported_transaction_version: Some(0), + commitment: Some(solana_commitment_config::CommitmentConfig::confirmed()), + }, + ) + .await + { + Ok(details) => details, + Err(_) => { + // 交易可能还未上链,继续等待 sleep(interval).await; + continue; + } + }; + + let meta = tx_details.transaction.meta; + if meta.is_none() { + sleep(interval).await; + } else { + let meta = meta.unwrap(); + if meta.err.is_none() { + return Ok(txt_sig); + } else { + // 从 log_messages 中提取错误信息 + let mut error_msg = String::new(); + if let solana_transaction_status::option_serializer::OptionSerializer::Some(logs) = + &meta.log_messages + { + for log in logs { + if let Some(idx) = log.find("Error Message: ") { + error_msg = log[idx + 15..].trim_end_matches('.').to_string(); + break; + } + } + } + + let ui_err = meta.err.unwrap(); + let tx_err: TransactionError = + serde_json::from_value(serde_json::to_value(&ui_err)?)?; + let mut code = 500; + let mut index = None; + match &tx_err { + TransactionError::InstructionError(i, i_error) => { + match i_error { + solana_sdk::instruction::InstructionError::Custom(c) => code = *c, + _ => {} + } + index = Some(*i); + } + _ => {} + } + return Err(anyhow::Error::new(TradeError { + code: code, + message: format!("{} {:?}", tx_err, error_msg), + instruction: index, + })); } } } diff --git a/src/trading/core/parallel.rs b/src/trading/core/parallel.rs deleted file mode 100755 index a6d3012..0000000 --- a/src/trading/core/parallel.rs +++ /dev/null @@ -1,255 +0,0 @@ -use anyhow::{anyhow, Result}; -use solana_hash::Hash; -use solana_sdk::{ - instruction::Instruction, pubkey::Pubkey, signature::Keypair, signature::Signature, -}; -use std::{str::FromStr, sync::Arc, time::Instant}; -use tokio::sync::mpsc; -use tokio::task::JoinHandle; -use log::{info, debug}; - -use crate::{ - common::nonce_cache::DurableNonceInfo, - common::{GasFeeStrategy, SolanaRpcClient}, - swqos::{SwqosClient, SwqosType, TradeType}, - trading::{common::build_transaction, MiddlewareManager, SwapParams}, -}; - -pub async fn buy_parallel_execute( - params: SwapParams, - instructions: Vec, - protocol_name: &'static str, -) -> Result<(bool, Signature)> { - parallel_execute( - params.swqos_clients, - params.payer, - params.rpc, - instructions, - params.lookup_table_key, - params.recent_blockhash, - params.durable_nonce, - params.data_size_limit, - params.middleware_manager, - protocol_name, - true, - params.wait_transaction_confirmed, - true, - ) - .await -} - -pub async fn sell_parallel_execute( - params: SwapParams, - instructions: Vec, - protocol_name: &'static str, -) -> Result<(bool, Signature)> { - parallel_execute( - params.swqos_clients, - params.payer, - params.rpc, - instructions, - params.lookup_table_key, - params.recent_blockhash, - params.durable_nonce, - 0, - params.middleware_manager, - protocol_name, - false, - params.wait_transaction_confirmed, - params.with_tip, - ) - .await -} - -/// Generic function for parallel transaction execution -async fn parallel_execute( - swqos_clients: Vec>, - payer: Arc, - rpc: Option>, - instructions: Vec, - lookup_table_key: Option, - recent_blockhash: Option, - durable_nonce: Option, - data_size_limit: u32, - middleware_manager: Option>, - protocol_name: &'static str, - is_buy: bool, - wait_transaction_confirmed: bool, - with_tip: bool, -) -> Result<(bool, 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.")); - } - // 🚀 获取 CPU 核心并优化亲和性分配 - let cores = core_affinity::get_core_ids().unwrap(); - let _num_cores = cores.len(); - let mut handles: Vec)>>> = - Vec::with_capacity(swqos_clients.len()); - - let instructions = Arc::new(instructions); - - // 预先计算所有有效的组合 - 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::get_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. Please configure GasFeeStrategy for specific SwqosType.")); - } - - 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 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 rpc = rpc.clone(); - let durable_nonce = durable_nonce.clone(); - - let handle = tokio::spawn(async move { - core_affinity::set_for_current(core_id); - - let mut start = Instant::now(); - - let tip_amount = if with_tip { tip } else { 0.0 }; - - let transaction = build_transaction( - payer, - rpc, - unit_limit, - unit_price, - instructions.as_ref().clone(), - lookup_table_key, - recent_blockhash, - data_size_limit, - middleware_manager, - protocol_name, - is_buy, - swqos_type != SwqosType::Default, - &tip_account, - tip_amount, - durable_nonce, - // current_nonce, - ) - .await?; - - debug!( - "[{:?}] - [{:?}] - Building transaction instructions: {:?}", - swqos_type, - gas_fee_strategy_config.1, - start.elapsed() - ); - - start = Instant::now(); - - let mut err = None; - - let success = match swqos_client - .send_transaction( - if is_buy { TradeType::Buy } else { TradeType::Sell }, - &transaction, - ) - .await - { - Ok(()) => true, - Err(e) => { - err = Some(e); - false - } - }; - - debug!( - "[{:?}] - [{:?}] - Submitting transaction instructions: {:?}", - swqos_type, - gas_fee_strategy_config.1, - start.elapsed() - ); - - if let Some(signature) = transaction.signatures.first() { - return Ok((success, signature.clone(), err)); - } else { - return Err(anyhow!("Transaction has no signatures")); - } - }); - - handles.push(handle); - } - // Return as soon as any one succeeds - let (tx, mut rx) = mpsc::channel(handles.len()); - - // Start monitoring tasks - for handle in handles { - let tx = tx.clone(); - tokio::spawn(async move { - let result = handle.await; - let _ = tx.send(result).await; - }); - } - drop(tx); // Close the sender - - // Wait for the first successful result - let mut errors = Vec::new(); - - if !wait_transaction_confirmed { - if let Some(result) = rx.recv().await { - match result { - Ok(Ok((success, sig, _))) => return Ok((success, sig)), - Ok(Err(e)) => errors.push(format!("Task error: {}", e)), - Err(e) => errors.push(format!("Join error: {}", e)), - } - } - return Err(anyhow!("No transaction signature available")); - } - - let mut last_signature = None; - - while let Some(result) = rx.recv().await { - match result { - Ok(Ok((success, sig, err))) => { - if success { - return Ok((success, sig)); - } - if let Some(err) = err { - errors.push(format!("Task error: {}", err)); - } - last_signature = Some(sig); - } - Ok(Err(e)) => errors.push(format!("Task error: {}", e)), - Err(e) => errors.push(format!("Join error: {}", e)), - } - } - - info!("All transactions failed: {:?}", errors); - return Ok((false, last_signature.unwrap())); -}