1use 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#[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
62pub 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#[derive(Clone, PartialEq)]
78pub enum TradeTokenType {
79 SOL,
80 WSOL,
81 USD1,
82 USDC,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum AccountPolicy {
92 Auto,
100 HotPathMinimal,
106 CreateMissing,
112 AssumePrepared,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum BuyAmount {
126 ExactInput(u64),
132 ExactOutput { output_amount: u64, max_input_amount: u64 },
137 WithMaxInput { quote_amount: u64 },
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum SellAmount {
152 ExactInput(u64),
157 ExactOutput { output_amount: u64, max_input_amount: u64 },
162}
163
164#[derive(Clone)]
171pub struct SimpleBuyParams {
172 pub dex_type: DexType,
174 pub pay_with: TradeTokenType,
181 pub mint: Pubkey,
183 pub amount: BuyAmount,
185 pub slippage_basis_points: Option<u64>,
187 pub recent_blockhash: Option<Hash>,
193 pub extension_params: DexParamEnum,
195 pub gas_fee_strategy: GasFeeStrategy,
197 pub account_policy: AccountPolicy,
199 pub address_lookup_table_account: Option<AddressLookupTableAccount>,
201 pub wait_tx_confirmed: bool,
203 pub wait_for_all_submits: bool,
206 pub durable_nonce: Option<DurableNonceInfo>,
208 pub simulate: bool,
210 pub grpc_recv_us: Option<i64>,
212}
213
214#[derive(Clone)]
219pub struct SimpleSellParams {
220 pub dex_type: DexType,
222 pub receive_as: TradeTokenType,
224 pub mint: Pubkey,
226 pub amount: SellAmount,
228 pub slippage_basis_points: Option<u64>,
230 pub recent_blockhash: Option<Hash>,
232 pub extension_params: DexParamEnum,
234 pub gas_fee_strategy: GasFeeStrategy,
236 pub account_policy: AccountPolicy,
238 pub address_lookup_table_account: Option<AddressLookupTableAccount>,
240 pub wait_tx_confirmed: bool,
242 pub wait_for_all_submits: bool,
245 pub durable_nonce: Option<DurableNonceInfo>,
247 pub simulate: bool,
249 pub with_tip: bool,
251 pub grpc_recv_us: Option<i64>,
253}
254
255impl SimpleBuyParams {
256 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 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 pub fn slippage_basis_points(mut self, value: u64) -> Self {
322 self.slippage_basis_points = Some(value);
323 self
324 }
325
326 pub fn account_policy(mut self, value: AccountPolicy) -> Self {
329 self.account_policy = value;
330 self
331 }
332
333 pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
335 self.address_lookup_table_account = Some(value);
336 self
337 }
338
339 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 pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
351 self.wait_tx_confirmed = value;
352 self
353 }
354
355 pub fn wait_for_all_submits(mut self, value: bool) -> Self {
357 self.wait_for_all_submits = value;
358 self
359 }
360
361 pub fn simulate(mut self, value: bool) -> Self {
363 self.simulate = value;
364 self
365 }
366
367 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 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 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 pub fn slippage_basis_points(mut self, value: u64) -> Self {
441 self.slippage_basis_points = Some(value);
442 self
443 }
444
445 pub fn account_policy(mut self, value: AccountPolicy) -> Self {
448 self.account_policy = value;
449 self
450 }
451
452 pub fn address_lookup_table_account(mut self, value: AddressLookupTableAccount) -> Self {
454 self.address_lookup_table_account = Some(value);
455 self
456 }
457
458 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 pub fn wait_tx_confirmed(mut self, value: bool) -> Self {
470 self.wait_tx_confirmed = value;
471 self
472 }
473
474 pub fn wait_for_all_submits(mut self, value: bool) -> Self {
476 self.wait_for_all_submits = value;
477 self
478 }
479
480 pub fn simulate(mut self, value: bool) -> Self {
482 self.simulate = value;
483 self
484 }
485
486 pub fn with_tip(mut self, value: bool) -> Self {
488 self.with_tip = value;
489 self
490 }
491
492 pub fn grpc_recv_us(mut self, value: i64) -> Self {
494 self.grpc_recv_us = Some(value);
495 self
496 }
497}
498
499pub struct TradingInfrastructure {
504 pub rpc: Arc<SolanaRpcClient>,
506 pub swqos_clients: Arc<Vec<Arc<SwqosClient>>>,
508 pub config: InfrastructureConfig,
510 pub max_sender_concurrency: usize,
512 pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
514}
515
516impl TradingInfrastructure {
517 pub async fn new(config: InfrastructureConfig) -> Self {
524 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 let rpc = Arc::new(SolanaRpcClient::new_with_commitment(
533 config.rpc_url.clone(),
534 config.commitment.clone(),
535 ));
536
537 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 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 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
694pub 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
711pub struct TradingClient {
717 pub payer: Arc<Keypair>,
719 pub infrastructure: Arc<TradingInfrastructure>,
722 pub middleware_manager: Option<Arc<MiddlewareManager>>,
724 pub use_seed_optimize: bool,
727 pub use_dedicated_sender_threads: bool,
729 pub sender_thread_cores: Option<Arc<Vec<usize>>>,
731 pub max_sender_concurrency: usize,
733 pub effective_core_ids: Arc<Vec<core_affinity::CoreId>>,
735 pub log_enabled: bool,
737 pub check_min_tip: bool,
739}
740
741static INSTANCE: Mutex<Option<Arc<TradingClient>>> = Mutex::new(None);
742
743pub 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#[derive(Clone)]
768pub struct TradeBuyParams {
769 pub dex_type: DexType,
772 pub input_token_type: TradeTokenType,
774 pub mint: Pubkey,
776 pub input_token_amount: u64,
778 pub slippage_basis_points: Option<u64>,
780 pub recent_blockhash: Option<Hash>,
782 pub extension_params: DexParamEnum,
784 pub address_lookup_table_account: Option<AddressLookupTableAccount>,
787 pub wait_tx_confirmed: bool,
789 pub wait_for_all_submits: bool,
794 pub create_input_token_ata: bool,
796 pub close_input_token_ata: bool,
798 pub create_mint_ata: bool,
800 pub durable_nonce: Option<DurableNonceInfo>,
802 pub fixed_output_token_amount: Option<u64>,
805 pub gas_fee_strategy: GasFeeStrategy,
807 pub simulate: bool,
809 pub use_exact_sol_amount: Option<bool>,
814 pub grpc_recv_us: Option<i64>,
816}
817
818#[derive(Clone)]
823pub struct TradeSellParams {
824 pub dex_type: DexType,
827 pub output_token_type: TradeTokenType,
829 pub mint: Pubkey,
831 pub input_token_amount: u64,
833 pub slippage_basis_points: Option<u64>,
835 pub recent_blockhash: Option<Hash>,
837 pub with_tip: bool,
839 pub extension_params: DexParamEnum,
841 pub address_lookup_table_account: Option<AddressLookupTableAccount>,
844 pub wait_tx_confirmed: bool,
846 pub wait_for_all_submits: bool,
851 pub create_output_token_ata: bool,
853 pub close_output_token_ata: bool,
855 pub close_mint_token_ata: bool,
857 pub durable_nonce: Option<DurableNonceInfo>,
859 pub fixed_output_token_amount: Option<u64>,
862 pub gas_fee_strategy: GasFeeStrategy,
864 pub simulate: bool,
866 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, ¶ms.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 pub fn from_infrastructure(
975 payer: Arc<Keypair>,
976 infrastructure: Arc<TradingInfrastructure>,
977 use_seed_optimize: bool,
978 ) -> Self {
979 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 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 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 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 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 #[inline]
1168 pub async fn new(payer: Arc<Keypair>, trade_config: TradeConfig) -> Self {
1169 sdk_log::set_sdk_log_enabled(trade_config.log_enabled);
1171 let _ = crate::common::clock::now_micros();
1173 let infra_config = InfrastructureConfig::from_trade_config(&trade_config);
1175 let infrastructure = Arc::new(TradingInfrastructure::new(infra_config).await);
1176
1177 crate::common::fast_fn::fast_init(&payer.pubkey());
1179
1180 if trade_config.create_wsol_ata_on_startup {
1187 const MIN_SOL_FOR_WSOL_ATA_LAMPORTS: u64 = 500_000; 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 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 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 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 pub fn get_rpc(&self) -> &Arc<SolanaRpcClient> {
1286 &self.infrastructure.rpc
1287 }
1288
1289 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 #[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, 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 #[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 #[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, 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 #[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 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 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 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 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.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 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 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 wrap_wsol_to_sol_without_create(&self.payer.pubkey(), amount)?
1743 } else {
1744 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 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 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}