Skip to main content

sol_trade_sdk/client/
mod.rs

1//! High-level [`TradingClient`], [`TradingInfrastructure`], and trade parameter types.
2
3use crate::common::nonce_cache::DurableNonceInfo;
4use crate::common::sdk_log;
5use crate::common::GasFeeStrategy;
6use crate::common::SolanaRpcClient;
7use crate::common::{InfrastructureConfig, TradeConfig};
8#[cfg(feature = "perf-trace")]
9use crate::constants::trade::trade::DEFAULT_SLIPPAGE;
10use crate::constants::SOL_TOKEN_ACCOUNT;
11use crate::constants::USD1_TOKEN_ACCOUNT;
12use crate::constants::USDC_TOKEN_ACCOUNT;
13use crate::constants::WSOL_TOKEN_ACCOUNT;
14use crate::swqos::common::TradeError;
15use crate::swqos::SwqosClient;
16use crate::swqos::SwqosConfig;
17use crate::swqos::SwqosType;
18use crate::swqos::TradeType;
19use crate::trading::core::params::BonkParams;
20use crate::trading::core::params::DexParamEnum;
21use crate::trading::core::params::MeteoraDammV2Params;
22use crate::trading::core::params::PumpFunParams;
23use crate::trading::core::params::PumpSwapParams;
24use crate::trading::core::params::RaydiumAmmV4Params;
25use crate::trading::core::params::RaydiumCpmmParams;
26use crate::trading::factory::DexType;
27use crate::trading::MiddlewareManager;
28use crate::trading::SwapParams;
29use crate::trading::TradeFactory;
30use parking_lot::Mutex;
31use rustls::crypto::{ring::default_provider, CryptoProvider};
32use solana_sdk::hash::Hash;
33use solana_sdk::message::AddressLookupTableAccount;
34use solana_sdk::signer::Signer;
35use solana_sdk::{pubkey::Pubkey, signature::Keypair, signature::Signature};
36use std::sync::Arc;
37#[allow(unused_imports)]
38use tracing::{debug, error, info, warn};
39
40/// Single place to validate that protocol params match the given DEX type (avoids duplicate match in buy/sell).
41#[inline(always)]
42fn validate_protocol_params(dex_type: DexType, params: &DexParamEnum) -> bool {
43    match dex_type {
44        DexType::PumpFun => params.as_any().downcast_ref::<PumpFunParams>().is_some(),
45        DexType::PumpSwap => params.as_any().downcast_ref::<PumpSwapParams>().is_some(),
46        DexType::Bonk => params.as_any().downcast_ref::<BonkParams>().is_some(),
47        DexType::RaydiumCpmm => params.as_any().downcast_ref::<RaydiumCpmmParams>().is_some(),
48        DexType::RaydiumAmmV4 => params.as_any().downcast_ref::<RaydiumAmmV4Params>().is_some(),
49        DexType::MeteoraDammV2 => params.as_any().downcast_ref::<MeteoraDammV2Params>().is_some(),
50    }
51}
52
53#[inline]
54fn normalize_swqos_configs(rpc_url: &str, configs: &[SwqosConfig]) -> Vec<SwqosConfig> {
55    let mut out = configs.to_vec();
56    if !out.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)) {
57        out.push(SwqosConfig::Default(rpc_url.to_string()));
58    }
59    out
60}
61
62/// 按 mint 查找池地址(通用入口,根据 DEX 类型分发,仅 PumpSwap 等已实现的类型会走优化路径)。
63///
64/// * `dex_type`:PumpSwap 时先走 PDA 再回退 getProgramAccounts,其他类型返回未实现错误。
65pub async fn find_pool_by_mint(
66    rpc: &SolanaRpcClient,
67    mint: &Pubkey,
68    dex_type: DexType,
69) -> Result<Pubkey, anyhow::Error> {
70    match dex_type {
71        DexType::PumpSwap => crate::instruction::utils::pumpswap::find_pool(rpc, mint).await,
72        _ => Err(anyhow::anyhow!("find_pool_by_mint not implemented for {:?}", dex_type)),
73    }
74}
75
76/// Type of the token to buy
77#[derive(Clone, PartialEq)]
78pub enum TradeTokenType {
79    SOL,
80    WSOL,
81    USD1,
82    USDC,
83}
84
85/// Account lifecycle policy for high-level trade requests.
86///
87/// This replaces low-level flags such as `create_input_token_ata`,
88/// `close_input_token_ata`, `create_mint_ata`, and `create_output_token_ata`.
89/// Use this when calling [`TradingClient::buy_simple`] or [`TradingClient::sell_simple`].
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum AccountPolicy {
92    /// SDK chooses a practical default for the protocol and token route.
93    ///
94    /// Buy: create the target token ATA because the wallet usually needs a place
95    /// to receive the bought token.
96    ///
97    /// Sell: create the output ATA only when receiving an SPL token such as USDC
98    /// or WSOL. Receiving native SOL does not need an ATA.
99    Auto,
100    /// Keep the transaction small for sniping/arbitrage; assume hot accounts are prepared.
101    ///
102    /// This avoids ATA create/close instructions in the trade transaction. It is
103    /// the recommended policy for latency-sensitive bots that pre-create WSOL
104    /// and token ATAs outside the hot path.
105    HotPathMinimal,
106    /// Include ATA create instructions when the route needs them.
107    ///
108    /// This is more convenient for normal trading but can make PumpFun V2 and
109    /// other large transactions exceed Solana packet size limits unless an ALT
110    /// is supplied or the transaction builder can compact optional instructions.
111    CreateMissing,
112    /// Do not create or close token accounts in the trade transaction.
113    ///
114    /// Use this when the caller has already prepared all required ATAs and wants
115    /// deterministic instruction layout. Unlike `HotPathMinimal`, this name is
116    /// intended for correctness/readability rather than bot latency.
117    AssumePrepared,
118}
119
120/// High-level buy sizing intent.
121///
122/// This replaces `input_token_amount`, `fixed_output_token_amount`, and
123/// `use_exact_sol_amount` in [`TradeBuyParams`].
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum BuyAmount {
126    /// Spend this exact quote amount and apply slippage to minimum output.
127    ///
128    /// Example: spend exactly `0.1 SOL` or exactly `100 USDC` worth of quote
129    /// token; if the received token amount is below the slippage-adjusted
130    /// minimum, the transaction fails.
131    ExactInput(u64),
132    /// Buy an exact token amount, using `max_input_amount` as the quote budget.
133    ///
134    /// Example: buy exactly `1_000_000` token base units, but fail if the quote
135    /// cost would exceed `max_input_amount`.
136    ExactOutput { output_amount: u64, max_input_amount: u64 },
137    /// Regular PumpFun/PumpSwap buy: estimate output from `quote_amount` and apply slippage to max quote cost.
138    ///
139    /// This maps to `use_exact_sol_amount = Some(false)` in the old API. It is
140    /// useful for bots that want to know the intended token output from the SDK
141    /// calculation while letting the chain fail if the max quote budget is
142    /// exceeded.
143    WithMaxInput { quote_amount: u64 },
144}
145
146/// High-level sell sizing intent.
147///
148/// This replaces `input_token_amount` and `fixed_output_token_amount` in
149/// [`TradeSellParams`].
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum SellAmount {
152    /// Sell this exact token amount and apply slippage to minimum output.
153    ///
154    /// Example: sell exactly `1_000_000` token base units and fail if the
155    /// received quote amount is below the slippage-adjusted minimum.
156    ExactInput(u64),
157    /// Exact-output sell where supported; `max_input_amount` is the token budget.
158    ///
159    /// Example: receive exactly `0.1 SOL` or `100 USDC`, but spend no more than
160    /// `max_input_amount` token base units.
161    ExactOutput { output_amount: u64, max_input_amount: u64 },
162}
163
164/// Simpler buy request that describes trade intent instead of low-level ATA flags.
165///
166/// Prefer constructing this with [`SimpleBuyParams::new`] or
167/// [`SimpleBuyParams::with_durable_nonce`], then optionally set slippage,
168/// account policy, ALT, simulation, or confirmation flags through builder-style
169/// methods.
170#[derive(Clone)]
171pub struct SimpleBuyParams {
172    /// DEX/protocol to route through, such as `DexType::PumpFun`.
173    pub dex_type: DexType,
174    /// Quote token used to pay for the buy: `SOL`, `WSOL`, `USDC`, or `USD1`.
175    ///
176    /// For PumpFun SOL-paired coins, use `TradeTokenType::SOL` for the normal
177    /// fast path even when parser data reports WSOL as the quote sentinel. The
178    /// SDK will prefer V1; choose `WSOL` only when intentionally spending an
179    /// existing WSOL ATA through V2.
180    pub pay_with: TradeTokenType,
181    /// Mint address of the token being bought.
182    pub mint: Pubkey,
183    /// Buy sizing intent. See [`BuyAmount`].
184    pub amount: BuyAmount,
185    /// Optional slippage in basis points. `100` means 1%.
186    pub slippage_basis_points: Option<u64>,
187    /// Recent blockhash for non-nonce transactions.
188    ///
189    /// The SDK intentionally does not fetch blockhash on the hot path. Use
190    /// [`SimpleBuyParams::with_durable_nonce`] instead when submitting multiple
191    /// SWQoS lanes against a pinned nonce.
192    pub recent_blockhash: Option<Hash>,
193    /// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
194    pub extension_params: DexParamEnum,
195    /// Compute unit price/limit and relay tip configuration.
196    pub gas_fee_strategy: GasFeeStrategy,
197    /// ATA creation/close behavior. See [`AccountPolicy`].
198    pub account_policy: AccountPolicy,
199    /// Optional Address Lookup Table to reduce transaction size.
200    pub address_lookup_table_account: Option<AddressLookupTableAccount>,
201    /// Wait until the transaction is confirmed before returning.
202    pub wait_tx_confirmed: bool,
203    /// Fast-submit mode only: wait for every SWQoS route's submit response so all
204    /// signatures can be returned.
205    pub wait_for_all_submits: bool,
206    /// Durable nonce info. Mutually exclusive with `recent_blockhash`.
207    pub durable_nonce: Option<DurableNonceInfo>,
208    /// Build and simulate the transaction instead of submitting it.
209    pub simulate: bool,
210    /// Optional upstream receive timestamp in microseconds for latency tracing.
211    pub grpc_recv_us: Option<i64>,
212}
213
214/// Simpler sell request that describes trade intent instead of low-level ATA flags.
215///
216/// Prefer constructing this with [`SimpleSellParams::new`] or
217/// [`SimpleSellParams::with_durable_nonce`].
218#[derive(Clone)]
219pub struct SimpleSellParams {
220    /// DEX/protocol to route through, such as `DexType::PumpFun`.
221    pub dex_type: DexType,
222    /// Quote token to receive from the sell: `SOL`, `WSOL`, `USDC`, or `USD1`.
223    pub receive_as: TradeTokenType,
224    /// Mint address of the token being sold.
225    pub mint: Pubkey,
226    /// Sell sizing intent. See [`SellAmount`].
227    pub amount: SellAmount,
228    /// Optional slippage in basis points. `100` means 1%.
229    pub slippage_basis_points: Option<u64>,
230    /// Recent blockhash for non-nonce transactions.
231    pub recent_blockhash: Option<Hash>,
232    /// Protocol-specific parameters, for example `DexParamEnum::PumpFun(...)`.
233    pub extension_params: DexParamEnum,
234    /// Compute unit price/limit and relay tip configuration.
235    pub gas_fee_strategy: GasFeeStrategy,
236    /// ATA creation/close behavior. See [`AccountPolicy`].
237    pub account_policy: AccountPolicy,
238    /// Optional Address Lookup Table to reduce transaction size.
239    pub address_lookup_table_account: Option<AddressLookupTableAccount>,
240    /// Wait until the transaction is confirmed before returning.
241    pub wait_tx_confirmed: bool,
242    /// Fast-submit mode only: wait for every SWQoS route's submit response so all
243    /// signatures can be returned.
244    pub wait_for_all_submits: bool,
245    /// Durable nonce info. Mutually exclusive with `recent_blockhash`.
246    pub durable_nonce: Option<DurableNonceInfo>,
247    /// Build and simulate the transaction instead of submitting it.
248    pub simulate: bool,
249    /// Whether to include relay tips for sell transactions.
250    pub with_tip: bool,
251    /// Optional upstream receive timestamp in microseconds for latency tracing.
252    pub grpc_recv_us: Option<i64>,
253}
254
255impl SimpleBuyParams {
256    /// Create a simple buy request using a recent blockhash.
257    ///
258    /// Defaults:
259    /// - `account_policy = AccountPolicy::Auto`
260    /// - `wait_tx_confirmed = false`
261    /// - `wait_for_all_submits = false`
262    /// - `simulate = false`
263    /// - no ALT
264    /// - no slippage override
265    pub fn new(
266        dex_type: DexType,
267        pay_with: TradeTokenType,
268        mint: Pubkey,
269        amount: BuyAmount,
270        extension_params: DexParamEnum,
271        recent_blockhash: Hash,
272        gas_fee_strategy: GasFeeStrategy,
273    ) -> Self {
274        Self {
275            dex_type,
276            pay_with,
277            mint,
278            amount,
279            slippage_basis_points: None,
280            recent_blockhash: Some(recent_blockhash),
281            extension_params,
282            gas_fee_strategy,
283            account_policy: AccountPolicy::Auto,
284            address_lookup_table_account: None,
285            wait_tx_confirmed: false,
286            wait_for_all_submits: false,
287            durable_nonce: None,
288            simulate: false,
289            grpc_recv_us: None,
290        }
291    }
292
293    /// Create a simple buy request using a durable nonce instead of a recent blockhash.
294    ///
295    /// Use this when sending through multiple SWQoS lanes with the same nonce.
296    pub fn with_durable_nonce(
297        dex_type: DexType,
298        pay_with: TradeTokenType,
299        mint: Pubkey,
300        amount: BuyAmount,
301        extension_params: DexParamEnum,
302        durable_nonce: DurableNonceInfo,
303        gas_fee_strategy: GasFeeStrategy,
304    ) -> Self {
305        Self {
306            durable_nonce: Some(durable_nonce),
307            recent_blockhash: None,
308            ..Self::new(
309                dex_type,
310                pay_with,
311                mint,
312                amount,
313                extension_params,
314                Hash::default(),
315                gas_fee_strategy,
316            )
317        }
318    }
319
320    /// Set slippage in basis points. `100` means 1%.
321    pub fn slippage_basis_points(mut self, value: u64) -> Self {
322        self.slippage_basis_points = Some(value);
323        self
324    }
325
326    /// Set account lifecycle behavior. Bots usually want `HotPathMinimal`;
327    /// normal integrations can keep the default `Auto`.
328    pub fn account_policy(mut self, value: AccountPolicy) -> Self {
329        self.account_policy = value;
330        self
331    }
332
333    /// Attach an Address Lookup Table to reduce transaction size.
334    pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
335        self.address_lookup_table_account = Some(value);
336        self
337    }
338
339    /// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
340    ///
341    /// This clears `recent_blockhash` because nonce transactions must use the
342    /// nonce value as their transaction blockhash.
343    pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
344        self.durable_nonce = Some(value);
345        self.recent_blockhash = None;
346        self
347    }
348
349    /// Wait for confirmation before returning.
350    pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
351        self.wait_tx_confirmed = value;
352        self
353    }
354
355    /// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
356    pub fn wait_for_all_submits(mut self, value: bool) -> Self {
357        self.wait_for_all_submits = value;
358        self
359    }
360
361    /// Simulate instead of submitting.
362    pub fn simulate(mut self, value: bool) -> Self {
363        self.simulate = value;
364        self
365    }
366
367    /// Attach upstream receive timestamp for latency tracing.
368    pub fn grpc_recv_us(mut self, value: i64) -> Self {
369        self.grpc_recv_us = Some(value);
370        self
371    }
372}
373
374impl SimpleSellParams {
375    /// Create a simple sell request using a recent blockhash.
376    ///
377    /// Defaults:
378    /// - `account_policy = AccountPolicy::Auto`
379    /// - `with_tip = true`
380    /// - `wait_tx_confirmed = false`
381    /// - `wait_for_all_submits = false`
382    /// - `simulate = false`
383    /// - no ALT
384    /// - no slippage override
385    pub fn new(
386        dex_type: DexType,
387        receive_as: TradeTokenType,
388        mint: Pubkey,
389        amount: SellAmount,
390        extension_params: DexParamEnum,
391        recent_blockhash: Hash,
392        gas_fee_strategy: GasFeeStrategy,
393    ) -> Self {
394        Self {
395            dex_type,
396            receive_as,
397            mint,
398            amount,
399            slippage_basis_points: None,
400            recent_blockhash: Some(recent_blockhash),
401            extension_params,
402            gas_fee_strategy,
403            account_policy: AccountPolicy::Auto,
404            address_lookup_table_account: None,
405            wait_tx_confirmed: false,
406            wait_for_all_submits: false,
407            durable_nonce: None,
408            simulate: false,
409            with_tip: true,
410            grpc_recv_us: None,
411        }
412    }
413
414    /// Create a simple sell request using a durable nonce instead of a recent blockhash.
415    pub fn with_durable_nonce(
416        dex_type: DexType,
417        receive_as: TradeTokenType,
418        mint: Pubkey,
419        amount: SellAmount,
420        extension_params: DexParamEnum,
421        durable_nonce: DurableNonceInfo,
422        gas_fee_strategy: GasFeeStrategy,
423    ) -> Self {
424        Self {
425            durable_nonce: Some(durable_nonce),
426            recent_blockhash: None,
427            ..Self::new(
428                dex_type,
429                receive_as,
430                mint,
431                amount,
432                extension_params,
433                Hash::default(),
434                gas_fee_strategy,
435            )
436        }
437    }
438
439    /// Set slippage in basis points. `100` means 1%.
440    pub fn slippage_basis_points(mut self, value: u64) -> Self {
441        self.slippage_basis_points = Some(value);
442        self
443    }
444
445    /// Set account lifecycle behavior. Bots usually want `HotPathMinimal`;
446    /// normal integrations can keep the default `Auto`.
447    pub fn account_policy(mut self, value: AccountPolicy) -> Self {
448        self.account_policy = value;
449        self
450    }
451
452    /// Attach an Address Lookup Table to reduce transaction size.
453    pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
454        self.address_lookup_table_account = Some(value);
455        self
456    }
457
458    /// Use a durable nonce instead of the recent blockhash passed to [`Self::new`].
459    ///
460    /// This clears `recent_blockhash` because nonce transactions must use the
461    /// nonce value as their transaction blockhash.
462    pub fn durable_nonce(mut self, value: DurableNonceInfo) -> Self {
463        self.durable_nonce = Some(value);
464        self.recent_blockhash = None;
465        self
466    }
467
468    /// Wait for confirmation before returning.
469    pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
470        self.wait_tx_confirmed = value;
471        self
472    }
473
474    /// In fast-submit mode, wait for all SWQoS submit responses and return every signature.
475    pub fn wait_for_all_submits(mut self, value: bool) -> Self {
476        self.wait_for_all_submits = value;
477        self
478    }
479
480    /// Simulate instead of submitting.
481    pub fn simulate(mut self, value: bool) -> Self {
482        self.simulate = value;
483        self
484    }
485
486    /// Enable or disable relay tips for sell transactions.
487    pub fn with_tip(mut self, value: bool) -> Self {
488        self.with_tip = value;
489        self
490    }
491
492    /// Attach upstream receive timestamp for latency tracing.
493    pub fn grpc_recv_us(mut self, value: i64) -> Self {
494        self.grpc_recv_us = Some(value);
495        self
496    }
497}
498
499/// Shared infrastructure components that can be reused across multiple wallets
500///
501/// This struct holds the expensive-to-initialize components (RPC client, SWQOS clients)
502/// that are wallet-independent and can be shared when only the trading wallet changes.
503pub struct TradingInfrastructure {
504    /// Shared RPC client for blockchain interactions
505    pub rpc: Arc<SolanaRpcClient>,
506    /// Shared SWQOS clients for transaction priority and routing. Arc<Vec<..>> so cloning into SwapParams is a single Arc clone.
507    pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
508    /// Configuration used to create this infrastructure
509    pub config: InfrastructureConfig,
510    /// Precomputed at init: min(max SWQOS submit lanes, 2/3 * num_cores). Not computed on trade hot path.
511    pub max_sender_concurrency: usize,
512    /// Precomputed at init: first max_sender_concurrency CoreIds for job affinity. Empty if no cores. Not computed on trade hot path.
513    pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
514}
515
516impl TradingInfrastructure {
517    /// Create new shared infrastructure from configuration
518    ///
519    /// This performs the expensive initialization:
520    /// - Creates RPC client with connection pool
521    /// - Creates SWQOS clients (each with their own HTTP client)
522    /// - Initializes rent cache and starts background updater
523    pub async fn new(config: InfrastructureConfig) -> Self {
524        // Install crypto provider (idempotent)
525        if CryptoProvider::get_default().is_none() {
526            let _ = default_provider()
527                .install_default()
528                .map_err(|e| anyhow::anyhow!("Failed to install crypto provider: {:?}", e));
529        }
530
531        // Create RPC client
532        let rpc = Arc::new(SolanaRpcClient::new_with_commitment(
533            config.rpc_url.clone(),
534            config.commitment.clone(),
535        ));
536
537        // Initialize rent cache (with timeout so slow RPC doesn't block forever)
538        const RENT_UPDATE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
539        match tokio::time::timeout(RENT_UPDATE_TIMEOUT, crate::common::seed::update_rents(&rpc))
540            .await
541        {
542            Ok(Ok(())) => {}
543            Ok(Err(e)) => {
544                if sdk_log::sdk_log_enabled() {
545                    warn!(target: "sol_trade_sdk", "rent update failed: {}, using defaults", e);
546                }
547                crate::common::seed::set_default_rents();
548            }
549            Err(_) => {
550                if sdk_log::sdk_log_enabled() {
551                    warn!(target: "sol_trade_sdk", "rent update timed out ({}s), using defaults; check RPC", RENT_UPDATE_TIMEOUT.as_secs());
552                }
553                crate::common::seed::set_default_rents();
554            }
555        }
556        crate::common::seed::start_rent_updater(rpc.clone());
557
558        // Create SWQOS clients with blacklist checking(QUIC 握手可能较慢,单节点超时 15s)
559        const SWQOS_CLIENT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
560        let swqos_configs = normalize_swqos_configs(&config.rpc_url, &config.swqos_configs);
561        let mut swqos_clients: Vec<Arc<SwqosClient>> = vec![];
562        for swqos in &swqos_configs {
563            if swqos.is_blacklisted() {
564                if sdk_log::sdk_log_enabled() {
565                    warn!(target: "sol_trade_sdk", "⚠️ SWQOS {:?} is blacklisted, skipping", swqos.swqos_type());
566                }
567                continue;
568            }
569            match tokio::time::timeout(
570                SWQOS_CLIENT_TIMEOUT,
571                SwqosConfig::get_swqos_client(
572                    config.rpc_url.clone(),
573                    config.commitment.clone(),
574                    swqos.clone(),
575                    config.mev_protection,
576                ),
577            )
578            .await
579            {
580                Ok(Ok(swqos_client)) => swqos_clients.push(swqos_client),
581                Ok(Err(err)) => {
582                    eprintln!(
583                        "⚠️  SWQOS {:?} 初始化失败: {}(已从列表中排除)",
584                        swqos.swqos_type(),
585                        err
586                    );
587                    if sdk_log::sdk_log_enabled() {
588                        warn!(
589                            target: "sol_trade_sdk",
590                            "failed to create {:?} swqos client: {err}. Excluding from swqos list",
591                            swqos.swqos_type()
592                        );
593                    }
594                }
595                Err(_) => {
596                    eprintln!(
597                        "⚠️  SWQOS {:?} 初始化超时({}s),已跳过",
598                        swqos.swqos_type(),
599                        SWQOS_CLIENT_TIMEOUT.as_secs()
600                    );
601                    if sdk_log::sdk_log_enabled() {
602                        warn!(
603                            target: "sol_trade_sdk",
604                            "swqos {:?} init timed out ({}s), skipping",
605                            swqos.swqos_type(),
606                            SWQOS_CLIENT_TIMEOUT.as_secs()
607                        );
608                    }
609                }
610            }
611        }
612
613        // 若全部失败、被黑名单跳过或仅配置了不可用通道,至少保留一条 Rpc Default,否则 execute_parallel 会因 swqos_clients 为空直接报错。
614        if swqos_clients.is_empty() {
615            eprintln!(
616                "⚠️  无任何 SWQOS 客户端初始化成功,将回退为普通 RPC 发送: {}",
617                config.rpc_url
618            );
619            if sdk_log::sdk_log_enabled() {
620                warn!(
621                    target: "sol_trade_sdk",
622                    "no SWQOS clients initialized; falling back to Rpc Default ({})",
623                    config.rpc_url
624                );
625            }
626            match SwqosConfig::get_swqos_client(
627                config.rpc_url.clone(),
628                config.commitment.clone(),
629                SwqosConfig::Default(config.rpc_url.clone()),
630                config.mev_protection,
631            )
632            .await
633            {
634                Ok(c) => swqos_clients.push(c),
635                Err(e) => {
636                    if sdk_log::sdk_log_enabled() {
637                        warn!(
638                            target: "sol_trade_sdk",
639                            "fallback Rpc Default client failed: {}",
640                            e
641                        );
642                    }
643                }
644            }
645        }
646
647        if !swqos_clients.is_empty() {
648            let labels: Vec<&str> =
649                swqos_clients.iter().map(|c| c.get_swqos_type().as_str()).collect();
650            eprintln!("ℹ️  SWQOS 通道已就绪: {} 条 → [{}]", swqos_clients.len(), labels.join(", "));
651        }
652
653        let max_submit_lanes = swqos_clients
654            .iter()
655            .map(|client| {
656                if matches!(client.get_swqos_type(), SwqosType::Default) {
657                    1usize
658                } else {
659                    2usize
660                }
661            })
662            .sum::<usize>()
663            .max(1);
664        let (max_sender_concurrency, effective_core_ids) = {
665            let num_cores = core_affinity::get_core_ids().map(|c| c.len()).unwrap_or(0);
666            let max_by_cores = (num_cores * 2 / 3).max(1);
667            let cap = max_submit_lanes.min(max_by_cores).max(1);
668            let ids = core_affinity::get_core_ids()
669                .map(|all| {
670                    let v: Vec<_> = all.into_iter().collect();
671                    let len = v.len();
672                    if config.swqos_cores_from_end && len >= cap {
673                        v.into_iter().skip(len - cap).collect()
674                    } else {
675                        v.into_iter().take(cap).collect()
676                    }
677                })
678                .unwrap_or_default();
679            (cap, Arc::new(ids))
680        };
681
682        crate::instruction::utils::pumpswap::warm_pumpswap_global_config(Some(&rpc)).await;
683
684        Self {
685            rpc,
686            swqos_clients: Arc::new(swqos_clients),
687            config,
688            max_sender_concurrency,
689            effective_core_ids,
690        }
691    }
692}
693
694/// When using `TradeConfig::with_swqos_cores_from_end(true)`, returns the same "last N" core indices
695/// that the infrastructure uses. Pass the result to `TradingClient::with_dedicated_sender_threads`
696/// for 方式 C (组合使用): SWQOS on last N cores and dedicated sender threads pinned to those cores.
697///
698/// Returns `None` if core count cannot be determined. `swqos_count` is typically `swqos_configs.len()`.
699pub fn recommended_sender_thread_core_indices(swqos_count: usize) -> Option<Vec<usize>> {
700    let all = core_affinity::get_core_ids()?;
701    let num_cores = all.len();
702    if num_cores == 0 {
703        return None;
704    }
705    let max_by_cores = (num_cores * 2 / 3).max(1);
706    let cap = swqos_count.min(max_by_cores).max(1).min(num_cores);
707    let start = num_cores.saturating_sub(cap);
708    Some((start..num_cores).collect())
709}
710
711/// Main trading client for Solana DeFi protocols
712///
713/// `SolTradingSDK` provides a unified interface for trading across multiple Solana DEXs
714/// including PumpFun, PumpSwap, Bonk, Raydium AMM V4, and Raydium CPMM.
715/// It manages RPC connections, transaction signing, and SWQOS (Solana Web Quality of Service) settings.
716pub struct TradingClient {
717    /// The keypair used for signing all transactions
718    pub payer: Arc<Keypair>,
719    /// Shared infrastructure (RPC client, SWQOS clients)
720    /// Can be shared across multiple TradingClient instances with different wallets
721    pub infrastructure: Arc<TradingInfrastructure>,
722    /// Optional middleware manager for custom transaction processing
723    pub middleware_manager: Option<Arc<MiddlewareManager>>,
724    /// Whether to use seed optimization for all ATA operations (default: true)
725    /// Applies to all token account creations across buy and sell operations
726    pub use_seed_optimize: bool,
727    /// Internal: use dedicated sender threads (default false). Set via with_dedicated_sender_threads() for advanced use.
728    pub use_dedicated_sender_threads: bool,
729    /// Internal: core indices for dedicated sender threads. Trimmed to ≤ max_sender_concurrency at set.
730    pub sender_thread_cores: Option<Arc<Vec<usize>>>,
731    /// Internal: precomputed at infra init (min(swqos_count, 2/3*cores)). Not user-configurable.
732    pub max_sender_concurrency: usize,
733    /// Internal: precomputed at infra init for job affinity. Not user-configurable.
734    pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
735    /// Whether to output all SDK logs (from TradeConfig.log_enabled).
736    pub log_enabled: bool,
737    /// Whether to check minimum tip per SWQOS (from TradeConfig.check_min_tip). Default false for lower latency.
738    pub check_min_tip: bool,
739}
740
741static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
742
743/// 🔄 向后兼容:SolanaTrade 别名
744pub type SolanaTrade = TradingClient;
745
746impl Clone for TradingClient {
747    fn clone(&self) -> Self {
748        Self {
749            payer: self.payer.clone(),
750            infrastructure: self.infrastructure.clone(),
751            middleware_manager: self.middleware_manager.clone(),
752            use_seed_optimize: self.use_seed_optimize,
753            use_dedicated_sender_threads: self.use_dedicated_sender_threads,
754            sender_thread_cores: self.sender_thread_cores.clone(),
755            max_sender_concurrency: self.max_sender_concurrency,
756            effective_core_ids: self.effective_core_ids.clone(),
757            log_enabled: self.log_enabled,
758            check_min_tip: self.check_min_tip,
759        }
760    }
761}
762
763/// Parameters for executing buy orders across different DEX protocols
764///
765/// Contains all necessary configuration for purchasing tokens, including
766/// protocol-specific settings, account management options, and transaction preferences.
767#[derive(Clone)]
768pub struct TradeBuyParams {
769    // Trading configuration
770    /// The DEX protocol to use for the trade
771    pub dex_type: DexType,
772    /// Type of the token to buy
773    pub input_token_type: TradeTokenType,
774    /// Public key of the token to purchase
775    pub mint: Pubkey,
776    /// Amount of tokens to buy (in smallest token units)
777    pub input_token_amount: u64,
778    /// Optional slippage tolerance in basis points (e.g., 100 = 1%)
779    pub slippage_basis_points: Option<u64>,
780    /// Recent blockhash for transaction validity
781    pub recent_blockhash: Option<Hash>,
782    /// Protocol-specific parameters (PumpFun, Raydium, etc.)
783    pub extension_params: DexParamEnum,
784    // Extended configuration
785    /// Optional address lookup table for transaction size optimization
786    pub address_lookup_table_account: Option<AddressLookupTableAccount>,
787    /// Whether to wait for transaction confirmation before returning
788    pub wait_tx_confirmed: bool,
789    /// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
790    /// SWQOS route's HTTP submit response so all submitted signatures are
791    /// returned. Set to true when confirming externally against a pinned
792    /// durable nonce; defaults to false. See `SwapParams.wait_for_all_submits`.
793    pub wait_for_all_submits: bool,
794    /// Whether to create input token associated token account
795    pub create_input_token_ata: bool,
796    /// Whether to close input token associated token account after trade
797    pub close_input_token_ata: bool,
798    /// Whether to create token mint associated token account
799    pub create_mint_ata: bool,
800    /// Durable nonce information
801    pub durable_nonce: Option<DurableNonceInfo>,
802    /// Optional fixed output token amount. On exact-out capable DEXes this uses
803    /// the exact-out instruction and treats `input_token_amount` as max input.
804    pub fixed_output_token_amount: Option<u64>,
805    /// Gas fee strategy
806    pub gas_fee_strategy: GasFeeStrategy,
807    /// Whether to simulate the transaction instead of executing it
808    pub simulate: bool,
809    /// Use exact quote-input buy instructions (legacy PumpFun uses SOL quote; V2/PumpSwap use generic quote).
810    /// When Some(true) or None (default), the exact SOL/quote amount is spent and slippage is applied to output tokens.
811    /// When Some(false), uses regular buy instruction where slippage is applied to SOL/quote input.
812    /// This option only applies to PumpFun and PumpSwap DEXes; it is ignored for other DEXes.
813    pub use_exact_sol_amount: Option<bool>,
814    /// Optional upstream receive timestamp (e.g. gRPC recv) in microseconds for latency tracing.
815    pub grpc_recv_us: Option<i64>,
816}
817
818/// Parameters for executing sell orders across different DEX protocols
819///
820/// Contains all necessary configuration for selling tokens, including
821/// protocol-specific settings, tip preferences, account management options, and transaction preferences.
822#[derive(Clone)]
823pub struct TradeSellParams {
824    // Trading configuration
825    /// The DEX protocol to use for the trade
826    pub dex_type: DexType,
827    /// Type of the token to sell
828    pub output_token_type: TradeTokenType,
829    /// Public key of the token to sell
830    pub mint: Pubkey,
831    /// Amount of tokens to sell (in smallest token units)
832    pub input_token_amount: u64,
833    /// Optional slippage tolerance in basis points (e.g., 100 = 1%)
834    pub slippage_basis_points: Option<u64>,
835    /// Recent blockhash for transaction validity
836    pub recent_blockhash: Option<Hash>,
837    /// Whether to include tip for transaction priority
838    pub with_tip: bool,
839    /// Protocol-specific parameters (PumpFun, Raydium, etc.)
840    pub extension_params: DexParamEnum,
841    // Extended configuration
842    /// Optional address lookup table for transaction size optimization
843    pub address_lookup_table_account: Option<AddressLookupTableAccount>,
844    /// Whether to wait for transaction confirmation before returning
845    pub wait_tx_confirmed: bool,
846    /// Fast-submit only (`wait_tx_confirmed = false`): when true, wait for every
847    /// SWQOS route's HTTP submit response so all submitted signatures are
848    /// returned. Set to true when confirming externally against a pinned
849    /// durable nonce; defaults to false. See `SwapParams.wait_for_all_submits`.
850    pub wait_for_all_submits: bool,
851    /// Whether to create output token associated token account
852    pub create_output_token_ata: bool,
853    /// Whether to close output token associated token account after trade
854    pub close_output_token_ata: bool,
855    /// Whether to close mint token associated token account after trade
856    pub close_mint_token_ata: bool,
857    /// Durable nonce information
858    pub durable_nonce: Option<DurableNonceInfo>,
859    /// Optional fixed output token amount. On exact-out capable DEXes this uses
860    /// the exact-out instruction and treats `input_token_amount` as max input.
861    pub fixed_output_token_amount: Option<u64>,
862    /// Gas fee strategy
863    pub gas_fee_strategy: GasFeeStrategy,
864    /// Whether to simulate the transaction instead of executing it
865    pub simulate: bool,
866    /// Optional upstream receive timestamp (e.g. gRPC recv) in microseconds for latency tracing.
867    pub grpc_recv_us: Option<i64>,
868}
869
870#[inline]
871fn buy_account_flags(policy: AccountPolicy) -> (bool, bool, bool) {
872    match policy {
873        AccountPolicy::Auto => (false, true, false),
874        AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false),
875        AccountPolicy::CreateMissing => (true, true, false),
876    }
877}
878
879#[inline]
880fn sell_account_flags(policy: AccountPolicy, receive_as: &TradeTokenType) -> (bool, bool, bool) {
881    match policy {
882        AccountPolicy::Auto => (*receive_as != TradeTokenType::SOL, false, false),
883        AccountPolicy::HotPathMinimal | AccountPolicy::AssumePrepared => (false, false, false),
884        AccountPolicy::CreateMissing => (true, false, false),
885    }
886}
887
888impl From<SimpleBuyParams> for TradeBuyParams {
889    fn from(params: SimpleBuyParams) -> Self {
890        let (input_token_amount, fixed_output_token_amount, use_exact_sol_amount) =
891            match params.amount {
892                BuyAmount::ExactInput(amount) => (amount, None, Some(true)),
893                BuyAmount::ExactOutput { output_amount, max_input_amount } => {
894                    (max_input_amount, Some(output_amount), Some(true))
895                }
896                BuyAmount::WithMaxInput { quote_amount } => (quote_amount, None, Some(false)),
897            };
898        let (create_input_token_ata, create_mint_ata, close_input_token_ata) =
899            buy_account_flags(params.account_policy);
900
901        TradeBuyParams {
902            dex_type: params.dex_type,
903            input_token_type: params.pay_with,
904            mint: params.mint,
905            input_token_amount,
906            slippage_basis_points: params.slippage_basis_points,
907            recent_blockhash: params.recent_blockhash,
908            extension_params: params.extension_params,
909            address_lookup_table_account: params.address_lookup_table_account,
910            wait_tx_confirmed: params.wait_tx_confirmed,
911            wait_for_all_submits: params.wait_for_all_submits,
912            create_input_token_ata,
913            close_input_token_ata,
914            create_mint_ata,
915            durable_nonce: params.durable_nonce,
916            fixed_output_token_amount,
917            gas_fee_strategy: params.gas_fee_strategy,
918            simulate: params.simulate,
919            use_exact_sol_amount,
920            grpc_recv_us: params.grpc_recv_us,
921        }
922    }
923}
924
925impl From<SimpleSellParams> for TradeSellParams {
926    fn from(params: SimpleSellParams) -> Self {
927        let (input_token_amount, fixed_output_token_amount) = match params.amount {
928            SellAmount::ExactInput(amount) => (amount, None),
929            SellAmount::ExactOutput { output_amount, max_input_amount } => {
930                (max_input_amount, Some(output_amount))
931            }
932        };
933        let (create_output_token_ata, close_output_token_ata, close_mint_token_ata) =
934            sell_account_flags(params.account_policy, &params.receive_as);
935
936        TradeSellParams {
937            dex_type: params.dex_type,
938            output_token_type: params.receive_as,
939            mint: params.mint,
940            input_token_amount,
941            slippage_basis_points: params.slippage_basis_points,
942            recent_blockhash: params.recent_blockhash,
943            with_tip: params.with_tip,
944            extension_params: params.extension_params,
945            address_lookup_table_account: params.address_lookup_table_account,
946            wait_tx_confirmed: params.wait_tx_confirmed,
947            wait_for_all_submits: params.wait_for_all_submits,
948            create_output_token_ata,
949            close_output_token_ata,
950            close_mint_token_ata,
951            durable_nonce: params.durable_nonce,
952            fixed_output_token_amount,
953            gas_fee_strategy: params.gas_fee_strategy,
954            simulate: params.simulate,
955            grpc_recv_us: params.grpc_recv_us,
956        }
957    }
958}
959
960impl TradingClient {
961    /// Create a TradingClient from shared infrastructure (fast path)
962    ///
963    /// This is the preferred method when multiple wallets share the same infrastructure.
964    /// It only performs wallet-specific initialization (fast_init) without the expensive
965    /// RPC/SWQOS client creation.
966    ///
967    /// # Arguments
968    /// * `payer` - The keypair used for signing transactions
969    /// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients)
970    /// * `use_seed_optimize` - Whether to use seed optimization for ATA operations
971    ///
972    /// # Returns
973    /// Returns a configured `TradingClient` instance ready for trading operations
974    pub fn from_infrastructure(
975        payer: Arc<Keypair>,
976        infrastructure: Arc<TradingInfrastructure>,
977        use_seed_optimize: bool,
978    ) -> Self {
979        // Initialize wallet-specific caches (fast, synchronous)
980        crate::common::fast_fn::fast_init(&payer.pubkey());
981        let max_sender_concurrency = infrastructure.max_sender_concurrency;
982        let effective_core_ids = infrastructure.effective_core_ids.clone();
983
984        Self {
985            payer,
986            infrastructure,
987            middleware_manager: None,
988            use_seed_optimize,
989            use_dedicated_sender_threads: false,
990            sender_thread_cores: None,
991            max_sender_concurrency,
992            effective_core_ids,
993            log_enabled: true,
994            check_min_tip: false,
995        }
996    }
997
998    /// Create a TradingClient from shared infrastructure with optional WSOL ATA setup
999    ///
1000    /// Same as `from_infrastructure` but also handles WSOL ATA creation if requested.
1001    ///
1002    /// # Arguments
1003    /// * `payer` - The keypair used for signing transactions
1004    /// * `infrastructure` - Shared infrastructure (RPC client, SWQOS clients)
1005    /// * `use_seed_optimize` - Whether to use seed optimization for ATA operations
1006    /// * `create_wsol_ata` - Whether to check/create WSOL ATA
1007    pub async fn from_infrastructure_with_wsol_setup(
1008        payer: Arc<Keypair>,
1009        infrastructure: Arc<TradingInfrastructure>,
1010        use_seed_optimize: bool,
1011        create_wsol_ata: bool,
1012    ) -> Self {
1013        crate::common::fast_fn::fast_init(&payer.pubkey());
1014
1015        if create_wsol_ata {
1016            // 在后台异步创建 WSOL ATA,不阻塞启动
1017            let payer_clone = payer.clone();
1018            let rpc_clone = infrastructure.rpc.clone();
1019            tokio::spawn(async move {
1020                Self::ensure_wsol_ata(&payer_clone, &rpc_clone).await;
1021            });
1022            if sdk_log::sdk_log_enabled() {
1023                info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA creation started in background, does not block bot startup");
1024            }
1025        }
1026
1027        let max_sender_concurrency = infrastructure.max_sender_concurrency;
1028        let effective_core_ids = infrastructure.effective_core_ids.clone();
1029
1030        Self {
1031            payer,
1032            infrastructure,
1033            middleware_manager: None,
1034            use_seed_optimize,
1035            use_dedicated_sender_threads: false,
1036            sender_thread_cores: None,
1037            max_sender_concurrency,
1038            effective_core_ids,
1039            log_enabled: true,
1040            check_min_tip: false,
1041        }
1042    }
1043
1044    /// 单次尝试创建 WSOL ATA:获取 blockhash、组交易、发送并确认。成功或账户已存在返回 Ok(()),否则返回 Err(错误信息)。
1045    async fn try_create_wsol_ata_once(
1046        rpc: &SolanaRpcClient,
1047        payer: &Arc<Keypair>,
1048        wsol_ata: &solana_sdk::pubkey::Pubkey,
1049        create_ata_ixs: &[solana_sdk::instruction::Instruction],
1050        timeout_secs: u64,
1051    ) -> Result<(), String> {
1052        use solana_sdk::transaction::Transaction;
1053        let recent_blockhash = rpc
1054            .get_latest_blockhash()
1055            .await
1056            .map_err(|e| format!("Failed to get blockhash: {}", e))?;
1057        let tx = Transaction::new_signed_with_payer(
1058            create_ata_ixs,
1059            Some(&payer.pubkey()),
1060            &[payer.as_ref()],
1061            recent_blockhash,
1062        );
1063        let send_result = tokio::time::timeout(
1064            tokio::time::Duration::from_secs(timeout_secs),
1065            rpc.send_and_confirm_transaction(&tx),
1066        )
1067        .await;
1068        match send_result {
1069            Ok(Ok(_signature)) => Ok(()),
1070            Ok(Err(e)) => {
1071                if rpc.get_account(wsol_ata).await.is_ok() {
1072                    return Ok(());
1073                }
1074                Err(format!("{}", e))
1075            }
1076            Err(_) => Err(format!("Transaction confirmation timeout ({}s)", timeout_secs)),
1077        }
1078    }
1079
1080    /// 确保钱包存在 WSOL ATA;不存在则发交易创建(会花费租金 + 手续费,初始化阶段唯一会扣钱的逻辑)
1081    async fn ensure_wsol_ata(payer: &Arc<Keypair>, rpc: &Arc<SolanaRpcClient>) {
1082        const MAX_RETRIES: usize = 3;
1083        const TIMEOUT_SECS: u64 = 10;
1084
1085        let wsol_ata = crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
1086            &payer.pubkey(),
1087            &WSOL_TOKEN_ACCOUNT,
1088            &crate::constants::TOKEN_PROGRAM,
1089        );
1090
1091        if rpc.get_account(&wsol_ata).await.is_ok() {
1092            if sdk_log::sdk_log_enabled() {
1093                info!(target: "sol_trade_sdk", "✅ WSOL ATA already exists: {}", wsol_ata);
1094            }
1095            return;
1096        }
1097
1098        let create_ata_ixs = crate::trading::common::wsol_manager::create_wsol_ata(&payer.pubkey());
1099        if create_ata_ixs.is_empty() {
1100            if sdk_log::sdk_log_enabled() {
1101                info!(target: "sol_trade_sdk", "ℹ️ WSOL ATA already exists (no need to create)");
1102            }
1103            return;
1104        }
1105
1106        if sdk_log::sdk_log_enabled() {
1107            info!(target: "sol_trade_sdk", "🔨 Creating WSOL ATA: {}", wsol_ata);
1108        }
1109        let mut last_error = None;
1110        for attempt in 1..=MAX_RETRIES {
1111            if attempt > 1 {
1112                if sdk_log::sdk_log_enabled() {
1113                    info!(target: "sol_trade_sdk", "🔄 Retrying WSOL ATA creation (attempt {}/{})...", attempt, MAX_RETRIES);
1114                }
1115                tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
1116            }
1117            match Self::try_create_wsol_ata_once(
1118                rpc.as_ref(),
1119                payer,
1120                &wsol_ata,
1121                &create_ata_ixs,
1122                TIMEOUT_SECS,
1123            )
1124            .await
1125            {
1126                Ok(()) => {
1127                    if sdk_log::sdk_log_enabled() {
1128                        info!(target: "sol_trade_sdk", "✅ WSOL ATA created or already exists");
1129                    }
1130                    return;
1131                }
1132                Err(e) => {
1133                    last_error = Some(e.clone());
1134                    if attempt < MAX_RETRIES && sdk_log::sdk_log_enabled() {
1135                        warn!(target: "sol_trade_sdk", "⚠️ Attempt {} failed: {}", attempt, e);
1136                    }
1137                }
1138            }
1139        }
1140
1141        if let Some(err) = last_error {
1142            if sdk_log::sdk_log_enabled() {
1143                error!(target: "sol_trade_sdk", "❌ WSOL ATA creation failed after {} retries: {}", MAX_RETRIES, wsol_ata);
1144                error!(target: "sol_trade_sdk", "   Error: {}", err);
1145                error!(target: "sol_trade_sdk", "   💡 Possible causes: insufficient SOL, RPC timeout, or fee");
1146                error!(target: "sol_trade_sdk", "   🔧 Solutions: fund wallet (e.g. 0.1 SOL), retry, check RPC");
1147            }
1148            std::thread::sleep(std::time::Duration::from_secs(5));
1149            panic!(
1150                "❌ WSOL ATA creation failed and account does not exist: {}. Error: {}",
1151                wsol_ata, err
1152            );
1153        }
1154    }
1155
1156    /// Creates a new SolTradingSDK instance with the specified configuration
1157    ///
1158    /// This function initializes the trading system with RPC connection, SWQOS settings,
1159    /// and sets up necessary components for trading operations.
1160    ///
1161    /// # Arguments
1162    /// * `payer` - The keypair used for signing transactions
1163    /// * `trade_config` - Trading configuration including RPC URL, SWQOS settings, etc.
1164    ///
1165    /// # Returns
1166    /// Returns a configured `SolTradingSDK` instance ready for trading operations
1167    #[inline]
1168    pub async fn new(payer: Arc<Keypair>, trade_config: TradeConfig) -> Self {
1169        // 设置 SDK 全局日志开关,后续所有 SDK 内日志(SWQOS/WSOL/耗时等)均受此控制
1170        sdk_log::set_sdk_log_enabled(trade_config.log_enabled);
1171        // 预热高性能时钟,避免首笔交易时触发 3 次 Utc::now() 校准
1172        let _ = crate::common::clock::now_micros();
1173        // Create infrastructure from trade config
1174        let infra_config = InfrastructureConfig::from_trade_config(&trade_config);
1175        let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
1176
1177        // Initialize wallet-specific caches
1178        crate::common::fast_fn::fast_init(&payer.pubkey());
1179
1180        // ═══════════════════════════════════════════════════════════════════════════════
1181        // 初始化阶段会花费租金/手续费的唯一路径:创建 WSOL ATA(ensure_wsol_ata)
1182        // - 触发条件:create_wsol_ata_on_startup == true 且钱包 SOL >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS
1183        // - 花费:ATA 租金(约 0.00203928 SOL)+ 交易手续费;钱包不足时已跳过
1184        // - 其它初始化(TradingInfrastructure::new、update_rents、get_swqos_client)仅 RPC/HTTP,不发送交易
1185        // ═══════════════════════════════════════════════════════════════════════════════
1186        if trade_config.create_wsol_ata_on_startup {
1187            const MIN_SOL_FOR_WSOL_ATA_LAMPORTS: u64 = 500_000; // 约 0.0005 SOL,用于 ATA 租金 + 手续费
1188            const BALANCE_CHECK_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
1189            let balance = tokio::time::timeout(
1190                BALANCE_CHECK_TIMEOUT,
1191                infrastructure.rpc.get_balance(&payer.pubkey()),
1192            )
1193            .await
1194            .unwrap_or(Ok(0))
1195            .unwrap_or(0);
1196            if balance >= MIN_SOL_FOR_WSOL_ATA_LAMPORTS {
1197                Self::ensure_wsol_ata(&payer, &infrastructure.rpc).await;
1198            } else if sdk_log::sdk_log_enabled() {
1199                info!(
1200                    target: "sol_trade_sdk",
1201                    "⏭️ 跳过创建 WSOL ATA:钱包 SOL 不足(当前 {} lamports,需要至少 {})",
1202                    balance,
1203                    MIN_SOL_FOR_WSOL_ATA_LAMPORTS
1204                );
1205            }
1206        }
1207
1208        // 并发/核心相关由 infrastructure 预计算,用户无需配置
1209        let instance = Self {
1210            payer,
1211            infrastructure: infrastructure.clone(),
1212            middleware_manager: None,
1213            use_seed_optimize: trade_config.use_seed_optimize,
1214            use_dedicated_sender_threads: false,
1215            sender_thread_cores: None,
1216            max_sender_concurrency: infrastructure.max_sender_concurrency,
1217            effective_core_ids: infrastructure.effective_core_ids.clone(),
1218            log_enabled: trade_config.log_enabled,
1219            check_min_tip: trade_config.check_min_tip,
1220        };
1221
1222        let mut current = INSTANCE.lock();
1223        *current = Some(Arc::new(instance.clone()));
1224
1225        instance
1226    }
1227
1228    /// Adds a middleware manager to the SolanaTrade instance
1229    ///
1230    /// Middleware managers can be used to implement custom logic that runs before or after trading operations,
1231    /// such as logging, monitoring, or custom validation.
1232    ///
1233    /// # Arguments
1234    /// * `middleware_manager` - The middleware manager to attach
1235    ///
1236    /// # Returns
1237    /// Returns the modified SolanaTrade instance with middleware manager attached
1238    pub fn with_middleware_manager(mut self, middleware_manager: MiddlewareManager) -> Self {
1239        self.middleware_manager = Some(Arc::new(middleware_manager));
1240        self
1241    }
1242
1243    /// **Advanced.** Use dedicated OS threads for sender pool (and optionally pin to cores).  
1244    /// By default the SDK uses a shared tokio pool; this can reduce scheduling contention when sending many txs.  
1245    /// Concurrency and core count are capped internally (≤ max submit lanes, ≤ 2/3 of CPU cores).
1246    /// - `None`: keep default (shared tokio pool).  
1247    /// - `Some(vec![])`: dedicated threads with default count, no core pinning.  
1248    /// - `Some(indices)`: dedicated threads pinned to those core indices (trimmed to cap).  
1249    ///  
1250    /// **Latency note:** If a core is busy with other work (node, bot), SWQOS submit on that core can be delayed.  
1251    /// For lowest latency, pass core indices that are *reserved* for SWQOS (do not run other CPU-heavy work on those cores).
1252    pub fn with_dedicated_sender_threads(mut self, core_indices: Option<Vec<usize>>) -> Self {
1253        match core_indices {
1254            None => {
1255                self.use_dedicated_sender_threads = false;
1256                self.sender_thread_cores = None;
1257            }
1258            Some(v) if v.is_empty() => {
1259                self.use_dedicated_sender_threads = true;
1260                self.sender_thread_cores = None;
1261            }
1262            Some(v) => {
1263                self.use_dedicated_sender_threads = true;
1264                let cap = v.len().min(self.max_sender_concurrency);
1265                self.sender_thread_cores =
1266                    Some(Arc::new(if cap < v.len() { v[..cap].to_vec() } else { v }));
1267            }
1268        }
1269        if self.use_dedicated_sender_threads {
1270            crate::trading::core::async_executor::warm_dedicated_sender_pool(
1271                self.sender_thread_cores.as_ref().map(|v| v.as_slice()),
1272                self.max_sender_concurrency,
1273            );
1274        }
1275        self
1276    }
1277
1278    /// Gets the RPC client instance for direct Solana blockchain interactions
1279    ///
1280    /// This provides access to the underlying Solana RPC client that can be used
1281    /// for custom blockchain operations outside of the trading framework.
1282    ///
1283    /// # Returns
1284    /// Returns a reference to the Arc-wrapped SolanaRpcClient instance
1285    pub fn get_rpc(&self) -> &Arc<SolanaRpcClient> {
1286        &self.infrastructure.rpc
1287    }
1288
1289    /// Gets the current globally shared SolanaTrade instance
1290    ///
1291    /// This provides access to the singleton instance that was created with `new()`.
1292    /// Useful for accessing the trading instance from different parts of the application.
1293    ///
1294    /// # Returns
1295    /// Returns the Arc-wrapped SolanaTrade instance
1296    ///
1297    /// # Panics
1298    /// Panics if no instance has been initialized yet. Make sure to call `new()` first.
1299    pub fn get_instance() -> Arc<Self> {
1300        let instance = INSTANCE.lock();
1301        instance
1302            .as_ref()
1303            .expect("SolanaTrade instance not initialized. Please call new() first.")
1304            .clone()
1305    }
1306
1307    /// Execute a buy order for a specified token
1308    ///
1309    /// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
1310    /// - bool: 是否至少有一个交易成功
1311    /// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
1312    /// - Option<TradeError>: 最后一个错误(如果全部失败)
1313    ///
1314    /// # Arguments
1315    ///
1316    /// * `params` - Buy trade parameters containing all necessary trading configuration
1317    ///
1318    /// # Returns
1319    ///
1320    /// Returns `Ok((bool, Vec<Signature>, Option<TradeError>))` with success flag and all transaction signatures,
1321    /// or an error if the transaction fails.
1322    ///
1323    /// # Errors
1324    ///
1325    /// This function will return an error if:
1326    /// - Invalid protocol parameters are provided for the specified DEX type
1327    /// - The transaction fails to execute
1328    /// - Network or RPC errors occur
1329    /// - Insufficient SOL balance for the purchase
1330    /// - Required accounts cannot be created or accessed
1331    #[inline]
1332    pub async fn buy(
1333        &self,
1334        params: TradeBuyParams,
1335    ) -> Result<
1336        (bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
1337        anyhow::Error,
1338    > {
1339        if params.recent_blockhash.is_none() && params.durable_nonce.is_none() {
1340            return Err(anyhow::anyhow!(
1341                "Must provide either recent_blockhash or durable_nonce for buy (required for transaction validity)"
1342            ));
1343        }
1344        #[cfg(feature = "perf-trace")]
1345        if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
1346            debug!(
1347                target: "sol_trade_sdk",
1348                "slippage_basis_points is none, use default slippage basis points: {}",
1349                DEFAULT_SLIPPAGE
1350            );
1351        }
1352        if params.input_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk {
1353            return Err(anyhow::anyhow!(
1354                " Current version only supports USD1 trading on Bonk protocols"
1355            ));
1356        }
1357        let protocol_params = params.extension_params;
1358        if !validate_protocol_params(params.dex_type, &protocol_params) {
1359            return Err(anyhow::anyhow!(
1360                "Invalid protocol params for Trade (dex={:?})",
1361                params.dex_type
1362            ));
1363        }
1364        let input_token_mint = if params.input_token_type == TradeTokenType::SOL {
1365            SOL_TOKEN_ACCOUNT
1366        } else if params.input_token_type == TradeTokenType::WSOL {
1367            WSOL_TOKEN_ACCOUNT
1368        } else if params.input_token_type == TradeTokenType::USDC {
1369            USDC_TOKEN_ACCOUNT
1370        } else {
1371            USD1_TOKEN_ACCOUNT
1372        };
1373        let executor = TradeFactory::create_executor(params.dex_type);
1374        let buy_params = SwapParams {
1375            rpc: Some(self.infrastructure.rpc.clone()),
1376            payer: self.payer.clone(),
1377            trade_type: TradeType::Buy,
1378            input_mint: input_token_mint,
1379            output_mint: params.mint,
1380            input_token_program: None,
1381            output_token_program: None,
1382            input_amount: Some(params.input_token_amount),
1383            slippage_basis_points: params.slippage_basis_points,
1384            address_lookup_table_account: params.address_lookup_table_account,
1385            recent_blockhash: params.recent_blockhash,
1386            wait_tx_confirmed: params.wait_tx_confirmed,
1387            protocol_params,
1388            open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置
1389            swqos_clients: self.infrastructure.swqos_clients.clone(),
1390            middleware_manager: self.middleware_manager.clone(),
1391            durable_nonce: params.durable_nonce,
1392            with_tip: true,
1393            create_input_mint_ata: params.create_input_token_ata,
1394            close_input_mint_ata: params.close_input_token_ata,
1395            create_output_mint_ata: params.create_mint_ata,
1396            close_output_mint_ata: false,
1397            fixed_output_amount: params.fixed_output_token_amount,
1398            gas_fee_strategy: params.gas_fee_strategy,
1399            simulate: params.simulate,
1400            log_enabled: self.log_enabled,
1401            wait_for_all_submits: params.wait_for_all_submits,
1402            use_dedicated_sender_threads: self.use_dedicated_sender_threads,
1403            sender_thread_cores: self.sender_thread_cores.clone(),
1404            max_sender_concurrency: self.max_sender_concurrency,
1405            effective_core_ids: self.effective_core_ids.clone(),
1406            check_min_tip: self.check_min_tip,
1407            grpc_recv_us: params.grpc_recv_us,
1408            use_exact_sol_amount: params.use_exact_sol_amount,
1409        };
1410
1411        let swap_result = executor.swap(buy_params).await;
1412        let result = swap_result.map(|(success, sigs, err, timings)| {
1413            let legacy_timings = timings
1414                .into_iter()
1415                .map(|timing| (timing.swqos_type, timing.submit_done_us))
1416                .collect();
1417            (success, sigs, err.map(TradeError::from), legacy_timings)
1418        });
1419        result
1420    }
1421
1422    /// Execute a high-level buy request.
1423    #[inline]
1424    pub async fn buy_simple(
1425        &self,
1426        params: SimpleBuyParams,
1427    ) -> Result<
1428        (bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
1429        anyhow::Error,
1430    > {
1431        self.buy(params.into()).await
1432    }
1433
1434    /// Execute a sell order for a specified token
1435    ///
1436    /// 🔧 修复:返回Vec<Signature>支持多SWQOS并发交易
1437    /// - bool: 是否至少有一个交易成功
1438    /// - Vec<Signature>: 所有提交的交易签名(按SWQOS顺序)
1439    /// - Option<TradeError>: 最后一个错误(如果全部失败)
1440    ///
1441    /// # Arguments
1442    ///
1443    /// * `params` - Sell trade parameters containing all necessary trading configuration
1444    ///
1445    /// # Returns
1446    ///
1447    /// Returns `Ok((bool, Vec<Signature>, Option<TradeError>))` with success flag and all transaction signatures,
1448    /// or an error if the transaction fails.
1449    ///
1450    /// # Errors
1451    ///
1452    /// This function will return an error if:
1453    /// - Invalid protocol parameters are provided for the specified DEX type
1454    /// - The transaction fails to execute
1455    /// - Network or RPC errors occur
1456    /// - Insufficient token balance for the sale
1457    /// - Token account doesn't exist or is not properly initialized
1458    /// - Required accounts cannot be created or accessed
1459    #[inline]
1460    pub async fn sell(
1461        &self,
1462        params: TradeSellParams,
1463    ) -> Result<
1464        (bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
1465        anyhow::Error,
1466    > {
1467        #[cfg(feature = "perf-trace")]
1468        if sdk_log::sdk_log_enabled() && params.slippage_basis_points.is_none() {
1469            debug!(
1470                target: "sol_trade_sdk",
1471                "slippage_basis_points is none, use default slippage basis points: {}",
1472                DEFAULT_SLIPPAGE
1473            );
1474        }
1475        if params.recent_blockhash.is_none() && params.durable_nonce.is_none() {
1476            return Err(anyhow::anyhow!(
1477                "Must provide either recent_blockhash or durable_nonce for sell (required for transaction validity)"
1478            ));
1479        }
1480        if params.output_token_type == TradeTokenType::USD1 && params.dex_type != DexType::Bonk {
1481            return Err(anyhow::anyhow!(
1482                " Current version only supports USD1 trading on Bonk protocols"
1483            ));
1484        }
1485        let protocol_params = params.extension_params;
1486        if !validate_protocol_params(params.dex_type, &protocol_params) {
1487            return Err(anyhow::anyhow!(
1488                "Invalid protocol params for Trade (dex={:?})",
1489                params.dex_type
1490            ));
1491        }
1492        let executor = TradeFactory::create_executor(params.dex_type);
1493        let output_token_mint = if params.output_token_type == TradeTokenType::SOL {
1494            SOL_TOKEN_ACCOUNT
1495        } else if params.output_token_type == TradeTokenType::WSOL {
1496            WSOL_TOKEN_ACCOUNT
1497        } else if params.output_token_type == TradeTokenType::USDC {
1498            USDC_TOKEN_ACCOUNT
1499        } else {
1500            USD1_TOKEN_ACCOUNT
1501        };
1502        let sell_params = SwapParams {
1503            rpc: Some(self.infrastructure.rpc.clone()),
1504            payer: self.payer.clone(),
1505            trade_type: TradeType::Sell,
1506            input_mint: params.mint,
1507            output_mint: output_token_mint,
1508            input_token_program: None,
1509            output_token_program: None,
1510            input_amount: Some(params.input_token_amount),
1511            slippage_basis_points: params.slippage_basis_points,
1512            address_lookup_table_account: params.address_lookup_table_account,
1513            recent_blockhash: params.recent_blockhash,
1514            wait_tx_confirmed: params.wait_tx_confirmed,
1515            protocol_params,
1516            with_tip: params.with_tip,
1517            open_seed_optimize: self.use_seed_optimize, // 使用全局seed优化配置
1518            swqos_clients: self.infrastructure.swqos_clients.clone(),
1519            middleware_manager: self.middleware_manager.clone(),
1520            durable_nonce: params.durable_nonce,
1521            create_input_mint_ata: false,
1522            close_input_mint_ata: params.close_mint_token_ata,
1523            create_output_mint_ata: params.create_output_token_ata,
1524            close_output_mint_ata: params.close_output_token_ata,
1525            fixed_output_amount: params.fixed_output_token_amount,
1526            gas_fee_strategy: params.gas_fee_strategy,
1527            simulate: params.simulate,
1528            log_enabled: self.log_enabled,
1529            wait_for_all_submits: params.wait_for_all_submits,
1530            use_dedicated_sender_threads: self.use_dedicated_sender_threads,
1531            sender_thread_cores: self.sender_thread_cores.clone(),
1532            max_sender_concurrency: self.max_sender_concurrency,
1533            effective_core_ids: self.effective_core_ids.clone(),
1534            check_min_tip: self.check_min_tip,
1535            grpc_recv_us: params.grpc_recv_us,
1536            use_exact_sol_amount: None,
1537        };
1538
1539        let swap_result = executor.swap(sell_params).await;
1540        let result = swap_result.map(|(success, sigs, err, timings)| {
1541            let legacy_timings = timings
1542                .into_iter()
1543                .map(|timing| (timing.swqos_type, timing.submit_done_us))
1544                .collect();
1545            (success, sigs, err.map(TradeError::from), legacy_timings)
1546        });
1547        result
1548    }
1549
1550    /// Execute a high-level sell request.
1551    #[inline]
1552    pub async fn sell_simple(
1553        &self,
1554        params: SimpleSellParams,
1555    ) -> Result<
1556        (bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
1557        anyhow::Error,
1558    > {
1559        self.sell(params.into()).await
1560    }
1561
1562    /// Execute a sell order for a percentage of the specified token amount
1563    ///
1564    /// This is a convenience function that calculates the exact amount to sell based on
1565    /// a percentage of the total token amount and then calls the `sell` function.
1566    ///
1567    /// # Arguments
1568    ///
1569    /// * `params` - Sell trade parameters (will be modified with calculated token amount)
1570    /// * `amount_token` - Total amount of tokens available (in smallest token units)
1571    /// * `percent` - Percentage of tokens to sell (1-100, where 100 = 100%)
1572    ///
1573    /// # Returns
1574    ///
1575    /// Returns `Ok(Signature)` with the transaction signature if the sell order is successfully executed,
1576    /// or an error if the transaction fails.
1577    ///
1578    /// # Errors
1579    ///
1580    /// This function will return an error if:
1581    /// - `percent` is 0 or greater than 100
1582    /// - Invalid protocol parameters are provided for the specified DEX type
1583    /// - The transaction fails to execute
1584    /// - Network or RPC errors occur
1585    /// - Insufficient token balance for the calculated sale amount
1586    /// - Token account doesn't exist or is not properly initialized
1587    /// - Required accounts cannot be created or accessed
1588    pub async fn sell_by_percent(
1589        &self,
1590        mut params: TradeSellParams,
1591        amount_token: u64,
1592        percent: u64,
1593    ) -> Result<
1594        (bool, Vec<Signature>, Option<TradeError>, Vec<(crate::swqos::SwqosType, i64)>),
1595        anyhow::Error,
1596    > {
1597        if percent == 0 || percent > 100 {
1598            return Err(anyhow::anyhow!("Percentage must be between 1 and 100"));
1599        }
1600        let amount = amount_token * percent / 100;
1601        params.input_token_amount = amount;
1602        self.sell(params).await
1603    }
1604
1605    /// Wraps native SOL into wSOL (Wrapped SOL) for use in SPL token operations
1606    ///
1607    /// This function creates a wSOL associated token account (if it doesn't exist),
1608    /// transfers the specified amount of SOL to that account, and then syncs the native
1609    /// token balance to make SOL usable as an SPL token in trading operations.
1610    ///
1611    /// # Arguments
1612    /// * `amount` - The amount of SOL to wrap (in lamports)
1613    ///
1614    /// # Returns
1615    /// * `Ok(String)` - Transaction signature if successful
1616    /// * `Err(anyhow::Error)` - If the transaction fails to execute
1617    ///
1618    /// # Errors
1619    ///
1620    /// This function will return an error if:
1621    /// - Insufficient SOL balance for the wrap operation
1622    /// - wSOL associated token account creation fails
1623    /// - Transaction fails to execute or confirm
1624    /// - Network or RPC errors occur
1625    pub async fn wrap_sol_to_wsol(&self, amount: u64) -> Result<String, anyhow::Error> {
1626        use crate::trading::common::wsol_manager::handle_wsol;
1627        use solana_sdk::transaction::Transaction;
1628        let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
1629        let instructions = handle_wsol(&self.payer.pubkey(), amount);
1630        let mut transaction =
1631            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
1632        transaction.sign(&[&*self.payer], recent_blockhash);
1633        let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
1634        Ok(signature.to_string())
1635    }
1636    /// Closes the wSOL associated token account and unwraps remaining balance to native SOL
1637    ///
1638    /// This function closes the wSOL associated token account, which automatically
1639    /// transfers any remaining wSOL balance back to the account owner as native SOL.
1640    /// This is useful for cleaning up wSOL accounts and recovering wrapped SOL after trading operations.
1641    ///
1642    /// # Returns
1643    /// * `Ok(String)` - Transaction signature if successful
1644    /// * `Err(anyhow::Error)` - If the transaction fails to execute
1645    ///
1646    /// # Errors
1647    ///
1648    /// This function will return an error if:
1649    /// - wSOL associated token account doesn't exist
1650    /// - Account closure fails due to insufficient permissions
1651    /// - Transaction fails to execute or confirm
1652    /// - Network or RPC errors occur
1653    pub async fn close_wsol(&self) -> Result<String, anyhow::Error> {
1654        use crate::trading::common::wsol_manager::close_wsol;
1655        use solana_sdk::transaction::Transaction;
1656        let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
1657        let instructions = close_wsol(&self.payer.pubkey());
1658        let mut transaction =
1659            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
1660        transaction.sign(&[&*self.payer], recent_blockhash);
1661        let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
1662        Ok(signature.to_string())
1663    }
1664
1665    /// Creates a wSOL associated token account (ATA) without wrapping any SOL
1666    ///
1667    /// This function only creates the wSOL associated token account for the payer
1668    /// without transferring any SOL into it. This is useful when you want to set up
1669    /// the account infrastructure in advance without committing funds yet.
1670    ///
1671    /// # Returns
1672    /// * `Ok(String)` - Transaction signature if successful
1673    /// * `Err(anyhow::Error)` - If the transaction fails to execute
1674    ///
1675    /// # Errors
1676    ///
1677    /// This function will return an error if:
1678    /// - wSOL ATA account already exists (idempotent, will succeed silently)
1679    /// - Transaction fails to execute or confirm
1680    /// - Network or RPC errors occur
1681    /// - Insufficient SOL for transaction fees
1682    pub async fn create_wsol_ata(&self) -> Result<String, anyhow::Error> {
1683        use crate::trading::common::wsol_manager::create_wsol_ata;
1684        use solana_sdk::transaction::Transaction;
1685
1686        let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
1687        let instructions = create_wsol_ata(&self.payer.pubkey());
1688
1689        // If instructions are empty, ATA already exists
1690        if instructions.is_empty() {
1691            return Err(anyhow::anyhow!("wSOL ATA already exists or no instructions needed"));
1692        }
1693
1694        let mut transaction =
1695            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
1696        transaction.sign(&[&*self.payer], recent_blockhash);
1697        let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
1698        Ok(signature.to_string())
1699    }
1700
1701    /// 将 WSOL 转换为 SOL,使用 seed 账户
1702    ///
1703    /// 这个函数实现以下步骤:
1704    /// 1. 使用 super::seed::create_associated_token_account_use_seed 创建 WSOL seed 账号
1705    /// 2. 使用 get_associated_token_address_with_program_id_use_seed 获取该账号的 ATA 地址
1706    /// 3. 添加从用户 WSOL ATA 转账到该 seed ATA 账号的指令
1707    /// 4. 添加关闭 WSOL seed 账号的指令
1708    ///
1709    /// # Arguments
1710    /// * `amount` - 要转换的 WSOL 数量(以 lamports 为单位)
1711    ///
1712    /// # Returns
1713    /// * `Ok(String)` - 交易签名
1714    /// * `Err(anyhow::Error)` - 如果交易执行失败
1715    ///
1716    /// # Errors
1717    ///
1718    /// 此函数在以下情况下会返回错误:
1719    /// - 用户 WSOL ATA 中余额不足
1720    /// - seed 账户创建失败
1721    /// - 转账指令执行失败
1722    /// - 交易执行或确认失败
1723    /// - 网络或 RPC 错误
1724    pub async fn wrap_wsol_to_sol(&self, amount: u64) -> Result<String, anyhow::Error> {
1725        use crate::common::seed::get_associated_token_address_with_program_id_use_seed;
1726        use crate::trading::common::wsol_manager::{
1727            wrap_wsol_to_sol as wrap_wsol_to_sol_internal, wrap_wsol_to_sol_without_create,
1728        };
1729        use solana_sdk::transaction::Transaction;
1730
1731        // 检查临时seed账户是否已存在
1732        let seed_ata_address = get_associated_token_address_with_program_id_use_seed(
1733            &self.payer.pubkey(),
1734            &crate::constants::WSOL_TOKEN_ACCOUNT,
1735            &crate::constants::TOKEN_PROGRAM,
1736        )?;
1737
1738        let account_exists = self.infrastructure.rpc.get_account(&seed_ata_address).await.is_ok();
1739
1740        let instructions = if account_exists {
1741            // 如果账户已存在,使用不创建账户的版本
1742            wrap_wsol_to_sol_without_create(&self.payer.pubkey(), amount)?
1743        } else {
1744            // 如果账户不存在,使用创建账户的版本
1745            wrap_wsol_to_sol_internal(&self.payer.pubkey(), amount)?
1746        };
1747
1748        let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
1749        let mut transaction =
1750            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
1751        transaction.sign(&[&*self.payer], recent_blockhash);
1752        let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
1753        Ok(signature.to_string())
1754    }
1755
1756    /// Claim Bonding Curve (Pump) cashback.
1757    ///
1758    /// Transfers native SOL from the user's UserVolumeAccumulator to the wallet.
1759    /// If there is nothing to claim, the transaction may still succeed with no SOL transferred.
1760    ///
1761    /// # Returns
1762    /// * `Ok(String)` - Transaction signature
1763    /// * `Err(anyhow::Error)` - Build or send failure (e.g. invalid PDA)
1764    pub async fn claim_cashback_pumpfun(&self) -> Result<String, anyhow::Error> {
1765        use solana_sdk::transaction::Transaction;
1766        let ix = crate::instruction::pumpfun::claim_cashback_pumpfun_instruction(
1767            &self.payer.pubkey(),
1768        )
1769        .ok_or_else(|| anyhow::anyhow!("Failed to build PumpFun claim_cashback instruction"))?;
1770        let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
1771        let mut transaction = Transaction::new_with_payer(&[ix], Some(&self.payer.pubkey()));
1772        transaction.sign(&[&*self.payer], recent_blockhash);
1773        let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
1774        Ok(signature.to_string())
1775    }
1776
1777    /// Claim PumpSwap (AMM) cashback.
1778    ///
1779    /// Transfers WSOL from the UserVolumeAccumulator to the user's WSOL ATA.
1780    /// Creates the user's WSOL ATA idempotently if it does not exist, then claims.
1781    ///
1782    /// # Returns
1783    /// * `Ok(String)` - Transaction signature
1784    /// * `Err(anyhow::Error)` - Build or send failure
1785    pub async fn claim_cashback_pumpswap(&self) -> Result<String, anyhow::Error> {
1786        use solana_sdk::transaction::Transaction;
1787        let mut instructions =
1788            crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
1789                &self.payer.pubkey(),
1790                &self.payer.pubkey(),
1791                &WSOL_TOKEN_ACCOUNT,
1792                &crate::constants::TOKEN_PROGRAM,
1793                self.use_seed_optimize,
1794            );
1795        let ix = crate::instruction::pumpswap::claim_cashback_pumpswap_instruction(
1796            &self.payer.pubkey(),
1797            WSOL_TOKEN_ACCOUNT,
1798            crate::constants::TOKEN_PROGRAM,
1799        )
1800        .ok_or_else(|| anyhow::anyhow!("Failed to build PumpSwap claim_cashback instruction"))?;
1801        instructions.push(ix);
1802        let recent_blockhash = self.infrastructure.rpc.get_latest_blockhash().await?;
1803        let mut transaction =
1804            Transaction::new_with_payer(&instructions, Some(&self.payer.pubkey()));
1805        transaction.sign(&[&*self.payer], recent_blockhash);
1806        let signature = self.infrastructure.rpc.send_and_confirm_transaction(&transaction).await?;
1807        Ok(signature.to_string())
1808    }
1809}
1810
1811#[cfg(test)]
1812mod tests {
1813    use super::*;
1814    use crate::instruction::utils::pumpfun::global_constants;
1815    use crate::swqos::SwqosRegion;
1816    use std::sync::Arc;
1817
1818    fn dummy_pumpfun_params() -> DexParamEnum {
1819        DexParamEnum::PumpFun(PumpFunParams {
1820            bonding_curve: Arc::new(Default::default()),
1821            associated_bonding_curve: Pubkey::default(),
1822            observed_trade_creator: None,
1823            creator_vault: Pubkey::default(),
1824            fee_sharing_creator_vault_if_active: None,
1825            token_program: Pubkey::default(),
1826            close_token_account_when_sell: None,
1827            fee_recipient: global_constants::FEE_RECIPIENT,
1828            quote_mint: Pubkey::default(),
1829        })
1830    }
1831
1832    #[test]
1833    fn normalize_swqos_configs_adds_default_rpc_route() {
1834        let configs = vec![SwqosConfig::Jito("uuid".to_string(), SwqosRegion::Frankfurt, None)];
1835        let normalized = normalize_swqos_configs("https://rpc.example", &configs);
1836
1837        assert_eq!(normalized.len(), 2);
1838        assert!(normalized.iter().any(|c| matches!(c.swqos_type(), SwqosType::Jito)));
1839        assert!(normalized.iter().any(|c| matches!(c.swqos_type(), SwqosType::Default)));
1840    }
1841
1842    #[test]
1843    fn normalize_swqos_configs_does_not_duplicate_default_rpc_route() {
1844        let configs = vec![SwqosConfig::Default("https://rpc.example".to_string())];
1845        let normalized = normalize_swqos_configs("https://rpc.example", &configs);
1846
1847        assert_eq!(normalized.len(), 1);
1848        assert!(matches!(normalized[0].swqos_type(), SwqosType::Default));
1849    }
1850
1851    #[test]
1852    fn simple_buy_hot_path_maps_to_low_level_params() {
1853        let simple = SimpleBuyParams {
1854            dex_type: DexType::PumpFun,
1855            pay_with: TradeTokenType::SOL,
1856            mint: Pubkey::new_unique(),
1857            amount: BuyAmount::WithMaxInput { quote_amount: 10_000 },
1858            slippage_basis_points: Some(100),
1859            recent_blockhash: Some(Hash::new_unique()),
1860            extension_params: dummy_pumpfun_params(),
1861            gas_fee_strategy: GasFeeStrategy::new(),
1862            account_policy: AccountPolicy::HotPathMinimal,
1863            address_lookup_table_account: None,
1864            wait_tx_confirmed: false,
1865            wait_for_all_submits: false,
1866            durable_nonce: None,
1867            simulate: false,
1868            grpc_recv_us: None,
1869        };
1870
1871        let low: TradeBuyParams = simple.into();
1872
1873        assert!(matches!(low.input_token_type, TradeTokenType::SOL));
1874        assert_eq!(low.input_token_amount, 10_000);
1875        assert_eq!(low.use_exact_sol_amount, Some(false));
1876        assert_eq!(low.fixed_output_token_amount, None);
1877        assert!(!low.create_input_token_ata);
1878        assert!(!low.close_input_token_ata);
1879        assert!(!low.create_mint_ata);
1880    }
1881
1882    #[test]
1883    fn simple_buy_auto_creates_target_mint_ata() {
1884        let simple = SimpleBuyParams {
1885            dex_type: DexType::PumpFun,
1886            pay_with: TradeTokenType::SOL,
1887            mint: Pubkey::new_unique(),
1888            amount: BuyAmount::ExactOutput { output_amount: 42, max_input_amount: 10_000 },
1889            slippage_basis_points: None,
1890            recent_blockhash: Some(Hash::new_unique()),
1891            extension_params: dummy_pumpfun_params(),
1892            gas_fee_strategy: GasFeeStrategy::new(),
1893            account_policy: AccountPolicy::Auto,
1894            address_lookup_table_account: None,
1895            wait_tx_confirmed: false,
1896            wait_for_all_submits: false,
1897            durable_nonce: None,
1898            simulate: false,
1899            grpc_recv_us: None,
1900        };
1901
1902        let low: TradeBuyParams = simple.into();
1903
1904        assert_eq!(low.input_token_amount, 10_000);
1905        assert_eq!(low.fixed_output_token_amount, Some(42));
1906        assert_eq!(low.use_exact_sol_amount, Some(true));
1907        assert!(low.create_mint_ata);
1908        assert!(!low.create_input_token_ata);
1909    }
1910
1911    #[test]
1912    fn simple_buy_builder_sets_defaults_and_overrides() {
1913        let simple = SimpleBuyParams::new(
1914            DexType::PumpFun,
1915            TradeTokenType::SOL,
1916            Pubkey::new_unique(),
1917            BuyAmount::ExactInput(10_000),
1918            dummy_pumpfun_params(),
1919            Hash::new_unique(),
1920            GasFeeStrategy::new(),
1921        )
1922        .slippage_basis_points(250)
1923        .account_policy(AccountPolicy::HotPathMinimal);
1924
1925        let low: TradeBuyParams = simple.into();
1926
1927        assert_eq!(low.slippage_basis_points, Some(250));
1928        assert_eq!(low.use_exact_sol_amount, Some(true));
1929        assert!(!low.create_input_token_ata);
1930        assert!(!low.create_mint_ata);
1931        assert!(!low.wait_tx_confirmed);
1932    }
1933
1934    #[test]
1935    fn simple_buy_builder_can_use_durable_nonce() {
1936        let nonce_account = Pubkey::new_unique();
1937        let nonce_hash = Hash::new_unique();
1938        let durable_nonce = DurableNonceInfo {
1939            nonce_account: Some(nonce_account),
1940            current_nonce: Some(nonce_hash),
1941        };
1942
1943        let simple = SimpleBuyParams::new(
1944            DexType::PumpFun,
1945            TradeTokenType::SOL,
1946            Pubkey::new_unique(),
1947            BuyAmount::ExactInput(10_000),
1948            dummy_pumpfun_params(),
1949            Hash::new_unique(),
1950            GasFeeStrategy::new(),
1951        )
1952        .durable_nonce(durable_nonce.clone());
1953
1954        let low: TradeBuyParams = simple.into();
1955
1956        assert!(low.recent_blockhash.is_none());
1957        assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
1958        assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
1959    }
1960
1961    #[test]
1962    fn simple_sell_auto_creates_non_sol_output_ata() {
1963        let simple = SimpleSellParams {
1964            dex_type: DexType::PumpFun,
1965            receive_as: TradeTokenType::USDC,
1966            mint: Pubkey::new_unique(),
1967            amount: SellAmount::ExactInput(50_000),
1968            slippage_basis_points: None,
1969            recent_blockhash: Some(Hash::new_unique()),
1970            extension_params: dummy_pumpfun_params(),
1971            gas_fee_strategy: GasFeeStrategy::new(),
1972            account_policy: AccountPolicy::Auto,
1973            address_lookup_table_account: None,
1974            wait_tx_confirmed: false,
1975            wait_for_all_submits: false,
1976            durable_nonce: None,
1977            simulate: false,
1978            with_tip: true,
1979            grpc_recv_us: None,
1980        };
1981
1982        let low: TradeSellParams = simple.into();
1983
1984        assert!(matches!(low.output_token_type, TradeTokenType::USDC));
1985        assert_eq!(low.input_token_amount, 50_000);
1986        assert!(low.create_output_token_ata);
1987        assert!(!low.close_output_token_ata);
1988        assert!(!low.close_mint_token_ata);
1989    }
1990
1991    #[test]
1992    fn simple_sell_builder_can_use_durable_nonce() {
1993        let nonce_account = Pubkey::new_unique();
1994        let nonce_hash = Hash::new_unique();
1995        let durable_nonce = DurableNonceInfo {
1996            nonce_account: Some(nonce_account),
1997            current_nonce: Some(nonce_hash),
1998        };
1999
2000        let simple = SimpleSellParams::new(
2001            DexType::PumpFun,
2002            TradeTokenType::SOL,
2003            Pubkey::new_unique(),
2004            SellAmount::ExactInput(50_000),
2005            dummy_pumpfun_params(),
2006            Hash::new_unique(),
2007            GasFeeStrategy::new(),
2008        )
2009        .durable_nonce(durable_nonce.clone());
2010
2011        let low: TradeSellParams = simple.into();
2012
2013        assert!(low.recent_blockhash.is_none());
2014        assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.nonce_account), Some(nonce_account));
2015        assert_eq!(low.durable_nonce.as_ref().and_then(|n| n.current_nonce), Some(nonce_hash));
2016    }
2017}