From fda211ea8743a8868bb4daf9ebc036acb2511a4d Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Mon, 4 May 2026 13:46:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20extract=20TradingClient=20fa=C3=A7a?= =?UTF-8?q?de=20to=20client/mod.rs=20refactor:=20split=20trading/core/para?= =?UTF-8?q?ms=20into=20per-protocol=20modules=20+=20dex=5Fswap=20build:=20?= =?UTF-8?q?crate-type=20rlib=20only=20(drop=20unused=20cdylib)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Cargo.toml | 2 +- src/client/mod.rs | 1222 +++++++++++++++++++ src/lib.rs | 1226 +------------------- src/trading/core/params.rs | 898 -------------- src/trading/core/params/bonk.rs | 178 +++ src/trading/core/params/dex_swap.rs | 118 ++ src/trading/core/params/meteora_damm_v2.rs | 67 ++ src/trading/core/params/mod.rs | 17 + src/trading/core/params/pumpfun.rs | 240 ++++ src/trading/core/params/pumpswap.rs | 220 ++++ src/trading/core/params/raydium_amm_v4.rs | 54 + src/trading/core/params/raydium_cpmm.rs | 89 ++ 12 files changed, 2212 insertions(+), 2119 deletions(-) create mode 100644 src/client/mod.rs delete mode 100755 src/trading/core/params.rs create mode 100644 src/trading/core/params/bonk.rs create mode 100644 src/trading/core/params/dex_swap.rs create mode 100644 src/trading/core/params/meteora_damm_v2.rs create mode 100644 src/trading/core/params/mod.rs create mode 100644 src/trading/core/params/pumpfun.rs create mode 100644 src/trading/core/params/pumpswap.rs create mode 100644 src/trading/core/params/raydium_amm_v4.rs create mode 100644 src/trading/core/params/raydium_cpmm.rs diff --git a/Cargo.toml b/Cargo.toml index 470d734..fc9dad8 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ members = [ ] [lib] -crate-type = ["cdylib", "rlib"] +crate-type = ["rlib"] [features] default = [] diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..f11a5a1 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,1222 @@ +//! High-level [`TradingClient`], [`TradingInfrastructure`], and trade parameter types. + +use crate::common::nonce_cache::DurableNonceInfo; +use crate::common::sdk_log; +use crate::common::GasFeeStrategy; +use crate::common::{InfrastructureConfig, TradeConfig}; +#[cfg(feature = "perf-trace")] +use crate::constants::trade::trade::DEFAULT_SLIPPAGE; +use crate::constants::SOL_TOKEN_ACCOUNT; +use crate::constants::USD1_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; +use crate::trading::core::params::BonkParams; +use crate::trading::core::params::DexParamEnum; +use crate::trading::core::params::MeteoraDammV2Params; +use crate::trading::core::params::PumpFunParams; +use crate::trading::core::params::PumpSwapParams; +use crate::trading::core::params::RaydiumAmmV4Params; +use crate::trading::core::params::RaydiumCpmmParams; +use crate::trading::factory::DexType; +use crate::trading::MiddlewareManager; +use crate::trading::SwapParams; +use crate::trading::TradeFactory; +use crate::common::SolanaRpcClient; +use parking_lot::Mutex; +use rustls::crypto::{ring::default_provider, CryptoProvider}; +use solana_sdk::hash::Hash; +use solana_sdk::message::AddressLookupTableAccount; +use solana_sdk::signer::Signer; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature}; +use std::sync::Arc; +#[allow(unused_imports)] +use tracing::{debug, error, info, warn}; + +/// Single place to validate that protocol params match the given DEX type (avoids duplicate match in buy/sell). +#[inline(always)] +fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool { + match dex_type { + DexType::PumpFun => params.as_any().downcast_ref::().is_some(), + DexType::PumpSwap => params.as_any().downcast_ref::().is_some(), + DexType::Bonk => params.as_any().downcast_ref::().is_some(), + DexType::RaydiumCpmm => params.as_any().downcast_ref::().is_some(), + DexType::RaydiumAmmV4 => params.as_any().downcast_ref::().is_some(), + DexType::MeteoraDammV2 => params.as_any().downcast_ref::().is_some(), + } +} + +/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。 +/// +/// * `dex_type`:PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。 +pub async fn find_pool_by_mint( + rpc: &SolanaRpcClient, + mint: &Pubkey, + dex_type: DexType, +) -> Result { + match dex_type { + DexType::PumpSwap => crate::instruction::utils::pumpswap::find_pool(rpc, mint).await, + _ => Err(anyhow::anyhow!("find_pool_by_mint not implemented for {:?}", dex_type)), + } +} + +/// Type of the token to buy +#[derive(Clone, PartialEq)] +pub enum TradeTokenType { + SOL, + WSOL, + USD1, + USDC, +} + +/// Shared infrastructure components that can be reused across multiple wallets +/// +/// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients) +/// that are wallet-independent and can be shared when only the trading wallet changes. +pub struct TradingInfrastructure { + /// Shared RPC client for blockchain interactions + pub rpc: Arc, + /// Shared SWQOS clients for transaction priority and routing. Arc> so cloning into SwapParams is a single Arc clone. + pub swqos_clients: Arc>>, + /// Configuration used to create this infrastructure + pub config: InfrastructureConfig, + /// Precomputed at init: min(swqos_clients.len(), 2/3 * num_cores). Not computed on trade hot path. + pub max_sender_concurrency: usize, + /// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path. + pub effective_core_ids: Arc>, +} + +impl TradingInfrastructure { + /// Create new shared infrastructure from configuration + /// + /// This performs the expensive initialization: + /// - Creates RPC client with connection pool + /// - Creates SWQOS clients (each with their own HTTP client) + /// - Initializes rent cache and starts background updater + pub async fn new(config: InfrastructureConfig) -> Self { + // Install crypto provider (idempotent) + if CryptoProvider::get_default().is_none() { + let _ = default_provider() + .install_default() + .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); + } + + // Create RPC client + let rpc = Arc::new(SolanaRpcClient::new_with_commitment( + config.rpc_url.clone(), + config.commitment.clone(), + )); + + // Initialize rent cache (with timeout so slow RPC doesn't block forever) + const RENT_UPDATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); + match tokio::time::timeout(RENT_UPDATE_TIMEOUT, crate::common::seed::update_rents(&rpc)).await { + Ok(Ok(())) => {} + Ok(Err(e)) => { + if sdk_log::sdk_log_enabled() { + warn!(target: "sol_trade_sdk", "rent update failed: {}, using defaults", e); + } + crate::common::seed::set_default_rents(); + } + Err(_) => { + if sdk_log::sdk_log_enabled() { + warn!(target: "sol_trade_sdk", "rent update timed out ({}s), using defaults; check RPC", RENT_UPDATE_TIMEOUT.as_secs()); + } + crate::common::seed::set_default_rents(); + } + } + crate::common::seed::start_rent_updater(rpc.clone()); + + // Create SWQOS clients with blacklist checking(QUIC 握手可能较慢,单节点超时 15s) + const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); + let mut swqos_clients: Vec> = vec![]; + for swqos in &config.swqos_configs { + if swqos.is_blacklisted() { + if sdk_log::sdk_log_enabled() { + warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type()); + } + continue; + } + match tokio::time::timeout( + SWQOS_CLIENT_TIMEOUT, + SwqosConfig::get_swqos_client( + config.rpc_url.clone(), + config.commitment.clone(), + swqos.clone(), + config.mev_protection, + ), + ) + .await + { + Ok(Ok(swqos_client)) => swqos_clients.push(swqos_client), + Ok(Err(err)) => { + eprintln!( + "⚠️ SWQOS {:?} 初始化失败: {}(已从列表中排除)", + swqos.swqos_type(), + err + ); + if sdk_log::sdk_log_enabled() { + warn!( + target: "sol_trade_sdk", + "failed to create {:?} swqos client: {err}. Excluding from swqos list", + swqos.swqos_type() + ); + } + } + Err(_) => { + eprintln!( + "⚠️ SWQOS {:?} 初始化超时({}s),已跳过", + swqos.swqos_type(), + SWQOS_CLIENT_TIMEOUT.as_secs() + ); + if sdk_log::sdk_log_enabled() { + warn!( + target: "sol_trade_sdk", + "swqos {:?} init timed out ({}s), skipping", + swqos.swqos_type(), + SWQOS_CLIENT_TIMEOUT.as_secs() + ); + } + } + } + } + + // 若全部失败、被黑名单跳过或仅配置了不可用通道,至少保留一条 Rpc Default,否则 execute_parallel 会因 swqos_clients 为空直接报错。 + if swqos_clients.is_empty() { + eprintln!( + "⚠️ 无任何 SWQOS 客户端初始化成功,将回退为普通 RPC 发送: {}", + config.rpc_url + ); + if sdk_log::sdk_log_enabled() { + warn!( + target: "sol_trade_sdk", + "no SWQOS clients initialized; falling back to Rpc Default ({})", + config.rpc_url + ); + } + match SwqosConfig::get_swqos_client( + config.rpc_url.clone(), + config.commitment.clone(), + SwqosConfig::Default(config.rpc_url.clone()), + config.mev_protection, + ) + .await + { + Ok(c) => swqos_clients.push(c), + Err(e) => { + if sdk_log::sdk_log_enabled() { + warn!( + target: "sol_trade_sdk", + "fallback Rpc Default client failed: {}", + e + ); + } + } + } + } + + if !swqos_clients.is_empty() { + let labels: Vec<&str> = swqos_clients + .iter() + .map(|c| c.get_swqos_type().as_str()) + .collect(); + eprintln!( + "ℹ️ SWQOS 通道已就绪: {} 条 → [{}]", + swqos_clients.len(), + labels.join(", ") + ); + } + + let swqos_count = swqos_clients.len(); + let (max_sender_concurrency, effective_core_ids) = { + let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0); + let max_by_cores = (num_cores * 2 / 3).max(1); + let cap = swqos_count.min(max_by_cores).max(1); + let ids = core_affinity::get_core_ids() + .map(|all| { + let v: Vec<_> = all.into_iter().collect(); + let len = v.len(); + if config.swqos_cores_from_end && len >= cap { + v.into_iter().skip(len - cap).collect() + } else { + v.into_iter().take(cap).collect() + } + }) + .unwrap_or_default(); + (cap, Arc::new(ids)) + }; + + Self { + rpc, + swqos_clients: Arc::new(swqos_clients), + config, + max_sender_concurrency, + effective_core_ids, + } + } +} + +/// When using `TradeConfig::with_swqos_cores_from_end(true)`, returns the same "last N" core indices +/// that the infrastructure uses. Pass the result to `TradingClient::with_dedicated_sender_threads` +/// for 方式 C (组合使用): SWQOS on last N cores and dedicated sender threads pinned to those cores. +/// +/// Returns `None` if core count cannot be determined. `swqos_count` is typically `swqos_configs.len()`. +pub fn recommended_sender_thread_core_indices(swqos_count: usize) -> Option> { + let all = core_affinity::get_core_ids()?; + let num_cores = all.len(); + if num_cores == 0 { + return None; + } + let max_by_cores = (num_cores * 2 / 3).max(1); + let cap = swqos_count.min(max_by_cores).max(1).min(num_cores); + let start = num_cores.saturating_sub(cap); + Some((start..num_cores).collect()) +} + +/// Main trading client for Solana DeFi protocols +/// +/// `SolTradingSDK` 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 TradingClient { + /// The keypair used for signing all transactions + pub payer: Arc, + /// Shared infrastructure (RPC client, SWQOS clients) + /// Can be shared across multiple TradingClient instances with different wallets + pub infrastructure: Arc, + /// Optional middleware manager for custom transaction processing + pub middleware_manager: Option>, + /// Whether to use seed optimization for all ATA operations (default: true) + /// Applies to all token account creations across buy and sell operations + pub use_seed_optimize: bool, + /// Internal: use dedicated sender threads (default false). Set via with_dedicated_sender_threads() for advanced use. + pub use_dedicated_sender_threads: bool, + /// Internal: core indices for dedicated sender threads. Trimmed to ≤ max_sender_concurrency at set. + pub sender_thread_cores: Option>>, + /// Internal: precomputed at infra init (min(swqos_count, 2/3*cores)). Not user-configurable. + pub max_sender_concurrency: usize, + /// Internal: precomputed at infra init for job affinity. Not user-configurable. + pub effective_core_ids: Arc>, + /// Whether to output all SDK logs (from TradeConfig.log_enabled). + pub log_enabled: bool, + /// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency. + pub check_min_tip: bool, +} + +static INSTANCE: Mutex>> = Mutex::new(None); + +/// 🔄 向后兼容:SolanaTrade 别名 +pub type SolanaTrade = TradingClient; + +impl Clone for TradingClient { + fn clone(&self) -> Self { + Self { + payer: self.payer.clone(), + infrastructure: self.infrastructure.clone(), + middleware_manager: self.middleware_manager.clone(), + use_seed_optimize: self.use_seed_optimize, + use_dedicated_sender_threads: self.use_dedicated_sender_threads, + sender_thread_cores: self.sender_thread_cores.clone(), + max_sender_concurrency: self.max_sender_concurrency, + effective_core_ids: self.effective_core_ids.clone(), + log_enabled: self.log_enabled, + check_min_tip: self.check_min_tip, + } + } +} + +/// 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, + /// Type of the token to buy + pub input_token_type: TradeTokenType, + /// Public key of the token to purchase + pub mint: Pubkey, + /// Amount of tokens to buy (in smallest token units) + pub input_token_amount: u64, + /// Optional slippage tolerance in basis points (e.g., 100 = 1%) + pub slippage_basis_points: Option, + /// Recent blockhash for transaction validity + pub recent_blockhash: Option, + /// Protocol-specific parameters (PumpFun, Raydium, etc.) + pub extension_params: DexParamEnum, + // Extended configuration + /// Optional address lookup table for transaction size optimization + pub address_lookup_table_account: Option, + /// Whether to wait for transaction confirmation before returning + pub wait_tx_confirmed: bool, + /// Whether to create input token associated token account + pub create_input_token_ata: bool, + /// Whether to close input token associated token account after trade + pub close_input_token_ata: bool, + /// Whether to create token mint associated token account + pub create_mint_ata: bool, + /// Durable nonce information + pub durable_nonce: Option, + /// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated) + pub fixed_output_token_amount: Option, + /// Gas fee strategy + pub gas_fee_strategy: GasFeeStrategy, + /// Whether to simulate the transaction instead of executing it + pub simulate: bool, + /// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap). + /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens. + /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input. + /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes. + pub use_exact_sol_amount: Option, + /// 可选:事件收到时间(微秒,与 sol-parser-sdk 的 metadata.grpc_recv_us / clock::now_micros 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点,打印起点→提交耗时。 + pub grpc_recv_us: Option, +} + +/// 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, + /// Type of the token to sell + pub output_token_type: TradeTokenType, + /// Public key of the token to sell + pub mint: Pubkey, + /// Amount of tokens to sell (in smallest token units) + pub input_token_amount: u64, + /// Optional slippage tolerance in basis points (e.g., 100 = 1%) + pub slippage_basis_points: Option, + /// Recent blockhash for transaction validity + pub recent_blockhash: Option, + /// Whether to include tip for transaction priority + pub with_tip: bool, + /// Protocol-specific parameters (PumpFun, Raydium, etc.) + pub extension_params: DexParamEnum, + // Extended configuration + /// Optional address lookup table for transaction size optimization + pub address_lookup_table_account: Option, + /// Whether to wait for transaction confirmation before returning + pub wait_tx_confirmed: bool, + /// Whether to create output token associated token account + pub create_output_token_ata: bool, + /// Whether to close output token associated token account after trade + pub close_output_token_ata: bool, + /// Whether to close mint token associated token account after trade + pub close_mint_token_ata: bool, + /// Durable nonce information + pub durable_nonce: Option, + /// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated) + pub fixed_output_token_amount: Option, + /// Gas fee strategy + pub gas_fee_strategy: GasFeeStrategy, + /// Whether to simulate the transaction instead of executing it + pub simulate: bool, + /// 可选:事件收到时间(微秒,与 sol-parser-sdk clock 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点。 + pub grpc_recv_us: Option, +} + +impl TradingClient { + /// Create a TradingClient from shared infrastructure (fast path) + /// + /// This is the preferred method when multiple wallets share the same infrastructure. + /// It only performs wallet-specific initialization (fast_init) without the expensive + /// RPC/SWQOS client creation. + /// + /// # Arguments + /// * `payer` - The keypair used for signing transactions + /// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients) + /// * `use_seed_optimize` - Whether to use seed optimization for ATA operations + /// + /// # Returns + /// Returns a configured `TradingClient` instance ready for trading operations + pub fn from_infrastructure( + payer: Arc, + infrastructure: Arc, + use_seed_optimize: bool, + ) -> Self { + // Initialize wallet-specific caches (fast, synchronous) + crate::common::fast_fn::fast_init(&payer.pubkey()); + let max_sender_concurrency = infrastructure.max_sender_concurrency; + let effective_core_ids = infrastructure.effective_core_ids.clone(); + + Self { + payer, + infrastructure, + middleware_manager: None, + use_seed_optimize, + use_dedicated_sender_threads: false, + sender_thread_cores: None, + max_sender_concurrency, + effective_core_ids, + log_enabled: true, + check_min_tip: false, + } + } + + /// Create a TradingClient from shared infrastructure with optional WSOL ATA setup + /// + /// Same as `from_infrastructure` but also handles WSOL ATA creation if requested. + /// + /// # Arguments + /// * `payer` - The keypair used for signing transactions + /// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients) + /// * `use_seed_optimize` - Whether to use seed optimization for ATA operations + /// * `create_wsol_ata` - Whether to check/create WSOL ATA + pub async fn from_infrastructure_with_wsol_setup( + payer: Arc, + infrastructure: Arc, + use_seed_optimize: bool, + create_wsol_ata: bool, + ) -> Self { + crate::common::fast_fn::fast_init(&payer.pubkey()); + + if create_wsol_ata { + // 在后台异步创建 WSOL ATA,不阻塞启动 + let payer_clone = payer.clone(); + let rpc_clone = infrastructure.rpc.clone(); + tokio::spawn(async move { + Self::ensure_wsol_ata(&payer_clone, &rpc_clone).await; + }); + if sdk_log::sdk_log_enabled() { + info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA creation started in background, does not block bot startup"); + } + } + + let max_sender_concurrency = infrastructure.max_sender_concurrency; + let effective_core_ids = infrastructure.effective_core_ids.clone(); + + Self { + payer, + infrastructure, + middleware_manager: None, + use_seed_optimize, + use_dedicated_sender_threads: false, + sender_thread_cores: None, + max_sender_concurrency, + effective_core_ids, + log_enabled: true, + check_min_tip: false, + } + } + + /// 单次尝试创建 WSOL ATA:获取 blockhash、组交易、发送并确认。成功或账户已存在返回 Ok(()),否则返回 Err(错误信息)。 + async fn try_create_wsol_ata_once( + rpc: &SolanaRpcClient, + payer: &Arc, + wsol_ata: &solana_sdk::pubkey::Pubkey, + create_ata_ixs: &[solana_sdk::instruction::Instruction], + timeout_secs: u64, + ) -> Result<(), String> { + use solana_sdk::transaction::Transaction; + let recent_blockhash = rpc + .get_latest_blockhash() + .await + .map_err(|e| format!("Failed to get blockhash: {}", e))?; + let tx = Transaction::new_signed_with_payer( + create_ata_ixs, + Some(&payer.pubkey()), + &[payer.as_ref()], + recent_blockhash, + ); + let send_result = tokio::time::timeout( + tokio::time::Duration::from_secs(timeout_secs), + rpc.send_and_confirm_transaction(&tx), + ) + .await; + match send_result { + Ok(Ok(_signature)) => Ok(()), + Ok(Err(e)) => { + if rpc.get_account(wsol_ata).await.is_ok() { + return Ok(()); + } + Err(format!("{}", e)) + } + Err(_) => Err(format!("Transaction confirmation timeout ({}s)", timeout_secs)), + } + } + + /// 确保钱包存在 WSOL ATA;不存在则发交易创建(会花费租金 + 手续费,初始化阶段唯一会扣钱的逻辑) + async fn ensure_wsol_ata(payer: &Arc, rpc: &Arc) { + const MAX_RETRIES: usize = 3; + const TIMEOUT_SECS: u64 = 10; + + let wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &payer.pubkey(), + &WSOL_TOKEN_ACCOUNT, + &crate::constants::TOKEN_PROGRAM, + ); + + if rpc.get_account(&wsol_ata).await.is_ok() { + if sdk_log::sdk_log_enabled() { + info!(target: "sol_trade_sdk", "✅ WSOL ATA already exists: {}", wsol_ata); + } + return; + } + + let create_ata_ixs = crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey()); + if create_ata_ixs.is_empty() { + if sdk_log::sdk_log_enabled() { + info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA already exists (no need to create)"); + } + return; + } + + if sdk_log::sdk_log_enabled() { + info!(target: "sol_trade_sdk", "🔨 Creating WSOL ATA: {}", wsol_ata); + } + let mut last_error = None; + for attempt in 1..=MAX_RETRIES { + if attempt > 1 { + if sdk_log::sdk_log_enabled() { + info!(target: "sol_trade_sdk", "🔄 Retrying WSOL ATA creation (attempt {}/{})...", attempt, MAX_RETRIES); + } + tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; + } + match Self::try_create_wsol_ata_once( + rpc.as_ref(), + payer, + &wsol_ata, + &create_ata_ixs, + TIMEOUT_SECS, + ) + .await + { + Ok(()) => { + if sdk_log::sdk_log_enabled() { + info!(target: "sol_trade_sdk", "✅ WSOL ATA created or already exists"); + } + return; + } + Err(e) => { + last_error = Some(e.clone()); + if attempt < MAX_RETRIES && sdk_log::sdk_log_enabled() { + warn!(target: "sol_trade_sdk", "⚠️ Attempt {} failed: {}", attempt, e); + } + } + } + } + + if let Some(err) = last_error { + if sdk_log::sdk_log_enabled() { + error!(target: "sol_trade_sdk", "❌ WSOL ATA creation failed after {} retries: {}", MAX_RETRIES, wsol_ata); + error!(target: "sol_trade_sdk", " Error: {}", err); + error!(target: "sol_trade_sdk", " 💡 Possible causes: insufficient SOL, RPC timeout, or fee"); + error!(target: "sol_trade_sdk", " 🔧 Solutions: fund wallet (e.g. 0.1 SOL), retry, check RPC"); + } + std::thread::sleep(std::time::Duration::from_secs(5)); + panic!( + "❌ WSOL ATA creation failed and account does not exist: {}. Error: {}", + wsol_ata, err + ); + } + } + + /// Creates a new SolTradingSDK 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 + /// * `trade_config` - Trading configuration including RPC URL, SWQOS settings, etc. + /// + /// # Returns + /// Returns a configured `SolTradingSDK` instance ready for trading operations + #[inline] + pub async fn new(payer: Arc, trade_config: TradeConfig) -> Self { + // 设置 SDK 全局日志开关,后续所有 SDK 内日志(SWQOS/WSOL/耗时等)均受此控制 + sdk_log::set_sdk_log_enabled(trade_config.log_enabled); + // 预热高性能时钟,避免首笔交易时触发 3 次 Utc::now() 校准 + let _ = crate::common::clock::now_micros(); + // Create infrastructure from trade config + let infra_config = InfrastructureConfig::from_trade_config(&trade_config); + let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await); + + // Initialize wallet-specific caches + crate::common::fast_fn::fast_init(&payer.pubkey()); + + // ═══════════════════════════════════════════════════════════════════════════════ + // 初始化阶段会花费租金/手续费的唯一路径:创建 WSOL ATA(ensure_wsol_ata) + // - 触发条件:create_wsol_ata_on_startup == true 且钱包 SOL >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS + // - 花费:ATA 租金(约 0.00203928 SOL)+ 交易手续费;钱包不足时已跳过 + // - 其它初始化(TradingInfrastructure::new、update_rents、get_swqos_client)仅 RPC/HTTP,不发送交易 + // ═══════════════════════════════════════════════════════════════════════════════ + if trade_config.create_wsol_ata_on_startup { + const MIN_SOL_FOR_WSOL_ATA_LAMPORTS: u64 = 500_000; // 约 0.0005 SOL,用于 ATA 租金 + 手续费 + const BALANCE_CHECK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); + let balance = tokio::time::timeout( + BALANCE_CHECK_TIMEOUT, + infrastructure.rpc.get_balance(&payer.pubkey()), + ) + .await + .unwrap_or(Ok(0)) + .unwrap_or(0); + if balance >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS { + Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await; + } else if sdk_log::sdk_log_enabled() { + info!( + target: "sol_trade_sdk", + "⏭️ 跳过创建 WSOL ATA:钱包 SOL 不足(当前 {} lamports,需要至少 {})", + balance, + MIN_SOL_FOR_WSOL_ATA_LAMPORTS + ); + } + } + + // 并发/核心相关由 infrastructure 预计算,用户无需配置 + let instance = Self { + payer, + infrastructure: infrastructure.clone(), + middleware_manager: None, + use_seed_optimize: trade_config.use_seed_optimize, + use_dedicated_sender_threads: false, + sender_thread_cores: None, + max_sender_concurrency: infrastructure.max_sender_concurrency, + effective_core_ids: infrastructure.effective_core_ids.clone(), + log_enabled: trade_config.log_enabled, + check_min_tip: trade_config.check_min_tip, + }; + + let mut current = INSTANCE.lock(); + *current = Some(Arc::new(instance.clone())); + + 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 + } + + /// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores). + /// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs. + /// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores). + /// - `None`: keep default (shared tokio pool). + /// - `Some(vec![])`: dedicated threads with default count, no core pinning. + /// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap). + /// + /// **Latency note:** If a core is busy with other work (node, bot), SWQOS submit on that core can be delayed. + /// For lowest latency, pass core indices that are *reserved* for SWQOS (do not run other CPU-heavy work on those cores). + pub fn with_dedicated_sender_threads(mut self, core_indices: Option>) -> Self { + match core_indices { + None => { + self.use_dedicated_sender_threads = false; + self.sender_thread_cores = None; + } + Some(v) if v.is_empty() => { + self.use_dedicated_sender_threads = true; + self.sender_thread_cores = None; + } + Some(v) => { + self.use_dedicated_sender_threads = true; + let cap = v.len().min(self.max_sender_concurrency); + self.sender_thread_cores = Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v })); + } + } + self + } + + /// 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 { + &self.infrastructure.rpc + } + + /// 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 { + let instance = INSTANCE.lock(); + instance + .as_ref() + .expect("SolanaTrade instance not initialized. Please call new() first.") + .clone() + } + + /// Execute a buy order for a specified token + /// + /// 🔧 修复:返回Vec支持多SWQOS并发交易 + /// - bool: 是否至少有一个交易成功 + /// - Vec: 所有提交的交易签名(按SWQOS顺序) + /// - Option: 最后一个错误(如果全部失败) + /// + /// # Arguments + /// + /// * `params` - Buy trade parameters containing all necessary trading configuration + /// + /// # Returns + /// + /// Returns `Ok((bool, Vec, Option))` with success flag and all transaction signatures, + /// or an error if the transaction fails. + /// + /// # Errors + /// + /// This function will return an error if: + /// - 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 + /// - Required accounts cannot be created or accessed + #[inline] + pub async fn buy( + &self, + params: TradeBuyParams, + ) -> Result<(bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> { + if params.recent_blockhash.is_none() && params.durable_nonce.is_none() { + return Err(anyhow::anyhow!( + "Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)" + )); + } + #[cfg(feature = "perf-trace")] + if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() { + debug!( + target: "sol_trade_sdk", + "slippage_basis_points is none, use default slippage basis points: {}", + DEFAULT_SLIPPAGE + ); + } + if params.input_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk { + return Err(anyhow::anyhow!( + " Current version only supports USD1 trading on Bonk protocols" + )); + } + let protocol_params = params.extension_params; + if !validate_protocol_params(params.dex_type, &protocol_params) { + return Err(anyhow::anyhow!( + "Invalid protocol params for Trade (dex={:?})", + params.dex_type + )); + } + let input_token_mint = if params.input_token_type == TradeTokenType::SOL { + SOL_TOKEN_ACCOUNT + } else if params.input_token_type == TradeTokenType::WSOL { + WSOL_TOKEN_ACCOUNT + } else if params.input_token_type == TradeTokenType::USDC { + USDC_TOKEN_ACCOUNT + } else { + USD1_TOKEN_ACCOUNT + }; + let executor = TradeFactory::create_executor(params.dex_type); + let buy_params = SwapParams { + rpc: Some(self.infrastructure.rpc.clone()), + payer: self.payer.clone(), + trade_type: TradeType::Buy, + input_mint: input_token_mint, + output_mint: params.mint, + input_token_program: None, + output_token_program: None, + input_amount: Some(params.input_token_amount), + slippage_basis_points: params.slippage_basis_points, + address_lookup_table_account: params.address_lookup_table_account, + recent_blockhash: params.recent_blockhash, + wait_tx_confirmed: params.wait_tx_confirmed, + protocol_params, + open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置 + swqos_clients: self.infrastructure.swqos_clients.clone(), + middleware_manager: self.middleware_manager.clone(), + durable_nonce: params.durable_nonce, + with_tip: true, + create_input_mint_ata: params.create_input_token_ata, + close_input_mint_ata: params.close_input_token_ata, + create_output_mint_ata: params.create_mint_ata, + close_output_mint_ata: false, + fixed_output_amount: params.fixed_output_token_amount, + gas_fee_strategy: params.gas_fee_strategy, + simulate: params.simulate, + log_enabled: self.log_enabled, + use_dedicated_sender_threads: self.use_dedicated_sender_threads, + sender_thread_cores: self.sender_thread_cores.clone(), + max_sender_concurrency: self.max_sender_concurrency, + effective_core_ids: self.effective_core_ids.clone(), + check_min_tip: self.check_min_tip, + grpc_recv_us: params.grpc_recv_us, + use_exact_sol_amount: params.use_exact_sol_amount, + }; + + let swap_result = executor.swap(buy_params).await; + let result = + swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings)); + result + } + + /// Execute a sell order for a specified token + /// + /// 🔧 修复:返回Vec支持多SWQOS并发交易 + /// - bool: 是否至少有一个交易成功 + /// - Vec: 所有提交的交易签名(按SWQOS顺序) + /// - Option: 最后一个错误(如果全部失败) + /// + /// # Arguments + /// + /// * `params` - Sell trade parameters containing all necessary trading configuration + /// + /// # Returns + /// + /// Returns `Ok((bool, Vec, Option))` with success flag and all transaction signatures, + /// or an error if the transaction fails. + /// + /// # Errors + /// + /// This function will return an error if: + /// - 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 + /// - Required accounts cannot be created or accessed + #[inline] + pub async fn sell( + &self, + params: TradeSellParams, + ) -> Result<(bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> { + #[cfg(feature = "perf-trace")] + if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() { + debug!( + target: "sol_trade_sdk", + "slippage_basis_points is none, use default slippage basis points: {}", + DEFAULT_SLIPPAGE + ); + } + if params.recent_blockhash.is_none() && params.durable_nonce.is_none() { + return Err(anyhow::anyhow!( + "Must provide either recent_blockhash or durable_nonce for sell (required for transaction validity)" + )); + } + if params.output_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk { + return Err(anyhow::anyhow!( + " Current version only supports USD1 trading on Bonk protocols" + )); + } + let protocol_params = params.extension_params; + if !validate_protocol_params(params.dex_type, &protocol_params) { + return Err(anyhow::anyhow!( + "Invalid protocol params for Trade (dex={:?})", + params.dex_type + )); + } + let executor = TradeFactory::create_executor(params.dex_type); + let output_token_mint = if params.output_token_type == TradeTokenType::SOL { + SOL_TOKEN_ACCOUNT + } else if params.output_token_type == TradeTokenType::WSOL { + WSOL_TOKEN_ACCOUNT + } else if params.output_token_type == TradeTokenType::USDC { + USDC_TOKEN_ACCOUNT + } else { + USD1_TOKEN_ACCOUNT + }; + let sell_params = SwapParams { + rpc: Some(self.infrastructure.rpc.clone()), + payer: self.payer.clone(), + trade_type: TradeType::Sell, + input_mint: params.mint, + output_mint: output_token_mint, + input_token_program: None, + output_token_program: None, + input_amount: Some(params.input_token_amount), + slippage_basis_points: params.slippage_basis_points, + address_lookup_table_account: params.address_lookup_table_account, + recent_blockhash: params.recent_blockhash, + wait_tx_confirmed: params.wait_tx_confirmed, + protocol_params, + with_tip: params.with_tip, + open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置 + swqos_clients: self.infrastructure.swqos_clients.clone(), + middleware_manager: self.middleware_manager.clone(), + durable_nonce: params.durable_nonce, + create_input_mint_ata: false, + close_input_mint_ata: params.close_mint_token_ata, + create_output_mint_ata: params.create_output_token_ata, + close_output_mint_ata: params.close_output_token_ata, + fixed_output_amount: params.fixed_output_token_amount, + gas_fee_strategy: params.gas_fee_strategy, + simulate: params.simulate, + log_enabled: self.log_enabled, + use_dedicated_sender_threads: self.use_dedicated_sender_threads, + sender_thread_cores: self.sender_thread_cores.clone(), + max_sender_concurrency: self.max_sender_concurrency, + effective_core_ids: self.effective_core_ids.clone(), + check_min_tip: self.check_min_tip, + grpc_recv_us: params.grpc_recv_us, + use_exact_sol_amount: None, + }; + + let swap_result = executor.swap(sell_params).await; + let result = + swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings)); + result + } + + /// Execute a sell order for a percentage of the specified token amount + /// + /// This is a convenience function that calculates the exact amount to sell based on + /// a percentage of the total token amount and then calls the `sell` function. + /// + /// # Arguments + /// + /// * `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%) + /// + /// # Returns + /// + /// 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 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, + mut params: TradeSellParams, + amount_token: u64, + percent: u64, + ) -> Result<(bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> { + if percent == 0 || percent > 100 { + return Err(anyhow::anyhow!("Percentage must be between 1 and 100")); + } + let amount = amount_token * percent / 100; + params.input_token_amount = amount; + self.sell(params).await + } + + /// 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 in trading operations. + /// + /// # Arguments + /// * `amount` - The amount of SOL to wrap (in lamports) + /// + /// # Returns + /// * `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 { + use crate::trading::common::wsol_manager::handle_wsol; + use solana_sdk::transaction::Transaction; + let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; + let instructions = handle_wsol(&self.payer.pubkey(), amount); + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); + transaction.sign(&[&*self.payer], recent_blockhash); + let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; + Ok(signature.to_string()) + } + /// 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 after trading operations. + /// + /// # Returns + /// * `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 { + use crate::trading::common::wsol_manager::close_wsol; + use solana_sdk::transaction::Transaction; + let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; + let instructions = close_wsol(&self.payer.pubkey()); + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); + transaction.sign(&[&*self.payer], recent_blockhash); + let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; + Ok(signature.to_string()) + } + + /// Creates a wSOL associated token account (ATA) without wrapping any SOL + /// + /// This function only creates the wSOL associated token account for the payer + /// without transferring any SOL into it. This is useful when you want to set up + /// the account infrastructure in advance without committing funds yet. + /// + /// # Returns + /// * `Ok(String)` - Transaction signature if successful + /// * `Err(anyhow::Error)` - If the transaction fails to execute + /// + /// # Errors + /// + /// This function will return an error if: + /// - wSOL ATA account already exists (idempotent, will succeed silently) + /// - Transaction fails to execute or confirm + /// - Network or RPC errors occur + /// - Insufficient SOL for transaction fees + pub async fn create_wsol_ata(&self) -> Result { + use crate::trading::common::wsol_manager::create_wsol_ata; + use solana_sdk::transaction::Transaction; + + let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; + let instructions = create_wsol_ata(&self.payer.pubkey()); + + // If instructions are empty, ATA already exists + if instructions.is_empty() { + return Err(anyhow::anyhow!("wSOL ATA already exists or no instructions needed")); + } + + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); + transaction.sign(&[&*self.payer], recent_blockhash); + let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; + Ok(signature.to_string()) + } + + /// 将 WSOL 转换为 SOL,使用 seed 账户 + /// + /// 这个函数实现以下步骤: + /// 1. 使用 super::seed::create_associated_token_account_use_seed 创建 WSOL seed 账号 + /// 2. 使用 get_associated_token_address_with_program_id_use_seed 获取该账号的 ATA 地址 + /// 3. 添加从用户 WSOL ATA 转账到该 seed ATA 账号的指令 + /// 4. 添加关闭 WSOL seed 账号的指令 + /// + /// # Arguments + /// * `amount` - 要转换的 WSOL 数量(以 lamports 为单位) + /// + /// # Returns + /// * `Ok(String)` - 交易签名 + /// * `Err(anyhow::Error)` - 如果交易执行失败 + /// + /// # Errors + /// + /// 此函数在以下情况下会返回错误: + /// - 用户 WSOL ATA 中余额不足 + /// - seed 账户创建失败 + /// - 转账指令执行失败 + /// - 交易执行或确认失败 + /// - 网络或 RPC 错误 + pub async fn wrap_wsol_to_sol(&self, amount: u64) -> Result { + use crate::common::seed::get_associated_token_address_with_program_id_use_seed; + use crate::trading::common::wsol_manager::{ + wrap_wsol_to_sol as wrap_wsol_to_sol_internal, wrap_wsol_to_sol_without_create, + }; + use solana_sdk::transaction::Transaction; + + // 检查临时seed账户是否已存在 + let seed_ata_address = get_associated_token_address_with_program_id_use_seed( + &self.payer.pubkey(), + &crate::constants::WSOL_TOKEN_ACCOUNT, + &crate::constants::TOKEN_PROGRAM, + )?; + + let account_exists = self.infrastructure.rpc.get_account(&seed_ata_address).await.is_ok(); + + let instructions = if account_exists { + // 如果账户已存在,使用不创建账户的版本 + wrap_wsol_to_sol_without_create(&self.payer.pubkey(), amount)? + } else { + // 如果账户不存在,使用创建账户的版本 + wrap_wsol_to_sol_internal(&self.payer.pubkey(), amount)? + }; + + let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); + transaction.sign(&[&*self.payer], recent_blockhash); + let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; + Ok(signature.to_string()) + } + + /// Claim Bonding Curve (Pump) cashback. + /// + /// Transfers native SOL from the user's UserVolumeAccumulator to the wallet. + /// If there is nothing to claim, the transaction may still succeed with no SOL transferred. + /// + /// # Returns + /// * `Ok(String)` - Transaction signature + /// * `Err(anyhow::Error)` - Build or send failure (e.g. invalid PDA) + pub async fn claim_cashback_pumpfun(&self) -> Result { + use solana_sdk::transaction::Transaction; + let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction( + &self.payer.pubkey(), + ) + .ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?; + let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; + let mut transaction = Transaction::new_with_payer(&[ix], Some(&self.payer.pubkey())); + transaction.sign(&[&*self.payer], recent_blockhash); + let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; + Ok(signature.to_string()) + } + + /// Claim PumpSwap (AMM) cashback. + /// + /// Transfers WSOL from the UserVolumeAccumulator to the user's WSOL ATA. + /// Creates the user's WSOL ATA idempotently if it does not exist, then claims. + /// + /// # Returns + /// * `Ok(String)` - Transaction signature + /// * `Err(anyhow::Error)` - Build or send failure + pub async fn claim_cashback_pumpswap(&self) -> Result { + use solana_sdk::transaction::Transaction; + let mut instructions = + crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( + &self.payer.pubkey(), + &self.payer.pubkey(), + &WSOL_TOKEN_ACCOUNT, + &crate::constants::TOKEN_PROGRAM, + self.use_seed_optimize, + ); + let ix = crate::instruction::pumpswap::claim_cashback_pumpswap_instruction( + &self.payer.pubkey(), + WSOL_TOKEN_ACCOUNT, + crate::constants::TOKEN_PROGRAM, + ) + .ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?; + instructions.push(ix); + let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; + let mut transaction = + Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); + transaction.sign(&[&*self.payer], recent_blockhash); + let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; + Ok(signature.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs index b6a141b..a05fba9 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod client; pub mod common; pub mod constants; pub mod instruction; @@ -5,1225 +6,10 @@ pub mod perf; pub mod swqos; pub mod trading; pub mod utils; -use crate::common::nonce_cache::DurableNonceInfo; -use crate::common::sdk_log; -use crate::common::GasFeeStrategy; -use crate::common::{InfrastructureConfig, TradeConfig}; -#[cfg(feature = "perf-trace")] -use crate::constants::trade::trade::DEFAULT_SLIPPAGE; -use crate::constants::SOL_TOKEN_ACCOUNT; -use crate::constants::USD1_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; + // Re-export for SwqosConfig (Node1/BlockRazor transport; Astralane submission mode) pub use crate::swqos::{AstralaneTransport, SwqosTransport}; -use crate::trading::core::params::BonkParams; -use crate::trading::core::params::DexParamEnum; -use crate::trading::core::params::MeteoraDammV2Params; -use crate::trading::core::params::PumpFunParams; -use crate::trading::core::params::PumpSwapParams; -use crate::trading::core::params::RaydiumAmmV4Params; -use crate::trading::core::params::RaydiumCpmmParams; -use crate::trading::factory::DexType; -use crate::trading::MiddlewareManager; -use crate::trading::SwapParams; -use crate::trading::TradeFactory; -use common::SolanaRpcClient; -use parking_lot::Mutex; -use rustls::crypto::{ring::default_provider, CryptoProvider}; -use solana_sdk::hash::Hash; -use solana_sdk::message::AddressLookupTableAccount; -use solana_sdk::signer::Signer; -use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature}; -use std::sync::Arc; -#[allow(unused_imports)] -use tracing::{debug, error, info, warn}; - -/// Single place to validate that protocol params match the given DEX type (avoids duplicate match in buy/sell). -#[inline(always)] -fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool { - match dex_type { - DexType::PumpFun => params.as_any().downcast_ref::().is_some(), - DexType::PumpSwap => params.as_any().downcast_ref::().is_some(), - DexType::Bonk => params.as_any().downcast_ref::().is_some(), - DexType::RaydiumCpmm => params.as_any().downcast_ref::().is_some(), - DexType::RaydiumAmmV4 => params.as_any().downcast_ref::().is_some(), - DexType::MeteoraDammV2 => params.as_any().downcast_ref::().is_some(), - } -} - -/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。 -/// -/// * `dex_type`:PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。 -pub async fn find_pool_by_mint( - rpc: &SolanaRpcClient, - mint: &Pubkey, - dex_type: DexType, -) -> Result { - match dex_type { - DexType::PumpSwap => crate::instruction::utils::pumpswap::find_pool(rpc, mint).await, - _ => Err(anyhow::anyhow!("find_pool_by_mint not implemented for {:?}", dex_type)), - } -} - -/// Type of the token to buy -#[derive(Clone, PartialEq)] -pub enum TradeTokenType { - SOL, - WSOL, - USD1, - USDC, -} - -/// Shared infrastructure components that can be reused across multiple wallets -/// -/// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients) -/// that are wallet-independent and can be shared when only the trading wallet changes. -pub struct TradingInfrastructure { - /// Shared RPC client for blockchain interactions - pub rpc: Arc, - /// Shared SWQOS clients for transaction priority and routing. Arc> so cloning into SwapParams is a single Arc clone. - pub swqos_clients: Arc>>, - /// Configuration used to create this infrastructure - pub config: InfrastructureConfig, - /// Precomputed at init: min(swqos_clients.len(), 2/3 * num_cores). Not computed on trade hot path. - pub max_sender_concurrency: usize, - /// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path. - pub effective_core_ids: Arc>, -} - -impl TradingInfrastructure { - /// Create new shared infrastructure from configuration - /// - /// This performs the expensive initialization: - /// - Creates RPC client with connection pool - /// - Creates SWQOS clients (each with their own HTTP client) - /// - Initializes rent cache and starts background updater - pub async fn new(config: InfrastructureConfig) -> Self { - // Install crypto provider (idempotent) - if CryptoProvider::get_default().is_none() { - let _ = default_provider() - .install_default() - .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e)); - } - - // Create RPC client - let rpc = Arc::new(SolanaRpcClient::new_with_commitment( - config.rpc_url.clone(), - config.commitment.clone(), - )); - - // Initialize rent cache (with timeout so slow RPC doesn't block forever) - const RENT_UPDATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); - match tokio::time::timeout(RENT_UPDATE_TIMEOUT, common::seed::update_rents(&rpc)).await { - Ok(Ok(())) => {} - Ok(Err(e)) => { - if sdk_log::sdk_log_enabled() { - warn!(target: "sol_trade_sdk", "rent update failed: {}, using defaults", e); - } - common::seed::set_default_rents(); - } - Err(_) => { - if sdk_log::sdk_log_enabled() { - warn!(target: "sol_trade_sdk", "rent update timed out ({}s), using defaults; check RPC", RENT_UPDATE_TIMEOUT.as_secs()); - } - common::seed::set_default_rents(); - } - } - common::seed::start_rent_updater(rpc.clone()); - - // Create SWQOS clients with blacklist checking(QUIC 握手可能较慢,单节点超时 15s) - const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15); - let mut swqos_clients: Vec> = vec![]; - for swqos in &config.swqos_configs { - if swqos.is_blacklisted() { - if sdk_log::sdk_log_enabled() { - warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type()); - } - continue; - } - match tokio::time::timeout( - SWQOS_CLIENT_TIMEOUT, - SwqosConfig::get_swqos_client( - config.rpc_url.clone(), - config.commitment.clone(), - swqos.clone(), - config.mev_protection, - ), - ) - .await - { - Ok(Ok(swqos_client)) => swqos_clients.push(swqos_client), - Ok(Err(err)) => { - eprintln!( - "⚠️ SWQOS {:?} 初始化失败: {}(已从列表中排除)", - swqos.swqos_type(), - err - ); - if sdk_log::sdk_log_enabled() { - warn!( - target: "sol_trade_sdk", - "failed to create {:?} swqos client: {err}. Excluding from swqos list", - swqos.swqos_type() - ); - } - } - Err(_) => { - eprintln!( - "⚠️ SWQOS {:?} 初始化超时({}s),已跳过", - swqos.swqos_type(), - SWQOS_CLIENT_TIMEOUT.as_secs() - ); - if sdk_log::sdk_log_enabled() { - warn!( - target: "sol_trade_sdk", - "swqos {:?} init timed out ({}s), skipping", - swqos.swqos_type(), - SWQOS_CLIENT_TIMEOUT.as_secs() - ); - } - } - } - } - - // 若全部失败、被黑名单跳过或仅配置了不可用通道,至少保留一条 Rpc Default,否则 execute_parallel 会因 swqos_clients 为空直接报错。 - if swqos_clients.is_empty() { - eprintln!( - "⚠️ 无任何 SWQOS 客户端初始化成功,将回退为普通 RPC 发送: {}", - config.rpc_url - ); - if sdk_log::sdk_log_enabled() { - warn!( - target: "sol_trade_sdk", - "no SWQOS clients initialized; falling back to Rpc Default ({})", - config.rpc_url - ); - } - match SwqosConfig::get_swqos_client( - config.rpc_url.clone(), - config.commitment.clone(), - SwqosConfig::Default(config.rpc_url.clone()), - config.mev_protection, - ) - .await - { - Ok(c) => swqos_clients.push(c), - Err(e) => { - if sdk_log::sdk_log_enabled() { - warn!( - target: "sol_trade_sdk", - "fallback Rpc Default client failed: {}", - e - ); - } - } - } - } - - if !swqos_clients.is_empty() { - let labels: Vec<&str> = swqos_clients - .iter() - .map(|c| c.get_swqos_type().as_str()) - .collect(); - eprintln!( - "ℹ️ SWQOS 通道已就绪: {} 条 → [{}]", - swqos_clients.len(), - labels.join(", ") - ); - } - - let swqos_count = swqos_clients.len(); - let (max_sender_concurrency, effective_core_ids) = { - let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0); - let max_by_cores = (num_cores * 2 / 3).max(1); - let cap = swqos_count.min(max_by_cores).max(1); - let ids = core_affinity::get_core_ids() - .map(|all| { - let v: Vec<_> = all.into_iter().collect(); - let len = v.len(); - if config.swqos_cores_from_end && len >= cap { - v.into_iter().skip(len - cap).collect() - } else { - v.into_iter().take(cap).collect() - } - }) - .unwrap_or_default(); - (cap, Arc::new(ids)) - }; - - Self { - rpc, - swqos_clients: Arc::new(swqos_clients), - config, - max_sender_concurrency, - effective_core_ids, - } - } -} - -/// When using `TradeConfig::with_swqos_cores_from_end(true)`, returns the same "last N" core indices -/// that the infrastructure uses. Pass the result to `TradingClient::with_dedicated_sender_threads` -/// for 方式 C (组合使用): SWQOS on last N cores and dedicated sender threads pinned to those cores. -/// -/// Returns `None` if core count cannot be determined. `swqos_count` is typically `swqos_configs.len()`. -pub fn recommended_sender_thread_core_indices(swqos_count: usize) -> Option> { - let all = core_affinity::get_core_ids()?; - let num_cores = all.len(); - if num_cores == 0 { - return None; - } - let max_by_cores = (num_cores * 2 / 3).max(1); - let cap = swqos_count.min(max_by_cores).max(1).min(num_cores); - let start = num_cores.saturating_sub(cap); - Some((start..num_cores).collect()) -} - -/// Main trading client for Solana DeFi protocols -/// -/// `SolTradingSDK` 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 TradingClient { - /// The keypair used for signing all transactions - pub payer: Arc, - /// Shared infrastructure (RPC client, SWQOS clients) - /// Can be shared across multiple TradingClient instances with different wallets - pub infrastructure: Arc, - /// Optional middleware manager for custom transaction processing - pub middleware_manager: Option>, - /// Whether to use seed optimization for all ATA operations (default: true) - /// Applies to all token account creations across buy and sell operations - pub use_seed_optimize: bool, - /// Internal: use dedicated sender threads (default false). Set via with_dedicated_sender_threads() for advanced use. - pub use_dedicated_sender_threads: bool, - /// Internal: core indices for dedicated sender threads. Trimmed to ≤ max_sender_concurrency at set. - pub sender_thread_cores: Option>>, - /// Internal: precomputed at infra init (min(swqos_count, 2/3*cores)). Not user-configurable. - pub max_sender_concurrency: usize, - /// Internal: precomputed at infra init for job affinity. Not user-configurable. - pub effective_core_ids: Arc>, - /// Whether to output all SDK logs (from TradeConfig.log_enabled). - pub log_enabled: bool, - /// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency. - pub check_min_tip: bool, -} - -static INSTANCE: Mutex>> = Mutex::new(None); - -/// 🔄 向后兼容:SolanaTrade 别名 -pub type SolanaTrade = TradingClient; - -impl Clone for TradingClient { - fn clone(&self) -> Self { - Self { - payer: self.payer.clone(), - infrastructure: self.infrastructure.clone(), - middleware_manager: self.middleware_manager.clone(), - use_seed_optimize: self.use_seed_optimize, - use_dedicated_sender_threads: self.use_dedicated_sender_threads, - sender_thread_cores: self.sender_thread_cores.clone(), - max_sender_concurrency: self.max_sender_concurrency, - effective_core_ids: self.effective_core_ids.clone(), - log_enabled: self.log_enabled, - check_min_tip: self.check_min_tip, - } - } -} - -/// 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, - /// Type of the token to buy - pub input_token_type: TradeTokenType, - /// Public key of the token to purchase - pub mint: Pubkey, - /// Amount of tokens to buy (in smallest token units) - pub input_token_amount: u64, - /// Optional slippage tolerance in basis points (e.g., 100 = 1%) - pub slippage_basis_points: Option, - /// Recent blockhash for transaction validity - pub recent_blockhash: Option, - /// Protocol-specific parameters (PumpFun, Raydium, etc.) - pub extension_params: DexParamEnum, - // Extended configuration - /// Optional address lookup table for transaction size optimization - pub address_lookup_table_account: Option, - /// Whether to wait for transaction confirmation before returning - pub wait_tx_confirmed: bool, - /// Whether to create input token associated token account - pub create_input_token_ata: bool, - /// Whether to close input token associated token account after trade - pub close_input_token_ata: bool, - /// Whether to create token mint associated token account - pub create_mint_ata: bool, - /// Durable nonce information - pub durable_nonce: Option, - /// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated) - pub fixed_output_token_amount: Option, - /// Gas fee strategy - pub gas_fee_strategy: GasFeeStrategy, - /// Whether to simulate the transaction instead of executing it - pub simulate: bool, - /// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap). - /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens. - /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input. - /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes. - pub use_exact_sol_amount: Option, - /// 可选:事件收到时间(微秒,与 sol-parser-sdk 的 metadata.grpc_recv_us / clock::now_micros 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点,打印起点→提交耗时。 - pub grpc_recv_us: Option, -} - -/// 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, - /// Type of the token to sell - pub output_token_type: TradeTokenType, - /// Public key of the token to sell - pub mint: Pubkey, - /// Amount of tokens to sell (in smallest token units) - pub input_token_amount: u64, - /// Optional slippage tolerance in basis points (e.g., 100 = 1%) - pub slippage_basis_points: Option, - /// Recent blockhash for transaction validity - pub recent_blockhash: Option, - /// Whether to include tip for transaction priority - pub with_tip: bool, - /// Protocol-specific parameters (PumpFun, Raydium, etc.) - pub extension_params: DexParamEnum, - // Extended configuration - /// Optional address lookup table for transaction size optimization - pub address_lookup_table_account: Option, - /// Whether to wait for transaction confirmation before returning - pub wait_tx_confirmed: bool, - /// Whether to create output token associated token account - pub create_output_token_ata: bool, - /// Whether to close output token associated token account after trade - pub close_output_token_ata: bool, - /// Whether to close mint token associated token account after trade - pub close_mint_token_ata: bool, - /// Durable nonce information - pub durable_nonce: Option, - /// Optional fixed output token amount (If this value is set, it will be directly assigned to the output amount instead of being calculated) - pub fixed_output_token_amount: Option, - /// Gas fee strategy - pub gas_fee_strategy: GasFeeStrategy, - /// Whether to simulate the transaction instead of executing it - pub simulate: bool, - /// 可选:事件收到时间(微秒,与 sol-parser-sdk clock 同源)。不传且开启 log_enabled 时 SDK 用 now_micros() 作为起点。 - pub grpc_recv_us: Option, -} - -impl TradingClient { - /// Create a TradingClient from shared infrastructure (fast path) - /// - /// This is the preferred method when multiple wallets share the same infrastructure. - /// It only performs wallet-specific initialization (fast_init) without the expensive - /// RPC/SWQOS client creation. - /// - /// # Arguments - /// * `payer` - The keypair used for signing transactions - /// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients) - /// * `use_seed_optimize` - Whether to use seed optimization for ATA operations - /// - /// # Returns - /// Returns a configured `TradingClient` instance ready for trading operations - pub fn from_infrastructure( - payer: Arc, - infrastructure: Arc, - use_seed_optimize: bool, - ) -> Self { - // Initialize wallet-specific caches (fast, synchronous) - crate::common::fast_fn::fast_init(&payer.pubkey()); - let max_sender_concurrency = infrastructure.max_sender_concurrency; - let effective_core_ids = infrastructure.effective_core_ids.clone(); - - Self { - payer, - infrastructure, - middleware_manager: None, - use_seed_optimize, - use_dedicated_sender_threads: false, - sender_thread_cores: None, - max_sender_concurrency, - effective_core_ids, - log_enabled: true, - check_min_tip: false, - } - } - - /// Create a TradingClient from shared infrastructure with optional WSOL ATA setup - /// - /// Same as `from_infrastructure` but also handles WSOL ATA creation if requested. - /// - /// # Arguments - /// * `payer` - The keypair used for signing transactions - /// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients) - /// * `use_seed_optimize` - Whether to use seed optimization for ATA operations - /// * `create_wsol_ata` - Whether to check/create WSOL ATA - pub async fn from_infrastructure_with_wsol_setup( - payer: Arc, - infrastructure: Arc, - use_seed_optimize: bool, - create_wsol_ata: bool, - ) -> Self { - crate::common::fast_fn::fast_init(&payer.pubkey()); - - if create_wsol_ata { - // 在后台异步创建 WSOL ATA,不阻塞启动 - let payer_clone = payer.clone(); - let rpc_clone = infrastructure.rpc.clone(); - tokio::spawn(async move { - Self::ensure_wsol_ata(&payer_clone, &rpc_clone).await; - }); - if sdk_log::sdk_log_enabled() { - info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA creation started in background, does not block bot startup"); - } - } - - let max_sender_concurrency = infrastructure.max_sender_concurrency; - let effective_core_ids = infrastructure.effective_core_ids.clone(); - - Self { - payer, - infrastructure, - middleware_manager: None, - use_seed_optimize, - use_dedicated_sender_threads: false, - sender_thread_cores: None, - max_sender_concurrency, - effective_core_ids, - log_enabled: true, - check_min_tip: false, - } - } - - /// 单次尝试创建 WSOL ATA:获取 blockhash、组交易、发送并确认。成功或账户已存在返回 Ok(()),否则返回 Err(错误信息)。 - async fn try_create_wsol_ata_once( - rpc: &SolanaRpcClient, - payer: &Arc, - wsol_ata: &solana_sdk::pubkey::Pubkey, - create_ata_ixs: &[solana_sdk::instruction::Instruction], - timeout_secs: u64, - ) -> Result<(), String> { - use solana_sdk::transaction::Transaction; - let recent_blockhash = rpc - .get_latest_blockhash() - .await - .map_err(|e| format!("Failed to get blockhash: {}", e))?; - let tx = Transaction::new_signed_with_payer( - create_ata_ixs, - Some(&payer.pubkey()), - &[payer.as_ref()], - recent_blockhash, - ); - let send_result = tokio::time::timeout( - tokio::time::Duration::from_secs(timeout_secs), - rpc.send_and_confirm_transaction(&tx), - ) - .await; - match send_result { - Ok(Ok(_signature)) => Ok(()), - Ok(Err(e)) => { - if rpc.get_account(wsol_ata).await.is_ok() { - return Ok(()); - } - Err(format!("{}", e)) - } - Err(_) => Err(format!("Transaction confirmation timeout ({}s)", timeout_secs)), - } - } - - /// 确保钱包存在 WSOL ATA;不存在则发交易创建(会花费租金 + 手续费,初始化阶段唯一会扣钱的逻辑) - async fn ensure_wsol_ata(payer: &Arc, rpc: &Arc) { - const MAX_RETRIES: usize = 3; - const TIMEOUT_SECS: u64 = 10; - - let wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &payer.pubkey(), - &WSOL_TOKEN_ACCOUNT, - &crate::constants::TOKEN_PROGRAM, - ); - - if rpc.get_account(&wsol_ata).await.is_ok() { - if sdk_log::sdk_log_enabled() { - info!(target: "sol_trade_sdk", "✅ WSOL ATA already exists: {}", wsol_ata); - } - return; - } - - let create_ata_ixs = crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey()); - if create_ata_ixs.is_empty() { - if sdk_log::sdk_log_enabled() { - info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA already exists (no need to create)"); - } - return; - } - - if sdk_log::sdk_log_enabled() { - info!(target: "sol_trade_sdk", "🔨 Creating WSOL ATA: {}", wsol_ata); - } - let mut last_error = None; - for attempt in 1..=MAX_RETRIES { - if attempt > 1 { - if sdk_log::sdk_log_enabled() { - info!(target: "sol_trade_sdk", "🔄 Retrying WSOL ATA creation (attempt {}/{})...", attempt, MAX_RETRIES); - } - tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - } - match Self::try_create_wsol_ata_once( - rpc.as_ref(), - payer, - &wsol_ata, - &create_ata_ixs, - TIMEOUT_SECS, - ) - .await - { - Ok(()) => { - if sdk_log::sdk_log_enabled() { - info!(target: "sol_trade_sdk", "✅ WSOL ATA created or already exists"); - } - return; - } - Err(e) => { - last_error = Some(e.clone()); - if attempt < MAX_RETRIES && sdk_log::sdk_log_enabled() { - warn!(target: "sol_trade_sdk", "⚠️ Attempt {} failed: {}", attempt, e); - } - } - } - } - - if let Some(err) = last_error { - if sdk_log::sdk_log_enabled() { - error!(target: "sol_trade_sdk", "❌ WSOL ATA creation failed after {} retries: {}", MAX_RETRIES, wsol_ata); - error!(target: "sol_trade_sdk", " Error: {}", err); - error!(target: "sol_trade_sdk", " 💡 Possible causes: insufficient SOL, RPC timeout, or fee"); - error!(target: "sol_trade_sdk", " 🔧 Solutions: fund wallet (e.g. 0.1 SOL), retry, check RPC"); - } - std::thread::sleep(std::time::Duration::from_secs(5)); - panic!( - "❌ WSOL ATA creation failed and account does not exist: {}. Error: {}", - wsol_ata, err - ); - } - } - - /// Creates a new SolTradingSDK 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 - /// * `trade_config` - Trading configuration including RPC URL, SWQOS settings, etc. - /// - /// # Returns - /// Returns a configured `SolTradingSDK` instance ready for trading operations - #[inline] - pub async fn new(payer: Arc, trade_config: TradeConfig) -> Self { - // 设置 SDK 全局日志开关,后续所有 SDK 内日志(SWQOS/WSOL/耗时等)均受此控制 - sdk_log::set_sdk_log_enabled(trade_config.log_enabled); - // 预热高性能时钟,避免首笔交易时触发 3 次 Utc::now() 校准 - let _ = crate::common::clock::now_micros(); - // Create infrastructure from trade config - let infra_config = InfrastructureConfig::from_trade_config(&trade_config); - let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await); - - // Initialize wallet-specific caches - crate::common::fast_fn::fast_init(&payer.pubkey()); - - // ═══════════════════════════════════════════════════════════════════════════════ - // 初始化阶段会花费租金/手续费的唯一路径:创建 WSOL ATA(ensure_wsol_ata) - // - 触发条件:create_wsol_ata_on_startup == true 且钱包 SOL >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS - // - 花费:ATA 租金(约 0.00203928 SOL)+ 交易手续费;钱包不足时已跳过 - // - 其它初始化(TradingInfrastructure::new、update_rents、get_swqos_client)仅 RPC/HTTP,不发送交易 - // ═══════════════════════════════════════════════════════════════════════════════ - if trade_config.create_wsol_ata_on_startup { - const MIN_SOL_FOR_WSOL_ATA_LAMPORTS: u64 = 500_000; // 约 0.0005 SOL,用于 ATA 租金 + 手续费 - const BALANCE_CHECK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5); - let balance = tokio::time::timeout( - BALANCE_CHECK_TIMEOUT, - infrastructure.rpc.get_balance(&payer.pubkey()), - ) - .await - .unwrap_or(Ok(0)) - .unwrap_or(0); - if balance >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS { - Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await; - } else if sdk_log::sdk_log_enabled() { - info!( - target: "sol_trade_sdk", - "⏭️ 跳过创建 WSOL ATA:钱包 SOL 不足(当前 {} lamports,需要至少 {})", - balance, - MIN_SOL_FOR_WSOL_ATA_LAMPORTS - ); - } - } - - // 并发/核心相关由 infrastructure 预计算,用户无需配置 - let instance = Self { - payer, - infrastructure: infrastructure.clone(), - middleware_manager: None, - use_seed_optimize: trade_config.use_seed_optimize, - use_dedicated_sender_threads: false, - sender_thread_cores: None, - max_sender_concurrency: infrastructure.max_sender_concurrency, - effective_core_ids: infrastructure.effective_core_ids.clone(), - log_enabled: trade_config.log_enabled, - check_min_tip: trade_config.check_min_tip, - }; - - let mut current = INSTANCE.lock(); - *current = Some(Arc::new(instance.clone())); - - 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 - } - - /// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores). - /// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs. - /// Concurrency and core count are capped internally (≤ swqos count, ≤ 2/3 of CPU cores). - /// - `None`: keep default (shared tokio pool). - /// - `Some(vec![])`: dedicated threads with default count, no core pinning. - /// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap). - /// - /// **Latency note:** If a core is busy with other work (node, bot), SWQOS submit on that core can be delayed. - /// For lowest latency, pass core indices that are *reserved* for SWQOS (do not run other CPU-heavy work on those cores). - pub fn with_dedicated_sender_threads(mut self, core_indices: Option>) -> Self { - match core_indices { - None => { - self.use_dedicated_sender_threads = false; - self.sender_thread_cores = None; - } - Some(v) if v.is_empty() => { - self.use_dedicated_sender_threads = true; - self.sender_thread_cores = None; - } - Some(v) => { - self.use_dedicated_sender_threads = true; - let cap = v.len().min(self.max_sender_concurrency); - self.sender_thread_cores = Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v })); - } - } - self - } - - /// 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 { - &self.infrastructure.rpc - } - - /// 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 { - let instance = INSTANCE.lock(); - instance - .as_ref() - .expect("SolanaTrade instance not initialized. Please call new() first.") - .clone() - } - - /// Execute a buy order for a specified token - /// - /// 🔧 修复:返回Vec支持多SWQOS并发交易 - /// - bool: 是否至少有一个交易成功 - /// - Vec: 所有提交的交易签名(按SWQOS顺序) - /// - Option: 最后一个错误(如果全部失败) - /// - /// # Arguments - /// - /// * `params` - Buy trade parameters containing all necessary trading configuration - /// - /// # Returns - /// - /// Returns `Ok((bool, Vec, Option))` with success flag and all transaction signatures, - /// or an error if the transaction fails. - /// - /// # Errors - /// - /// This function will return an error if: - /// - 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 - /// - Required accounts cannot be created or accessed - #[inline] - pub async fn buy( - &self, - params: TradeBuyParams, - ) -> Result<(bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> { - if params.recent_blockhash.is_none() && params.durable_nonce.is_none() { - return Err(anyhow::anyhow!( - "Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)" - )); - } - #[cfg(feature = "perf-trace")] - if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() { - debug!( - target: "sol_trade_sdk", - "slippage_basis_points is none, use default slippage basis points: {}", - DEFAULT_SLIPPAGE - ); - } - if params.input_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk { - return Err(anyhow::anyhow!( - " Current version only supports USD1 trading on Bonk protocols" - )); - } - let protocol_params = params.extension_params; - if !validate_protocol_params(params.dex_type, &protocol_params) { - return Err(anyhow::anyhow!( - "Invalid protocol params for Trade (dex={:?})", - params.dex_type - )); - } - let input_token_mint = if params.input_token_type == TradeTokenType::SOL { - SOL_TOKEN_ACCOUNT - } else if params.input_token_type == TradeTokenType::WSOL { - WSOL_TOKEN_ACCOUNT - } else if params.input_token_type == TradeTokenType::USDC { - USDC_TOKEN_ACCOUNT - } else { - USD1_TOKEN_ACCOUNT - }; - let executor = TradeFactory::create_executor(params.dex_type); - let buy_params = SwapParams { - rpc: Some(self.infrastructure.rpc.clone()), - payer: self.payer.clone(), - trade_type: TradeType::Buy, - input_mint: input_token_mint, - output_mint: params.mint, - input_token_program: None, - output_token_program: None, - input_amount: Some(params.input_token_amount), - slippage_basis_points: params.slippage_basis_points, - address_lookup_table_account: params.address_lookup_table_account, - recent_blockhash: params.recent_blockhash, - wait_tx_confirmed: params.wait_tx_confirmed, - protocol_params, - open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置 - swqos_clients: self.infrastructure.swqos_clients.clone(), - middleware_manager: self.middleware_manager.clone(), - durable_nonce: params.durable_nonce, - with_tip: true, - create_input_mint_ata: params.create_input_token_ata, - close_input_mint_ata: params.close_input_token_ata, - create_output_mint_ata: params.create_mint_ata, - close_output_mint_ata: false, - fixed_output_amount: params.fixed_output_token_amount, - gas_fee_strategy: params.gas_fee_strategy, - simulate: params.simulate, - log_enabled: self.log_enabled, - use_dedicated_sender_threads: self.use_dedicated_sender_threads, - sender_thread_cores: self.sender_thread_cores.clone(), - max_sender_concurrency: self.max_sender_concurrency, - effective_core_ids: self.effective_core_ids.clone(), - check_min_tip: self.check_min_tip, - grpc_recv_us: params.grpc_recv_us, - use_exact_sol_amount: params.use_exact_sol_amount, - }; - - let swap_result = executor.swap(buy_params).await; - let result = - swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings)); - result - } - - /// Execute a sell order for a specified token - /// - /// 🔧 修复:返回Vec支持多SWQOS并发交易 - /// - bool: 是否至少有一个交易成功 - /// - Vec: 所有提交的交易签名(按SWQOS顺序) - /// - Option: 最后一个错误(如果全部失败) - /// - /// # Arguments - /// - /// * `params` - Sell trade parameters containing all necessary trading configuration - /// - /// # Returns - /// - /// Returns `Ok((bool, Vec, Option))` with success flag and all transaction signatures, - /// or an error if the transaction fails. - /// - /// # Errors - /// - /// This function will return an error if: - /// - 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 - /// - Required accounts cannot be created or accessed - #[inline] - pub async fn sell( - &self, - params: TradeSellParams, - ) -> Result<(bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> { - #[cfg(feature = "perf-trace")] - if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() { - debug!( - target: "sol_trade_sdk", - "slippage_basis_points is none, use default slippage basis points: {}", - DEFAULT_SLIPPAGE - ); - } - if params.recent_blockhash.is_none() && params.durable_nonce.is_none() { - return Err(anyhow::anyhow!( - "Must provide either recent_blockhash or durable_nonce for sell (required for transaction validity)" - )); - } - if params.output_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk { - return Err(anyhow::anyhow!( - " Current version only supports USD1 trading on Bonk protocols" - )); - } - let protocol_params = params.extension_params; - if !validate_protocol_params(params.dex_type, &protocol_params) { - return Err(anyhow::anyhow!( - "Invalid protocol params for Trade (dex={:?})", - params.dex_type - )); - } - let executor = TradeFactory::create_executor(params.dex_type); - let output_token_mint = if params.output_token_type == TradeTokenType::SOL { - SOL_TOKEN_ACCOUNT - } else if params.output_token_type == TradeTokenType::WSOL { - WSOL_TOKEN_ACCOUNT - } else if params.output_token_type == TradeTokenType::USDC { - USDC_TOKEN_ACCOUNT - } else { - USD1_TOKEN_ACCOUNT - }; - let sell_params = SwapParams { - rpc: Some(self.infrastructure.rpc.clone()), - payer: self.payer.clone(), - trade_type: TradeType::Sell, - input_mint: params.mint, - output_mint: output_token_mint, - input_token_program: None, - output_token_program: None, - input_amount: Some(params.input_token_amount), - slippage_basis_points: params.slippage_basis_points, - address_lookup_table_account: params.address_lookup_table_account, - recent_blockhash: params.recent_blockhash, - wait_tx_confirmed: params.wait_tx_confirmed, - protocol_params, - with_tip: params.with_tip, - open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置 - swqos_clients: self.infrastructure.swqos_clients.clone(), - middleware_manager: self.middleware_manager.clone(), - durable_nonce: params.durable_nonce, - create_input_mint_ata: false, - close_input_mint_ata: params.close_mint_token_ata, - create_output_mint_ata: params.create_output_token_ata, - close_output_mint_ata: params.close_output_token_ata, - fixed_output_amount: params.fixed_output_token_amount, - gas_fee_strategy: params.gas_fee_strategy, - simulate: params.simulate, - log_enabled: self.log_enabled, - use_dedicated_sender_threads: self.use_dedicated_sender_threads, - sender_thread_cores: self.sender_thread_cores.clone(), - max_sender_concurrency: self.max_sender_concurrency, - effective_core_ids: self.effective_core_ids.clone(), - check_min_tip: self.check_min_tip, - grpc_recv_us: params.grpc_recv_us, - use_exact_sol_amount: None, - }; - - let swap_result = executor.swap(sell_params).await; - let result = - swap_result.map(|(success, sigs, err, timings)| (success, sigs, err.map(TradeError::from), timings)); - result - } - - /// Execute a sell order for a percentage of the specified token amount - /// - /// This is a convenience function that calculates the exact amount to sell based on - /// a percentage of the total token amount and then calls the `sell` function. - /// - /// # Arguments - /// - /// * `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%) - /// - /// # Returns - /// - /// 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 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, - mut params: TradeSellParams, - amount_token: u64, - percent: u64, - ) -> Result<(bool, Vec, Option, Vec<(crate::swqos::SwqosType, i64)>), anyhow::Error> { - if percent == 0 || percent > 100 { - return Err(anyhow::anyhow!("Percentage must be between 1 and 100")); - } - let amount = amount_token * percent / 100; - params.input_token_amount = amount; - self.sell(params).await - } - - /// 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 in trading operations. - /// - /// # Arguments - /// * `amount` - The amount of SOL to wrap (in lamports) - /// - /// # Returns - /// * `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 { - use crate::trading::common::wsol_manager::handle_wsol; - use solana_sdk::transaction::Transaction; - let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; - let instructions = handle_wsol(&self.payer.pubkey(), amount); - let mut transaction = - Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); - transaction.sign(&[&*self.payer], recent_blockhash); - let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; - Ok(signature.to_string()) - } - /// 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 after trading operations. - /// - /// # Returns - /// * `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 { - use crate::trading::common::wsol_manager::close_wsol; - use solana_sdk::transaction::Transaction; - let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; - let instructions = close_wsol(&self.payer.pubkey()); - let mut transaction = - Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); - transaction.sign(&[&*self.payer], recent_blockhash); - let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; - Ok(signature.to_string()) - } - - /// Creates a wSOL associated token account (ATA) without wrapping any SOL - /// - /// This function only creates the wSOL associated token account for the payer - /// without transferring any SOL into it. This is useful when you want to set up - /// the account infrastructure in advance without committing funds yet. - /// - /// # Returns - /// * `Ok(String)` - Transaction signature if successful - /// * `Err(anyhow::Error)` - If the transaction fails to execute - /// - /// # Errors - /// - /// This function will return an error if: - /// - wSOL ATA account already exists (idempotent, will succeed silently) - /// - Transaction fails to execute or confirm - /// - Network or RPC errors occur - /// - Insufficient SOL for transaction fees - pub async fn create_wsol_ata(&self) -> Result { - use crate::trading::common::wsol_manager::create_wsol_ata; - use solana_sdk::transaction::Transaction; - - let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; - let instructions = create_wsol_ata(&self.payer.pubkey()); - - // If instructions are empty, ATA already exists - if instructions.is_empty() { - return Err(anyhow::anyhow!("wSOL ATA already exists or no instructions needed")); - } - - let mut transaction = - Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); - transaction.sign(&[&*self.payer], recent_blockhash); - let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; - Ok(signature.to_string()) - } - - /// 将 WSOL 转换为 SOL,使用 seed 账户 - /// - /// 这个函数实现以下步骤: - /// 1. 使用 super::seed::create_associated_token_account_use_seed 创建 WSOL seed 账号 - /// 2. 使用 get_associated_token_address_with_program_id_use_seed 获取该账号的 ATA 地址 - /// 3. 添加从用户 WSOL ATA 转账到该 seed ATA 账号的指令 - /// 4. 添加关闭 WSOL seed 账号的指令 - /// - /// # Arguments - /// * `amount` - 要转换的 WSOL 数量(以 lamports 为单位) - /// - /// # Returns - /// * `Ok(String)` - 交易签名 - /// * `Err(anyhow::Error)` - 如果交易执行失败 - /// - /// # Errors - /// - /// 此函数在以下情况下会返回错误: - /// - 用户 WSOL ATA 中余额不足 - /// - seed 账户创建失败 - /// - 转账指令执行失败 - /// - 交易执行或确认失败 - /// - 网络或 RPC 错误 - pub async fn wrap_wsol_to_sol(&self, amount: u64) -> Result { - use crate::common::seed::get_associated_token_address_with_program_id_use_seed; - use crate::trading::common::wsol_manager::{ - wrap_wsol_to_sol as wrap_wsol_to_sol_internal, wrap_wsol_to_sol_without_create, - }; - use solana_sdk::transaction::Transaction; - - // 检查临时seed账户是否已存在 - let seed_ata_address = get_associated_token_address_with_program_id_use_seed( - &self.payer.pubkey(), - &crate::constants::WSOL_TOKEN_ACCOUNT, - &crate::constants::TOKEN_PROGRAM, - )?; - - let account_exists = self.infrastructure.rpc.get_account(&seed_ata_address).await.is_ok(); - - let instructions = if account_exists { - // 如果账户已存在,使用不创建账户的版本 - wrap_wsol_to_sol_without_create(&self.payer.pubkey(), amount)? - } else { - // 如果账户不存在,使用创建账户的版本 - wrap_wsol_to_sol_internal(&self.payer.pubkey(), amount)? - }; - - let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; - let mut transaction = - Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); - transaction.sign(&[&*self.payer], recent_blockhash); - let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; - Ok(signature.to_string()) - } - - /// Claim Bonding Curve (Pump) cashback. - /// - /// Transfers native SOL from the user's UserVolumeAccumulator to the wallet. - /// If there is nothing to claim, the transaction may still succeed with no SOL transferred. - /// - /// # Returns - /// * `Ok(String)` - Transaction signature - /// * `Err(anyhow::Error)` - Build or send failure (e.g. invalid PDA) - pub async fn claim_cashback_pumpfun(&self) -> Result { - use solana_sdk::transaction::Transaction; - let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction( - &self.payer.pubkey(), - ) - .ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?; - let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; - let mut transaction = Transaction::new_with_payer(&[ix], Some(&self.payer.pubkey())); - transaction.sign(&[&*self.payer], recent_blockhash); - let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; - Ok(signature.to_string()) - } - - /// Claim PumpSwap (AMM) cashback. - /// - /// Transfers WSOL from the UserVolumeAccumulator to the user's WSOL ATA. - /// Creates the user's WSOL ATA idempotently if it does not exist, then claims. - /// - /// # Returns - /// * `Ok(String)` - Transaction signature - /// * `Err(anyhow::Error)` - Build or send failure - pub async fn claim_cashback_pumpswap(&self) -> Result { - use solana_sdk::transaction::Transaction; - let mut instructions = - crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed( - &self.payer.pubkey(), - &self.payer.pubkey(), - &WSOL_TOKEN_ACCOUNT, - &crate::constants::TOKEN_PROGRAM, - self.use_seed_optimize, - ); - let ix = crate::instruction::pumpswap::claim_cashback_pumpswap_instruction( - &self.payer.pubkey(), - WSOL_TOKEN_ACCOUNT, - crate::constants::TOKEN_PROGRAM, - ) - .ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?; - instructions.push(ix); - let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?; - let mut transaction = - Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey())); - transaction.sign(&[&*self.payer], recent_blockhash); - let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?; - Ok(signature.to_string()) - } -} +pub use client::{ + find_pool_by_mint, recommended_sender_thread_core_indices, SolanaTrade, TradeBuyParams, + TradeSellParams, TradeTokenType, TradingClient, TradingInfrastructure, +}; diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs deleted file mode 100755 index 167d154..0000000 --- a/src/trading/core/params.rs +++ /dev/null @@ -1,898 +0,0 @@ -use crate::common::bonding_curve::BondingCurveAccount; -use crate::common::nonce_cache::DurableNonceInfo; -use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id; -use crate::common::{GasFeeStrategy, SolanaRpcClient}; -use core_affinity::CoreId; -use crate::instruction::utils::pumpfun::is_mayhem_fee_recipient; - -/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams. -#[derive(Clone)] -pub struct SenderConcurrencyConfig { - pub sender_thread_cores: Option>>, - pub effective_core_ids: Arc>, - pub max_sender_concurrency: usize, -} -use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP; -use crate::swqos::{SwqosClient, TradeType}; -use crate::trading::common::get_multi_token_balances; -use crate::trading::MiddlewareManager; -use solana_hash::Hash; -use solana_message::AddressLookupTableAccount; -use solana_sdk::{pubkey::Pubkey, signature::Keypair}; -use std::sync::Arc; - -/// DEX 参数枚举 - 零开销抽象替代 Box -#[derive(Clone)] -pub enum DexParamEnum { - PumpFun(PumpFunParams), - PumpSwap(PumpSwapParams), - Bonk(BonkParams), - RaydiumCpmm(RaydiumCpmmParams), - RaydiumAmmV4(RaydiumAmmV4Params), - MeteoraDammV2(MeteoraDammV2Params), -} - -impl DexParamEnum { - /// 获取内部参数的 Any 引用,用于向后兼容的类型检查 - #[inline] - pub fn as_any(&self) -> &dyn std::any::Any { - match self { - DexParamEnum::PumpFun(p) => p, - DexParamEnum::PumpSwap(p) => p, - DexParamEnum::Bonk(p) => p, - DexParamEnum::RaydiumCpmm(p) => p, - DexParamEnum::RaydiumAmmV4(p) => p, - DexParamEnum::MeteoraDammV2(p) => p, - } - } -} - -/// Swap parameters -#[derive(Clone)] -pub struct SwapParams { - pub rpc: Option>, - pub payer: Arc, - pub trade_type: TradeType, - pub input_mint: Pubkey, - pub input_token_program: Option, - pub output_mint: Pubkey, - pub output_token_program: Option, - pub input_amount: Option, - pub slippage_basis_points: Option, - pub address_lookup_table_account: Option, - pub recent_blockhash: Option, - pub wait_tx_confirmed: bool, - pub protocol_params: DexParamEnum, - pub open_seed_optimize: bool, - /// Arc> so cloning from infrastructure is a single Arc clone. - pub swqos_clients: Arc>>, - pub middleware_manager: Option>, - pub durable_nonce: Option, - pub with_tip: bool, - pub create_input_mint_ata: bool, - pub close_input_mint_ata: bool, - pub create_output_mint_ata: bool, - pub close_output_mint_ata: bool, - pub fixed_output_amount: Option, - pub gas_fee_strategy: GasFeeStrategy, - pub simulate: bool, - /// Whether to output SDK logs (from TradeConfig.log_enabled). - pub log_enabled: bool, - /// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()). - pub use_dedicated_sender_threads: bool, - /// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path. - pub sender_thread_cores: Option>>, - /// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path. - pub max_sender_concurrency: usize, - /// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only. - pub effective_core_ids: Arc>, - /// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency. - pub check_min_tip: bool, - /// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled. - pub grpc_recv_us: Option, - /// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap). - /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens. - /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input. - /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes. - pub use_exact_sol_amount: Option, -} - -impl SwapParams { - /// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only. - #[inline] - pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig { - SenderConcurrencyConfig { - sender_thread_cores: self.sender_thread_cores.clone(), - effective_core_ids: self.effective_core_ids.clone(), - max_sender_concurrency: self.max_sender_concurrency, - } - } -} - -impl std::fmt::Debug for SwapParams { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "SwapParams: ...") - } -} - -/// PumpFun protocol specific parameters -/// Configuration parameters specific to PumpFun trading protocol. -/// -/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` = -/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`]. -/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct -/// does not affect ix building. -#[derive(Clone)] -pub struct PumpFunParams { - pub bonding_curve: Arc, - pub associated_bonding_curve: Pubkey, - /// Resolved by [`resolve_creator_vault_for_ix`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix): use ix vault when it matches `PDA(creator)` or fee-sharing vault; else `PDA(creator)`. - pub creator_vault: Pubkey, - pub token_program: Pubkey, - /// Whether to close token account when selling, only effective during sell operations - pub close_token_account_when_sell: Option, - /// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。 - /// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。 - pub fee_recipient: Pubkey, -} - -impl PumpFunParams { - pub fn immediate_sell( - creator_vault: Pubkey, - token_program: Pubkey, - close_token_account_when_sell: bool, - ) -> Self { - Self { - bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }), - associated_bonding_curve: Pubkey::default(), - creator_vault: creator_vault, - token_program: token_program, - close_token_account_when_sell: Some(close_token_account_when_sell), - fee_recipient: Pubkey::default(), - } - } - - /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event - /// so that sell instructions include the correct remaining accounts for cashback. - /// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`). - /// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts). - pub fn from_dev_trade( - mint: Pubkey, - token_amount: u64, - max_sol_cost: u64, - creator: Pubkey, - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, - creator_vault: Pubkey, - close_token_account_when_sell: Option, - fee_recipient: Pubkey, - token_program: Pubkey, - is_cashback_coin: bool, - mayhem_mode: Option, - ) -> Self { - let is_mayhem_mode = - mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient)); - let bonding_curve_account = BondingCurveAccount::from_dev_trade( - bonding_curve, - &mint, - token_amount, - max_sol_cost, - creator, - is_mayhem_mode, - is_cashback_coin, - ); - let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix( - &bonding_curve_account.creator, - creator_vault, - &mint, - ) - .or_else(|| { - crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator) - }) - .unwrap_or_default(); - Self { - bonding_curve: Arc::new(bonding_curve_account), - associated_bonding_curve: associated_bonding_curve, - creator_vault: creator_vault_resolved, - close_token_account_when_sell: close_token_account_when_sell, - token_program: token_program, - fee_recipient, - } - } - - /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event - /// so that sell instructions include the correct remaining accounts for cashback. - /// - /// `mayhem_mode`: - /// - **`Some(v)`**(推荐):显式使用链上事件中的值。gRPC 日志解析对应 Explorer 的 `tradeEvent.mayhemMode`; - /// **不会**再用 `fee_recipient` 覆盖。 - /// - **`None`**:无该字段时(例如 ShredStream 仅解外层指令、或冷路径),才用 `fee_recipient` 是否落在 Mayhem 静态列表上推断。 - pub fn from_trade( - bonding_curve: Pubkey, - associated_bonding_curve: Pubkey, - mint: Pubkey, - creator: Pubkey, - creator_vault: Pubkey, - virtual_token_reserves: u64, - virtual_sol_reserves: u64, - real_token_reserves: u64, - real_sol_reserves: u64, - close_token_account_when_sell: Option, - fee_recipient: Pubkey, - token_program: Pubkey, - is_cashback_coin: bool, - mayhem_mode: Option, - ) -> Self { - let is_mayhem_mode = match mayhem_mode { - Some(v) => v, - None => is_mayhem_fee_recipient(&fee_recipient), - }; - let bonding_curve = BondingCurveAccount::from_trade( - bonding_curve, - mint, - creator, - virtual_token_reserves, - virtual_sol_reserves, - real_token_reserves, - real_sol_reserves, - is_mayhem_mode, - is_cashback_coin, - ); - let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix( - &bonding_curve.creator, - creator_vault, - &mint, - ) - .or_else(|| { - crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator) - }) - .unwrap_or_default(); - Self { - bonding_curve: Arc::new(bonding_curve), - associated_bonding_curve: associated_bonding_curve, - creator_vault: creator_vault_resolved, - close_token_account_when_sell: close_token_account_when_sell, - token_program: token_program, - fee_recipient, - } - } - - pub async fn from_mint_by_rpc( - rpc: &SolanaRpcClient, - mint: &Pubkey, - ) -> Result { - let account = - crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?; - let mint_account = rpc.get_account(&mint).await?; - let bonding_curve = BondingCurveAccount { - discriminator: 0, - account: account.1, - virtual_token_reserves: account.0.virtual_token_reserves, - virtual_sol_reserves: account.0.virtual_sol_reserves, - real_token_reserves: account.0.real_token_reserves, - real_sol_reserves: account.0.real_sol_reserves, - token_total_supply: account.0.token_total_supply, - complete: account.0.complete, - creator: account.0.creator, - is_mayhem_mode: account.0.is_mayhem_mode, - is_cashback_coin: account.0.is_cashback_coin, - }; - let associated_bonding_curve = get_associated_token_address_with_program_id( - &bonding_curve.account, - mint, - &mint_account.owner, - ); - let creator_vault = - crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator); - Ok(Self { - bonding_curve: Arc::new(bonding_curve), - associated_bonding_curve: associated_bonding_curve, - creator_vault: creator_vault.unwrap(), - close_token_account_when_sell: None, - token_program: mint_account.owner, - fee_recipient: Pubkey::default(), - }) - } - - /// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`]. - #[inline] - pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self { - self.creator_vault = creator_vault; - self - } -} - -/// PumpSwap Protocol Specific Parameters -/// -/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information, -/// token configuration, and transaction amounts. -/// -/// **Performance Note**: If these parameters are not provided, the system will attempt to -/// retrieve the relevant information from RPC, which will increase transaction time. -/// For optimal performance, it is recommended to provide all necessary parameters in advance. -#[derive(Clone)] -pub struct PumpSwapParams { - /// Liquidity pool address - pub pool: Pubkey, - /// Base token mint address - /// The mint account address of the base token in the trading pair - pub base_mint: Pubkey, - /// Quote token mint address - /// The mint account address of the quote token in the trading pair, usually SOL or USDC - pub quote_mint: Pubkey, - /// Pool base token account - pub pool_base_token_account: Pubkey, - /// Pool quote token account - pub pool_quote_token_account: Pubkey, - /// Base token reserves in the pool - pub pool_base_token_reserves: u64, - /// Quote token reserves in the pool - pub pool_quote_token_reserves: u64, - /// Coin creator vault ATA - pub coin_creator_vault_ata: Pubkey, - /// Coin creator vault authority - pub coin_creator_vault_authority: Pubkey, - /// Token program ID - pub base_token_program: Pubkey, - /// Quote token program ID - pub quote_token_program: Pubkey, - /// Whether the pool is in mayhem mode - pub is_mayhem_mode: bool, - /// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap - /// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()` - /// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053). - pub coin_creator: Pubkey, - /// Whether the pool's coin has cashback enabled - pub is_cashback_coin: bool, - /// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell - /// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS) - /// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket. - /// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps). - pub cashback_fee_basis_points: u64, -} - -impl PumpSwapParams { - pub fn new( - pool: Pubkey, - base_mint: Pubkey, - quote_mint: Pubkey, - pool_base_token_account: Pubkey, - pool_quote_token_account: Pubkey, - pool_base_token_reserves: u64, - pool_quote_token_reserves: u64, - coin_creator_vault_ata: Pubkey, - coin_creator_vault_authority: Pubkey, - base_token_program: Pubkey, - quote_token_program: Pubkey, - fee_recipient: Pubkey, - coin_creator: Pubkey, - is_cashback_coin: bool, - cashback_fee_basis_points: u64, - ) -> Self { - let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP; - Self { - pool, - base_mint, - quote_mint, - pool_base_token_account, - pool_quote_token_account, - pool_base_token_reserves, - pool_quote_token_reserves, - coin_creator_vault_ata, - coin_creator_vault_authority, - base_token_program, - quote_token_program, - is_mayhem_mode, - coin_creator, - is_cashback_coin, - cashback_fee_basis_points, - } - } - - /// Fast-path constructor for building PumpSwap parameters directly from decoded - /// trade/event data and the accompanying instruction accounts, avoiding RPC - /// lookups and associated latency. Token program IDs should be sourced from - /// the instruction accounts themselves to respect Token Program vs Token-2022 - /// differences. - /// - /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` - /// from the event so that buy/sell instructions include the correct remaining - /// accounts for cashback. - pub fn from_trade( - pool: Pubkey, - base_mint: Pubkey, - quote_mint: Pubkey, - pool_base_token_account: Pubkey, - pool_quote_token_account: Pubkey, - pool_base_token_reserves: u64, - pool_quote_token_reserves: u64, - coin_creator_vault_ata: Pubkey, - coin_creator_vault_authority: Pubkey, - base_token_program: Pubkey, - quote_token_program: Pubkey, - fee_recipient: Pubkey, - coin_creator: Pubkey, - is_cashback_coin: bool, - cashback_fee_basis_points: u64, - ) -> Self { - Self::new( - pool, - base_mint, - quote_mint, - pool_base_token_account, - pool_quote_token_account, - pool_base_token_reserves, - pool_quote_token_reserves, - coin_creator_vault_ata, - coin_creator_vault_authority, - base_token_program, - quote_token_program, - fee_recipient, - coin_creator, - is_cashback_coin, - cashback_fee_basis_points, - ) - } - - pub async fn from_mint_by_rpc( - rpc: &SolanaRpcClient, - mint: &Pubkey, - ) -> Result { - if let Ok((pool_address, _)) = - crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await - { - Self::from_pool_address_by_rpc(rpc, &pool_address).await - } else if let Ok((pool_address, _)) = - crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await - { - Self::from_pool_address_by_rpc(rpc, &pool_address).await - } else { - return Err(anyhow::anyhow!("No pool found for mint")); - } - } - - pub async fn from_pool_address_by_rpc( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - ) -> Result { - let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?; - Self::from_pool_data(rpc, pool_address, &pool_data).await - } - - /// Build params from an already-decoded Pool, only fetching token balances. - /// - /// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data - /// is already available (e.g. from `pumpswap::find_by_mint` which returns the - /// decoded Pool). - pub async fn from_pool_data( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - pool_data: &crate::instruction::utils::pumpswap_types::Pool, - ) -> Result { - let (pool_base_token_reserves, pool_quote_token_reserves) = - crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?; - let creator = pool_data.coin_creator; - let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata( - creator, - pool_data.quote_mint, - ); - let coin_creator_vault_authority = - crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator); - - let base_token_program_ata = get_associated_token_address_with_program_id( - pool_address, - &pool_data.base_mint, - &crate::constants::TOKEN_PROGRAM, - ); - let quote_token_program_ata = get_associated_token_address_with_program_id( - pool_address, - &pool_data.quote_mint, - &crate::constants::TOKEN_PROGRAM, - ); - - Ok(Self { - pool: *pool_address, - base_mint: pool_data.base_mint, - quote_mint: pool_data.quote_mint, - pool_base_token_account: pool_data.pool_base_token_account, - pool_quote_token_account: pool_data.pool_quote_token_account, - pool_base_token_reserves, - pool_quote_token_reserves, - coin_creator_vault_ata, - coin_creator_vault_authority, - base_token_program: if pool_data.pool_base_token_account == base_token_program_ata { - crate::constants::TOKEN_PROGRAM - } else { - crate::constants::TOKEN_PROGRAM_2022 - }, - is_cashback_coin: pool_data.is_cashback_coin, - quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata { - crate::constants::TOKEN_PROGRAM - } else { - crate::constants::TOKEN_PROGRAM_2022 - }, - is_mayhem_mode: pool_data.is_mayhem_mode, - coin_creator: pool_data.coin_creator, - cashback_fee_basis_points: 0, - }) - } -} - -/// Bonk protocol specific parameters -/// Configuration parameters specific to Bonk trading protocol -#[derive(Clone, Default)] -pub struct BonkParams { - pub virtual_base: u128, - pub virtual_quote: u128, - pub real_base: u128, - pub real_quote: u128, - pub pool_state: Pubkey, - pub base_vault: Pubkey, - pub quote_vault: Pubkey, - /// Token program ID - pub mint_token_program: Pubkey, - pub platform_config: Pubkey, - pub platform_associated_account: Pubkey, - pub creator_associated_account: Pubkey, - pub global_config: Pubkey, -} - -impl BonkParams { - pub fn immediate_sell( - mint_token_program: Pubkey, - platform_config: Pubkey, - platform_associated_account: Pubkey, - creator_associated_account: Pubkey, - global_config: Pubkey, - ) -> Self { - Self { - mint_token_program, - platform_config, - platform_associated_account, - creator_associated_account, - global_config, - ..Default::default() - } - } - pub fn from_trade( - virtual_base: u64, - virtual_quote: u64, - real_base_after: u64, - real_quote_after: u64, - pool_state: Pubkey, - base_vault: Pubkey, - quote_vault: Pubkey, - base_token_program: Pubkey, - platform_config: Pubkey, - platform_associated_account: Pubkey, - creator_associated_account: Pubkey, - global_config: Pubkey, - ) -> Self { - Self { - virtual_base: virtual_base as u128, - virtual_quote: virtual_quote as u128, - real_base: real_base_after as u128, - real_quote: real_quote_after as u128, - pool_state: pool_state, - base_vault: base_vault, - quote_vault: quote_vault, - mint_token_program: base_token_program, - platform_config: platform_config, - platform_associated_account: platform_associated_account, - creator_associated_account: creator_associated_account, - global_config: global_config, - } - } - - pub fn from_dev_trade( - is_exact_in: bool, - amount_in: u64, - amount_out: u64, - pool_state: Pubkey, - base_vault: Pubkey, - quote_vault: Pubkey, - base_token_program: Pubkey, - platform_config: Pubkey, - platform_associated_account: Pubkey, - creator_associated_account: Pubkey, - global_config: Pubkey, - ) -> Self { - const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382; - const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951; - let _amount_in = if is_exact_in { - amount_in - } else { - crate::instruction::utils::bonk::get_amount_in( - amount_out, - crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE, - crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE, - crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE, - DEFAULT_VIRTUAL_BASE, - DEFAULT_VIRTUAL_QUOTE, - 0, - 0, - 0, - ) - }; - let real_quote = crate::instruction::utils::bonk::get_amount_in_net( - amount_in, - crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE, - crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE, - crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE, - ) as u128; - let _amount_out = if is_exact_in { - crate::instruction::utils::bonk::get_amount_out( - amount_in, - crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE, - crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE, - crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE, - DEFAULT_VIRTUAL_BASE, - DEFAULT_VIRTUAL_QUOTE, - 0, - 0, - 0, - ) as u128 - } else { - amount_out as u128 - }; - let real_base = _amount_out; - Self { - virtual_base: DEFAULT_VIRTUAL_BASE, - virtual_quote: DEFAULT_VIRTUAL_QUOTE, - real_base: real_base, - real_quote: real_quote, - pool_state: pool_state, - base_vault: base_vault, - quote_vault: quote_vault, - mint_token_program: base_token_program, - platform_config: platform_config, - platform_associated_account: platform_associated_account, - creator_associated_account: creator_associated_account, - global_config: global_config, - } - } - - pub async fn from_mint_by_rpc( - rpc: &SolanaRpcClient, - mint: &Pubkey, - usd1_pool: bool, - ) -> Result { - let pool_address = crate::instruction::utils::bonk::get_pool_pda( - mint, - if usd1_pool { - &crate::constants::USD1_TOKEN_ACCOUNT - } else { - &crate::constants::WSOL_TOKEN_ACCOUNT - }, - ) - .unwrap(); - let pool_data = - crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?; - let token_account = rpc.get_account(&pool_data.base_mint).await?; - let platform_associated_account = - crate::instruction::utils::bonk::get_platform_associated_account( - &pool_data.platform_config, - ); - let creator_associated_account = - crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator); - let platform_associated_account = platform_associated_account.unwrap(); - let creator_associated_account = creator_associated_account.unwrap(); - Ok(Self { - virtual_base: pool_data.virtual_base as u128, - virtual_quote: pool_data.virtual_quote as u128, - real_base: pool_data.real_base as u128, - real_quote: pool_data.real_quote as u128, - pool_state: pool_address, - base_vault: pool_data.base_vault, - quote_vault: pool_data.quote_vault, - mint_token_program: token_account.owner, - platform_config: pool_data.platform_config, - platform_associated_account, - creator_associated_account, - global_config: pool_data.global_config, - }) - } -} - -/// RaydiumCpmm protocol specific parameters -/// Configuration parameters specific to Raydium CPMM trading protocol -#[derive(Clone)] -pub struct RaydiumCpmmParams { - /// Pool address - pub pool_state: Pubkey, - /// Amm config address - pub amm_config: Pubkey, - /// Base token mint address - pub base_mint: Pubkey, - /// Quote token mint address - pub quote_mint: Pubkey, - /// Base token reserve amount in the pool - pub base_reserve: u64, - /// Quote token reserve amount in the pool - pub quote_reserve: u64, - /// Base token vault address - pub base_vault: Pubkey, - /// Quote token vault address - pub quote_vault: Pubkey, - /// Base token program ID - pub base_token_program: Pubkey, - /// Quote token program ID - pub quote_token_program: Pubkey, - /// Observation state account - pub observation_state: Pubkey, -} - -impl RaydiumCpmmParams { - pub fn from_trade( - pool_state: Pubkey, - amm_config: Pubkey, - input_token_mint: Pubkey, - output_token_mint: Pubkey, - input_vault: Pubkey, - output_vault: Pubkey, - input_token_program: Pubkey, - output_token_program: Pubkey, - observation_state: Pubkey, - base_reserve: u64, - quote_reserve: u64, - ) -> Self { - Self { - pool_state: pool_state, - amm_config: amm_config, - base_mint: input_token_mint, - quote_mint: output_token_mint, - base_reserve: base_reserve, - quote_reserve: quote_reserve, - base_vault: input_vault, - quote_vault: output_vault, - base_token_program: input_token_program, - quote_token_program: output_token_program, - observation_state: observation_state, - } - } - - pub async fn from_pool_address_by_rpc( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - ) -> Result { - let pool = - crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?; - let (token0_balance, token1_balance) = - crate::instruction::utils::raydium_cpmm::get_pool_token_balances( - rpc, - pool_address, - &pool.token0_mint, - &pool.token1_mint, - ) - .await?; - Ok(Self { - pool_state: *pool_address, - amm_config: pool.amm_config, - base_mint: pool.token0_mint, - quote_mint: pool.token1_mint, - base_reserve: token0_balance, - quote_reserve: token1_balance, - base_vault: pool.token0_vault, - quote_vault: pool.token1_vault, - base_token_program: pool.token0_program, - quote_token_program: pool.token1_program, - observation_state: pool.observation_key, - }) - } -} - -/// RaydiumCpmm protocol specific parameters -/// Configuration parameters specific to Raydium CPMM trading protocol -#[derive(Clone)] -pub struct RaydiumAmmV4Params { - /// AMM pool address - pub amm: Pubkey, - /// Base token (coin) mint address - pub coin_mint: Pubkey, - /// Quote token (pc) mint address - pub pc_mint: Pubkey, - /// Pool's coin token account address - pub token_coin: Pubkey, - /// Pool's pc token account address - pub token_pc: Pubkey, - /// Current coin reserve amount in the pool - pub coin_reserve: u64, - /// Current pc reserve amount in the pool - pub pc_reserve: u64, -} - -impl RaydiumAmmV4Params { - pub fn new( - amm: Pubkey, - coin_mint: Pubkey, - pc_mint: Pubkey, - token_coin: Pubkey, - token_pc: Pubkey, - coin_reserve: u64, - pc_reserve: u64, - ) -> Self { - Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve } - } - pub async fn from_amm_address_by_rpc( - rpc: &SolanaRpcClient, - amm: Pubkey, - ) -> Result { - let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?; - let (coin_reserve, pc_reserve) = - get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?; - Ok(Self { - amm, - coin_mint: amm_info.coin_mint, - pc_mint: amm_info.pc_mint, - token_coin: amm_info.token_coin, - token_pc: amm_info.token_pc, - coin_reserve, - pc_reserve, - }) - } -} - -/// MeteoraDammV2 protocol specific parameters -/// Configuration parameters specific to Meteora Damm V2 trading protocol -#[derive(Clone)] -pub struct MeteoraDammV2Params { - pub pool: Pubkey, - pub token_a_vault: Pubkey, - pub token_b_vault: Pubkey, - pub token_a_mint: Pubkey, - pub token_b_mint: Pubkey, - pub token_a_program: Pubkey, - pub token_b_program: Pubkey, -} - -impl MeteoraDammV2Params { - pub fn new( - pool: Pubkey, - token_a_vault: Pubkey, - token_b_vault: Pubkey, - token_a_mint: Pubkey, - token_b_mint: Pubkey, - token_a_program: Pubkey, - token_b_program: Pubkey, - ) -> Self { - Self { - pool, - token_a_vault, - token_b_vault, - token_a_mint, - token_b_mint, - token_a_program, - token_b_program, - } - } - - pub async fn from_pool_address_by_rpc( - rpc: &SolanaRpcClient, - pool_address: &Pubkey, - ) -> Result { - let pool_data = - crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?; - let mint_accounts = rpc - .get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint]) - .await?; - let token_a_program = mint_accounts - .get(0) - .and_then(|a| a.as_ref()) - .map(|a| a.owner) - .ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?; - let token_b_program = mint_accounts - .get(1) - .and_then(|a| a.as_ref()) - .map(|a| a.owner) - .ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?; - Ok(Self { - pool: *pool_address, - token_a_vault: pool_data.token_a_vault, - token_b_vault: pool_data.token_b_vault, - token_a_mint: pool_data.token_a_mint, - token_b_mint: pool_data.token_b_mint, - token_a_program, - token_b_program, - }) - } -} diff --git a/src/trading/core/params/bonk.rs b/src/trading/core/params/bonk.rs new file mode 100644 index 0000000..5145fad --- /dev/null +++ b/src/trading/core/params/bonk.rs @@ -0,0 +1,178 @@ +use crate::common::SolanaRpcClient; +use solana_sdk::pubkey::Pubkey; + +/// Bonk protocol specific parameters +/// Configuration parameters specific to Bonk trading protocol +#[derive(Clone, Default)] +pub struct BonkParams { + pub virtual_base: u128, + pub virtual_quote: u128, + pub real_base: u128, + pub real_quote: u128, + pub pool_state: Pubkey, + pub base_vault: Pubkey, + pub quote_vault: Pubkey, + /// Token program ID + pub mint_token_program: Pubkey, + pub platform_config: Pubkey, + pub platform_associated_account: Pubkey, + pub creator_associated_account: Pubkey, + pub global_config: Pubkey, +} + +impl BonkParams { + pub fn immediate_sell( + mint_token_program: Pubkey, + platform_config: Pubkey, + platform_associated_account: Pubkey, + creator_associated_account: Pubkey, + global_config: Pubkey, + ) -> Self { + Self { + mint_token_program, + platform_config, + platform_associated_account, + creator_associated_account, + global_config, + ..Default::default() + } + } + pub fn from_trade( + virtual_base: u64, + virtual_quote: u64, + real_base_after: u64, + real_quote_after: u64, + pool_state: Pubkey, + base_vault: Pubkey, + quote_vault: Pubkey, + base_token_program: Pubkey, + platform_config: Pubkey, + platform_associated_account: Pubkey, + creator_associated_account: Pubkey, + global_config: Pubkey, + ) -> Self { + Self { + virtual_base: virtual_base as u128, + virtual_quote: virtual_quote as u128, + real_base: real_base_after as u128, + real_quote: real_quote_after as u128, + pool_state: pool_state, + base_vault: base_vault, + quote_vault: quote_vault, + mint_token_program: base_token_program, + platform_config: platform_config, + platform_associated_account: platform_associated_account, + creator_associated_account: creator_associated_account, + global_config: global_config, + } + } + + pub fn from_dev_trade( + is_exact_in: bool, + amount_in: u64, + amount_out: u64, + pool_state: Pubkey, + base_vault: Pubkey, + quote_vault: Pubkey, + base_token_program: Pubkey, + platform_config: Pubkey, + platform_associated_account: Pubkey, + creator_associated_account: Pubkey, + global_config: Pubkey, + ) -> Self { + const DEFAULT_VIRTUAL_BASE: u128 = 1073025605596382; + const DEFAULT_VIRTUAL_QUOTE: u128 = 30000852951; + let _amount_in = if is_exact_in { + amount_in + } else { + crate::instruction::utils::bonk::get_amount_in( + amount_out, + crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE, + crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE, + crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE, + DEFAULT_VIRTUAL_BASE, + DEFAULT_VIRTUAL_QUOTE, + 0, + 0, + 0, + ) + }; + let real_quote = crate::instruction::utils::bonk::get_amount_in_net( + amount_in, + crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE, + crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE, + crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE, + ) as u128; + let _amount_out = if is_exact_in { + crate::instruction::utils::bonk::get_amount_out( + amount_in, + crate::instruction::utils::bonk::accounts::PROTOCOL_FEE_RATE, + crate::instruction::utils::bonk::accounts::PLATFORM_FEE_RATE, + crate::instruction::utils::bonk::accounts::SHARE_FEE_RATE, + DEFAULT_VIRTUAL_BASE, + DEFAULT_VIRTUAL_QUOTE, + 0, + 0, + 0, + ) as u128 + } else { + amount_out as u128 + }; + let real_base = _amount_out; + Self { + virtual_base: DEFAULT_VIRTUAL_BASE, + virtual_quote: DEFAULT_VIRTUAL_QUOTE, + real_base: real_base, + real_quote: real_quote, + pool_state: pool_state, + base_vault: base_vault, + quote_vault: quote_vault, + mint_token_program: base_token_program, + platform_config: platform_config, + platform_associated_account: platform_associated_account, + creator_associated_account: creator_associated_account, + global_config: global_config, + } + } + + pub async fn from_mint_by_rpc( + rpc: &SolanaRpcClient, + mint: &Pubkey, + usd1_pool: bool, + ) -> Result { + let pool_address = crate::instruction::utils::bonk::get_pool_pda( + mint, + if usd1_pool { + &crate::constants::USD1_TOKEN_ACCOUNT + } else { + &crate::constants::WSOL_TOKEN_ACCOUNT + }, + ) + .unwrap(); + let pool_data = + crate::instruction::utils::bonk::fetch_pool_state(rpc, &pool_address).await?; + let token_account = rpc.get_account(&pool_data.base_mint).await?; + let platform_associated_account = + crate::instruction::utils::bonk::get_platform_associated_account( + &pool_data.platform_config, + ); + let creator_associated_account = + crate::instruction::utils::bonk::get_creator_associated_account(&pool_data.creator); + let platform_associated_account = platform_associated_account.unwrap(); + let creator_associated_account = creator_associated_account.unwrap(); + Ok(Self { + virtual_base: pool_data.virtual_base as u128, + virtual_quote: pool_data.virtual_quote as u128, + real_base: pool_data.real_base as u128, + real_quote: pool_data.real_quote as u128, + pool_state: pool_address, + base_vault: pool_data.base_vault, + quote_vault: pool_data.quote_vault, + mint_token_program: token_account.owner, + platform_config: pool_data.platform_config, + platform_associated_account, + creator_associated_account, + global_config: pool_data.global_config, + }) + } +} diff --git a/src/trading/core/params/dex_swap.rs b/src/trading/core/params/dex_swap.rs new file mode 100644 index 0000000..f256b5b --- /dev/null +++ b/src/trading/core/params/dex_swap.rs @@ -0,0 +1,118 @@ +use crate::common::nonce_cache::DurableNonceInfo; +use crate::common::{GasFeeStrategy, SolanaRpcClient}; +use crate::swqos::{SwqosClient, TradeType}; +use crate::trading::MiddlewareManager; +use core_affinity::CoreId; +use solana_hash::Hash; +use solana_message::AddressLookupTableAccount; +use solana_sdk::{pubkey::Pubkey, signature::Keypair}; +use std::sync::Arc; + +use super::bonk::BonkParams; +use super::meteora_damm_v2::MeteoraDammV2Params; +use super::pumpfun::PumpFunParams; +use super::pumpswap::PumpSwapParams; +use super::raydium_amm_v4::RaydiumAmmV4Params; +use super::raydium_cpmm::RaydiumCpmmParams; + +/// Concurrency + core binding config for parallel submit (precomputed at SDK init, one param on hot path). Uses Arc so no borrow of SwapParams. +#[derive(Clone)] +pub struct SenderConcurrencyConfig { + pub sender_thread_cores: Option>>, + pub effective_core_ids: Arc>, + pub max_sender_concurrency: usize, +} + +/// DEX 参数枚举 - 零开销抽象替代 Box +#[derive(Clone)] +pub enum DexParamEnum { + PumpFun(PumpFunParams), + PumpSwap(PumpSwapParams), + Bonk(BonkParams), + RaydiumCpmm(RaydiumCpmmParams), + RaydiumAmmV4(RaydiumAmmV4Params), + MeteoraDammV2(MeteoraDammV2Params), +} + +impl DexParamEnum { + /// 获取内部参数的 Any 引用,用于向后兼容的类型检查 + #[inline] + pub fn as_any(&self) -> &dyn std::any::Any { + match self { + DexParamEnum::PumpFun(p) => p, + DexParamEnum::PumpSwap(p) => p, + DexParamEnum::Bonk(p) => p, + DexParamEnum::RaydiumCpmm(p) => p, + DexParamEnum::RaydiumAmmV4(p) => p, + DexParamEnum::MeteoraDammV2(p) => p, + } + } +} + +/// Swap parameters +#[derive(Clone)] +pub struct SwapParams { + pub rpc: Option>, + pub payer: Arc, + pub trade_type: TradeType, + pub input_mint: Pubkey, + pub input_token_program: Option, + pub output_mint: Pubkey, + pub output_token_program: Option, + pub input_amount: Option, + pub slippage_basis_points: Option, + pub address_lookup_table_account: Option, + pub recent_blockhash: Option, + pub wait_tx_confirmed: bool, + pub protocol_params: DexParamEnum, + pub open_seed_optimize: bool, + /// Arc> so cloning from infrastructure is a single Arc clone. + pub swqos_clients: Arc>>, + pub middleware_manager: Option>, + pub durable_nonce: Option, + pub with_tip: bool, + pub create_input_mint_ata: bool, + pub close_input_mint_ata: bool, + pub create_output_mint_ata: bool, + pub close_output_mint_ata: bool, + pub fixed_output_amount: Option, + pub gas_fee_strategy: GasFeeStrategy, + pub simulate: bool, + /// Whether to output SDK logs (from TradeConfig.log_enabled). + pub log_enabled: bool, + /// Use dedicated sender threads (internal; set via client.with_dedicated_sender_threads()). + pub use_dedicated_sender_threads: bool, + /// Core indices for dedicated sender threads (from TradeConfig.sender_thread_cores). Arc avoids cloning the Vec on hot path. + pub sender_thread_cores: Option>>, + /// Precomputed at SDK init: min(swqos_count, 2/3*cores). Avoids get_core_ids() on trade hot path. + pub max_sender_concurrency: usize, + /// Precomputed at SDK init: first max_sender_concurrency CoreIds for job affinity. Arc clone only. + pub effective_core_ids: Arc>, + /// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). When false, skip filter for lower latency. + pub check_min_tip: bool, + /// Optional event receive time in microseconds (same scale as sol-parser-sdk clock::now_micros). Used as timing start when log_enabled. + pub grpc_recv_us: Option, + /// Use exact SOL amount instructions (buy_exact_sol_in for PumpFun, buy_exact_quote_in for PumpSwap). + /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens. + /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input. + /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes. + pub use_exact_sol_amount: Option, +} + +impl SwapParams { + /// One struct for execute_parallel: merges sender_thread_cores, effective_core_ids, max_sender_concurrency. Arc clone only. + #[inline] + pub fn sender_concurrency_config(&self) -> SenderConcurrencyConfig { + SenderConcurrencyConfig { + sender_thread_cores: self.sender_thread_cores.clone(), + effective_core_ids: self.effective_core_ids.clone(), + max_sender_concurrency: self.max_sender_concurrency, + } + } +} + +impl std::fmt::Debug for SwapParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SwapParams: ...") + } +} diff --git a/src/trading/core/params/meteora_damm_v2.rs b/src/trading/core/params/meteora_damm_v2.rs new file mode 100644 index 0000000..9f4e227 --- /dev/null +++ b/src/trading/core/params/meteora_damm_v2.rs @@ -0,0 +1,67 @@ +use crate::common::SolanaRpcClient; +use solana_sdk::pubkey::Pubkey; + +/// MeteoraDammV2 protocol specific parameters +/// Configuration parameters specific to Meteora Damm V2 trading protocol +#[derive(Clone)] +pub struct MeteoraDammV2Params { + pub pool: Pubkey, + pub token_a_vault: Pubkey, + pub token_b_vault: Pubkey, + pub token_a_mint: Pubkey, + pub token_b_mint: Pubkey, + pub token_a_program: Pubkey, + pub token_b_program: Pubkey, +} + +impl MeteoraDammV2Params { + pub fn new( + pool: Pubkey, + token_a_vault: Pubkey, + token_b_vault: Pubkey, + token_a_mint: Pubkey, + token_b_mint: Pubkey, + token_a_program: Pubkey, + token_b_program: Pubkey, + ) -> Self { + Self { + pool, + token_a_vault, + token_b_vault, + token_a_mint, + token_b_mint, + token_a_program, + token_b_program, + } + } + + pub async fn from_pool_address_by_rpc( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let pool_data = + crate::instruction::utils::meteora_damm_v2::fetch_pool(rpc, pool_address).await?; + let mint_accounts = rpc + .get_multiple_accounts(&[pool_data.token_a_mint, pool_data.token_b_mint]) + .await?; + let token_a_program = mint_accounts + .get(0) + .and_then(|a| a.as_ref()) + .map(|a| a.owner) + .ok_or_else(|| anyhow::anyhow!("Token A mint account not found"))?; + let token_b_program = mint_accounts + .get(1) + .and_then(|a| a.as_ref()) + .map(|a| a.owner) + .ok_or_else(|| anyhow::anyhow!("Token B mint account not found"))?; + Ok(Self { + pool: *pool_address, + token_a_vault: pool_data.token_a_vault, + token_b_vault: pool_data.token_b_vault, + token_a_mint: pool_data.token_a_mint, + token_b_mint: pool_data.token_b_mint, + token_a_program, + token_b_program, + }) + } +} diff --git a/src/trading/core/params/mod.rs b/src/trading/core/params/mod.rs new file mode 100644 index 0000000..3799895 --- /dev/null +++ b/src/trading/core/params/mod.rs @@ -0,0 +1,17 @@ +//! DEX protocol parameter types and [`SwapParams`]. + +mod bonk; +mod dex_swap; +mod meteora_damm_v2; +mod pumpfun; +mod pumpswap; +mod raydium_amm_v4; +mod raydium_cpmm; + +pub use bonk::BonkParams; +pub use dex_swap::{DexParamEnum, SenderConcurrencyConfig, SwapParams}; +pub use meteora_damm_v2::MeteoraDammV2Params; +pub use pumpfun::PumpFunParams; +pub use pumpswap::PumpSwapParams; +pub use raydium_amm_v4::RaydiumAmmV4Params; +pub use raydium_cpmm::RaydiumCpmmParams; diff --git a/src/trading/core/params/pumpfun.rs b/src/trading/core/params/pumpfun.rs new file mode 100644 index 0000000..c146eae --- /dev/null +++ b/src/trading/core/params/pumpfun.rs @@ -0,0 +1,240 @@ +use crate::common::bonding_curve::BondingCurveAccount; +use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id; +use crate::common::SolanaRpcClient; +use crate::instruction::utils::pumpfun::reconcile_mayhem_mode_for_trade; +use solana_sdk::pubkey::Pubkey; +use std::sync::Arc; + + +/// PumpFun protocol specific parameters +/// Configuration parameters specific to PumpFun trading protocol. +/// +/// **Creator vault**: Pump buy/sell pass `creator_vault` = `PDA(["creator-vault", authority])`. +/// Usually `authority` is [`BondingCurveAccount::creator`]; with **Creator Rewards Sharing** it is +/// `fee_sharing_config_pda(mint)` (see [`fetch_fee_sharing_creator_vault_if_active`](crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active)). +/// Keep `bonding_curve.creator` in sync with chain; ix building uses [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing). +#[derive(Clone)] +pub struct PumpFunParams { + pub bonding_curve: Arc, + pub associated_bonding_curve: Pubkey, + /// Resolved by [`resolve_creator_vault_for_ix_with_fee_sharing`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing): ix vault when it matches `PDA(creator)`, fee-sharing vault, or RPC hint. + pub creator_vault: Pubkey, + /// `Some(PDA(["creator-vault", fee_sharing_config]))` when pump-fees `SharingConfig` is **Active**; set by `from_mint_by_rpc` / [`refresh_fee_sharing_creator_vault_from_rpc`](Self::refresh_fee_sharing_creator_vault_from_rpc). + pub fee_sharing_creator_vault_if_active: Option, + pub token_program: Pubkey, + /// Whether to close token account when selling, only effective during sell operations + pub close_token_account_when_sell: Option, + /// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。 + /// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。 + pub fee_recipient: Pubkey, +} + +impl PumpFunParams { + pub fn immediate_sell( + creator_vault: Pubkey, + token_program: Pubkey, + close_token_account_when_sell: bool, + ) -> Self { + Self { + bonding_curve: Arc::new(BondingCurveAccount { ..Default::default() }), + associated_bonding_curve: Pubkey::default(), + creator_vault: creator_vault, + fee_sharing_creator_vault_if_active: None, + token_program: token_program, + close_token_account_when_sell: Some(close_token_account_when_sell), + fee_recipient: Pubkey::default(), + } + } + + /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event + /// so that sell instructions include the correct remaining accounts for cashback. + /// `mayhem_mode`: `Some` when known from Create/Trade event (`is_mayhem_mode` / `mayhem_mode`). + /// `None` falls back to detecting Mayhem via reserved fee recipient pubkeys only (not AMM protocol fee accounts). + pub fn from_dev_trade( + mint: Pubkey, + token_amount: u64, + max_sol_cost: u64, + creator: Pubkey, + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + creator_vault: Pubkey, + close_token_account_when_sell: Option, + fee_recipient: Pubkey, + token_program: Pubkey, + is_cashback_coin: bool, + mayhem_mode: Option, + ) -> Self { + let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient); + let bonding_curve_account = BondingCurveAccount::from_dev_trade( + bonding_curve, + &mint, + token_amount, + max_sol_cost, + creator, + is_mayhem_mode, + is_cashback_coin, + ); + let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( + &bonding_curve_account.creator, + creator_vault, + &mint, + None, + ) + .or_else(|| { + crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator) + }) + .unwrap_or_default(); + Self { + bonding_curve: Arc::new(bonding_curve_account), + associated_bonding_curve: associated_bonding_curve, + creator_vault: creator_vault_resolved, + fee_sharing_creator_vault_if_active: None, + close_token_account_when_sell: close_token_account_when_sell, + token_program: token_program, + fee_recipient, + } + } + + /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event + /// so that sell instructions include the correct remaining accounts for cashback. + /// + /// `mayhem_mode`: + /// - **`Some(v)`**:优先采用 gRPC / `tradeEvent`,但与 **`fee_recipient` 所属池**(Mayhem vs 普通,见 pump-public-docs)不一致时,以 fee 地址为准纠偏,避免链上 `NotAuthorized`。 + /// - **`None`**:用 `fee_recipient` 是否落在 Mayhem 静态列表推断。 + pub fn from_trade( + bonding_curve: Pubkey, + associated_bonding_curve: Pubkey, + mint: Pubkey, + creator: Pubkey, + creator_vault: Pubkey, + virtual_token_reserves: u64, + virtual_sol_reserves: u64, + real_token_reserves: u64, + real_sol_reserves: u64, + close_token_account_when_sell: Option, + fee_recipient: Pubkey, + token_program: Pubkey, + is_cashback_coin: bool, + mayhem_mode: Option, + ) -> Self { + let is_mayhem_mode = reconcile_mayhem_mode_for_trade(mayhem_mode, &fee_recipient); + let bonding_curve = BondingCurveAccount::from_trade( + bonding_curve, + mint, + creator, + virtual_token_reserves, + virtual_sol_reserves, + real_token_reserves, + real_sol_reserves, + is_mayhem_mode, + is_cashback_coin, + ); + let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( + &bonding_curve.creator, + creator_vault, + &mint, + None, + ) + .or_else(|| { + crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator) + }) + .unwrap_or_default(); + Self { + bonding_curve: Arc::new(bonding_curve), + associated_bonding_curve: associated_bonding_curve, + creator_vault: creator_vault_resolved, + fee_sharing_creator_vault_if_active: None, + close_token_account_when_sell: close_token_account_when_sell, + token_program: token_program, + fee_recipient, + } + } + + pub async fn from_mint_by_rpc( + rpc: &SolanaRpcClient, + mint: &Pubkey, + ) -> Result { + let account = + crate::instruction::utils::pumpfun::fetch_bonding_curve_account(rpc, mint).await?; + let mint_account = rpc.get_account(&mint).await?; + let bonding_curve = BondingCurveAccount { + discriminator: 0, + account: account.1, + virtual_token_reserves: account.0.virtual_token_reserves, + virtual_sol_reserves: account.0.virtual_sol_reserves, + real_token_reserves: account.0.real_token_reserves, + real_sol_reserves: account.0.real_sol_reserves, + token_total_supply: account.0.token_total_supply, + complete: account.0.complete, + creator: account.0.creator, + is_mayhem_mode: account.0.is_mayhem_mode, + is_cashback_coin: account.0.is_cashback_coin, + }; + let associated_bonding_curve = get_associated_token_address_with_program_id( + &bonding_curve.account, + mint, + &mint_account.owner, + ); + let fee_sharing_creator_vault_if_active = + crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint) + .await?; + let creator_vault = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( + &bonding_curve.creator, + Pubkey::default(), + mint, + fee_sharing_creator_vault_if_active, + ) + .or_else(|| crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)) + .unwrap_or_default(); + Ok(Self { + bonding_curve: Arc::new(bonding_curve), + associated_bonding_curve: associated_bonding_curve, + creator_vault, + fee_sharing_creator_vault_if_active, + close_token_account_when_sell: None, + token_program: mint_account.owner, + fee_recipient: Pubkey::default(), + }) + } + + /// One `getAccount` on pump-fees `SharingConfig` + re-resolves [`Self::creator_vault`]. Call before sell + /// when params come from gRPC/cache so migrated fee-sharing mints do not hit Anchor 2006. + pub async fn refresh_fee_sharing_creator_vault_from_rpc( + mut self, + rpc: &SolanaRpcClient, + mint: &Pubkey, + ) -> Result { + self.fee_sharing_creator_vault_if_active = + crate::instruction::utils::pumpfun::fetch_fee_sharing_creator_vault_if_active(rpc, mint) + .await?; + let c = self.bonding_curve.creator; + if let Some(v) = + crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix_with_fee_sharing( + &c, + self.creator_vault, + mint, + self.fee_sharing_creator_vault_if_active, + ) + { + self.creator_vault = v; + } + Ok(self) + } + + /// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`]. + #[inline] + pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self { + self.creator_vault = creator_vault; + self + } + + /// Override fee-sharing vault hint (e.g. from an off-chain indexer). `None` clears the hint. + #[inline] + pub fn with_fee_sharing_creator_vault_if_active( + mut self, + fee_sharing_creator_vault_if_active: Option, + ) -> Self { + self.fee_sharing_creator_vault_if_active = fee_sharing_creator_vault_if_active; + self + } +} diff --git a/src/trading/core/params/pumpswap.rs b/src/trading/core/params/pumpswap.rs new file mode 100644 index 0000000..11d75ce --- /dev/null +++ b/src/trading/core/params/pumpswap.rs @@ -0,0 +1,220 @@ +use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id; +use crate::common::SolanaRpcClient; +use crate::instruction::utils::pumpswap::accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP; +use solana_sdk::pubkey::Pubkey; + +/// PumpSwap Protocol Specific Parameters +/// +/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information, +/// token configuration, and transaction amounts. +/// +/// **Performance Note**: If these parameters are not provided, the system will attempt to +/// retrieve the relevant information from RPC, which will increase transaction time. +/// For optimal performance, it is recommended to provide all necessary parameters in advance. +#[derive(Clone)] +pub struct PumpSwapParams { + /// Liquidity pool address + pub pool: Pubkey, + /// Base token mint address + /// The mint account address of the base token in the trading pair + pub base_mint: Pubkey, + /// Quote token mint address + /// The mint account address of the quote token in the trading pair, usually SOL or USDC + pub quote_mint: Pubkey, + /// Pool base token account + pub pool_base_token_account: Pubkey, + /// Pool quote token account + pub pool_quote_token_account: Pubkey, + /// Base token reserves in the pool + pub pool_base_token_reserves: u64, + /// Quote token reserves in the pool + pub pool_quote_token_reserves: u64, + /// Coin creator vault ATA + pub coin_creator_vault_ata: Pubkey, + /// Coin creator vault authority + pub coin_creator_vault_authority: Pubkey, + /// Token program ID + pub base_token_program: Pubkey, + /// Quote token program ID + pub quote_token_program: Pubkey, + /// Whether the pool is in mayhem mode + pub is_mayhem_mode: bool, + /// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap + /// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()` + /// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053). + pub coin_creator: Pubkey, + /// Whether the pool's coin has cashback enabled + pub is_cashback_coin: bool, + /// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell + /// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS) + /// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket. + /// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps). + pub cashback_fee_basis_points: u64, +} + +impl PumpSwapParams { + pub fn new( + pool: Pubkey, + base_mint: Pubkey, + quote_mint: Pubkey, + pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, + pool_base_token_reserves: u64, + pool_quote_token_reserves: u64, + coin_creator_vault_ata: Pubkey, + coin_creator_vault_authority: Pubkey, + base_token_program: Pubkey, + quote_token_program: Pubkey, + fee_recipient: Pubkey, + coin_creator: Pubkey, + is_cashback_coin: bool, + cashback_fee_basis_points: u64, + ) -> Self { + let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP; + Self { + pool, + base_mint, + quote_mint, + pool_base_token_account, + pool_quote_token_account, + pool_base_token_reserves, + pool_quote_token_reserves, + coin_creator_vault_ata, + coin_creator_vault_authority, + base_token_program, + quote_token_program, + is_mayhem_mode, + coin_creator, + is_cashback_coin, + cashback_fee_basis_points, + } + } + + /// Fast-path constructor for building PumpSwap parameters directly from decoded + /// trade/event data and the accompanying instruction accounts, avoiding RPC + /// lookups and associated latency. Token program IDs should be sourced from + /// the instruction accounts themselves to respect Token Program vs Token-2022 + /// differences. + /// + /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` + /// from the event so that buy/sell instructions include the correct remaining + /// accounts for cashback. + pub fn from_trade( + pool: Pubkey, + base_mint: Pubkey, + quote_mint: Pubkey, + pool_base_token_account: Pubkey, + pool_quote_token_account: Pubkey, + pool_base_token_reserves: u64, + pool_quote_token_reserves: u64, + coin_creator_vault_ata: Pubkey, + coin_creator_vault_authority: Pubkey, + base_token_program: Pubkey, + quote_token_program: Pubkey, + fee_recipient: Pubkey, + coin_creator: Pubkey, + is_cashback_coin: bool, + cashback_fee_basis_points: u64, + ) -> Self { + Self::new( + pool, + base_mint, + quote_mint, + pool_base_token_account, + pool_quote_token_account, + pool_base_token_reserves, + pool_quote_token_reserves, + coin_creator_vault_ata, + coin_creator_vault_authority, + base_token_program, + quote_token_program, + fee_recipient, + coin_creator, + is_cashback_coin, + cashback_fee_basis_points, + ) + } + + pub async fn from_mint_by_rpc( + rpc: &SolanaRpcClient, + mint: &Pubkey, + ) -> Result { + if let Ok((pool_address, _)) = + crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await + { + Self::from_pool_address_by_rpc(rpc, &pool_address).await + } else if let Ok((pool_address, _)) = + crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await + { + Self::from_pool_address_by_rpc(rpc, &pool_address).await + } else { + return Err(anyhow::anyhow!("No pool found for mint")); + } + } + + pub async fn from_pool_address_by_rpc( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let pool_data = crate::instruction::utils::pumpswap::fetch_pool(rpc, pool_address).await?; + Self::from_pool_data(rpc, pool_address, &pool_data).await + } + + /// Build params from an already-decoded Pool, only fetching token balances. + /// + /// Saves 1 RPC `getAccount` call vs `from_pool_address_by_rpc` when pool data + /// is already available (e.g. from `pumpswap::find_by_mint` which returns the + /// decoded Pool). + pub async fn from_pool_data( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + pool_data: &crate::instruction::utils::pumpswap_types::Pool, + ) -> Result { + let (pool_base_token_reserves, pool_quote_token_reserves) = + crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?; + let creator = pool_data.coin_creator; + let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata( + creator, + pool_data.quote_mint, + ); + let coin_creator_vault_authority = + crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator); + + let base_token_program_ata = get_associated_token_address_with_program_id( + pool_address, + &pool_data.base_mint, + &crate::constants::TOKEN_PROGRAM, + ); + let quote_token_program_ata = get_associated_token_address_with_program_id( + pool_address, + &pool_data.quote_mint, + &crate::constants::TOKEN_PROGRAM, + ); + + Ok(Self { + pool: *pool_address, + base_mint: pool_data.base_mint, + quote_mint: pool_data.quote_mint, + pool_base_token_account: pool_data.pool_base_token_account, + pool_quote_token_account: pool_data.pool_quote_token_account, + pool_base_token_reserves, + pool_quote_token_reserves, + coin_creator_vault_ata, + coin_creator_vault_authority, + base_token_program: if pool_data.pool_base_token_account == base_token_program_ata { + crate::constants::TOKEN_PROGRAM + } else { + crate::constants::TOKEN_PROGRAM_2022 + }, + is_cashback_coin: pool_data.is_cashback_coin, + quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata { + crate::constants::TOKEN_PROGRAM + } else { + crate::constants::TOKEN_PROGRAM_2022 + }, + is_mayhem_mode: pool_data.is_mayhem_mode, + coin_creator: pool_data.coin_creator, + cashback_fee_basis_points: 0, + }) + } +} diff --git a/src/trading/core/params/raydium_amm_v4.rs b/src/trading/core/params/raydium_amm_v4.rs new file mode 100644 index 0000000..2975e21 --- /dev/null +++ b/src/trading/core/params/raydium_amm_v4.rs @@ -0,0 +1,54 @@ +use crate::common::SolanaRpcClient; +use crate::trading::common::get_multi_token_balances; +use solana_sdk::pubkey::Pubkey; + +/// RaydiumCpmm protocol specific parameters +/// Configuration parameters specific to Raydium CPMM trading protocol +#[derive(Clone)] +pub struct RaydiumAmmV4Params { + /// AMM pool address + pub amm: Pubkey, + /// Base token (coin) mint address + pub coin_mint: Pubkey, + /// Quote token (pc) mint address + pub pc_mint: Pubkey, + /// Pool's coin token account address + pub token_coin: Pubkey, + /// Pool's pc token account address + pub token_pc: Pubkey, + /// Current coin reserve amount in the pool + pub coin_reserve: u64, + /// Current pc reserve amount in the pool + pub pc_reserve: u64, +} + +impl RaydiumAmmV4Params { + pub fn new( + amm: Pubkey, + coin_mint: Pubkey, + pc_mint: Pubkey, + token_coin: Pubkey, + token_pc: Pubkey, + coin_reserve: u64, + pc_reserve: u64, + ) -> Self { + Self { amm, coin_mint, pc_mint, token_coin, token_pc, coin_reserve, pc_reserve } + } + pub async fn from_amm_address_by_rpc( + rpc: &SolanaRpcClient, + amm: Pubkey, + ) -> Result { + let amm_info = crate::instruction::utils::raydium_amm_v4::fetch_amm_info(rpc, amm).await?; + let (coin_reserve, pc_reserve) = + get_multi_token_balances(rpc, &amm_info.token_coin, &amm_info.token_pc).await?; + Ok(Self { + amm, + coin_mint: amm_info.coin_mint, + pc_mint: amm_info.pc_mint, + token_coin: amm_info.token_coin, + token_pc: amm_info.token_pc, + coin_reserve, + pc_reserve, + }) + } +} diff --git a/src/trading/core/params/raydium_cpmm.rs b/src/trading/core/params/raydium_cpmm.rs new file mode 100644 index 0000000..28d4b8a --- /dev/null +++ b/src/trading/core/params/raydium_cpmm.rs @@ -0,0 +1,89 @@ +use crate::common::SolanaRpcClient; +use solana_sdk::pubkey::Pubkey; + +/// RaydiumCpmm protocol specific parameters +/// Configuration parameters specific to Raydium CPMM trading protocol +#[derive(Clone)] +pub struct RaydiumCpmmParams { + /// Pool address + pub pool_state: Pubkey, + /// Amm config address + pub amm_config: Pubkey, + /// Base token mint address + pub base_mint: Pubkey, + /// Quote token mint address + pub quote_mint: Pubkey, + /// Base token reserve amount in the pool + pub base_reserve: u64, + /// Quote token reserve amount in the pool + pub quote_reserve: u64, + /// Base token vault address + pub base_vault: Pubkey, + /// Quote token vault address + pub quote_vault: Pubkey, + /// Base token program ID + pub base_token_program: Pubkey, + /// Quote token program ID + pub quote_token_program: Pubkey, + /// Observation state account + pub observation_state: Pubkey, +} + +impl RaydiumCpmmParams { + pub fn from_trade( + pool_state: Pubkey, + amm_config: Pubkey, + input_token_mint: Pubkey, + output_token_mint: Pubkey, + input_vault: Pubkey, + output_vault: Pubkey, + input_token_program: Pubkey, + output_token_program: Pubkey, + observation_state: Pubkey, + base_reserve: u64, + quote_reserve: u64, + ) -> Self { + Self { + pool_state: pool_state, + amm_config: amm_config, + base_mint: input_token_mint, + quote_mint: output_token_mint, + base_reserve: base_reserve, + quote_reserve: quote_reserve, + base_vault: input_vault, + quote_vault: output_vault, + base_token_program: input_token_program, + quote_token_program: output_token_program, + observation_state: observation_state, + } + } + + pub async fn from_pool_address_by_rpc( + rpc: &SolanaRpcClient, + pool_address: &Pubkey, + ) -> Result { + let pool = + crate::instruction::utils::raydium_cpmm::fetch_pool_state(rpc, pool_address).await?; + let (token0_balance, token1_balance) = + crate::instruction::utils::raydium_cpmm::get_pool_token_balances( + rpc, + pool_address, + &pool.token0_mint, + &pool.token1_mint, + ) + .await?; + Ok(Self { + pool_state: *pool_address, + amm_config: pool.amm_config, + base_mint: pool.token0_mint, + quote_mint: pool.token1_mint, + base_reserve: token0_balance, + quote_reserve: token1_balance, + base_vault: pool.token0_vault, + quote_vault: pool.token1_vault, + base_token_program: pool.token0_program, + quote_token_program: pool.token1_program, + observation_state: pool.observation_key, + }) + } +}