Skip to main content

scope/chains/
mod.rs

1//! # Blockchain Client Module
2//!
3//! This module provides abstractions and implementations for interacting
4//! with various blockchain networks. It defines a common `ChainClient` trait
5//! that all chain-specific implementations must satisfy.
6//!
7//! ## Capabilities
8//!
9//! All chain clients support:
10//! - **Balance queries** with optional USD valuation via DexScreener
11//! - **Transaction lookup** by hash/signature with full details
12//! - **Transaction history** for addresses with pagination
13//! - **Token balances** (ERC-20, SPL, TRC-20) for portfolio tracking
14//!
15//! ## Supported Chains
16//!
17//! ### EVM-Compatible Chains
18//!
19//! - **Ethereum** - Ethereum Mainnet (via Etherscan V2 API)
20//! - **Polygon** - Polygon PoS
21//! - **Arbitrum** - Arbitrum One
22//! - **Optimism** - Optimism Mainnet
23//! - **Base** - Base (Coinbase L2)
24//! - **BSC** - BNB Smart Chain (Binance)
25//!
26//! ### Non-EVM Chains
27//!
28//! - **Solana** - Solana Mainnet (JSON-RPC with `jsonParsed` encoding)
29//! - **Tron** - Tron Mainnet (TronGrid API, base58check address validation)
30//!
31//! ### DEX Data
32//!
33//! - **DexScreener** - Token prices, volume, liquidity, and trading data across all DEX pairs
34//!
35//! ## Usage
36//!
37//! ### Ethereum/EVM Client
38//!
39//! ```rust,no_run
40//! use scope::chains::{ChainClient, EthereumClient};
41//! use scope::Config;
42//!
43//! #[tokio::main]
44//! async fn main() -> scope::Result<()> {
45//!     let config = Config::load(None)?;
46//!     let client = EthereumClient::new(&config.chains)?;
47//!     
48//!     let balance = client.get_balance("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2").await?;
49//!     println!("Balance: {} ETH", balance.formatted);
50//!     Ok(())
51//! }
52//! ```
53//!
54//! ### Solana Client
55//!
56//! ```rust,no_run
57//! use scope::chains::SolanaClient;
58//! use scope::Config;
59//!
60//! #[tokio::main]
61//! async fn main() -> scope::Result<()> {
62//!     let config = Config::load(None)?;
63//!     let client = SolanaClient::new(&config.chains)?;
64//!     
65//!     let balance = client.get_balance("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy").await?;
66//!     println!("Balance: {} SOL", balance.formatted);
67//!     Ok(())
68//! }
69//! ```
70//!
71//! ### Tron Client
72//!
73//! ```rust,no_run
74//! use scope::chains::TronClient;
75//! use scope::Config;
76//!
77//! #[tokio::main]
78//! async fn main() -> scope::Result<()> {
79//!     let config = Config::load(None)?;
80//!     let client = TronClient::new(&config.chains)?;
81//!     
82//!     let balance = client.get_balance("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf").await?;
83//!     println!("Balance: {} TRX", balance.formatted);
84//!     Ok(())
85//! }
86//! ```
87
88pub mod dex;
89pub mod ethereum;
90pub mod solana;
91pub mod tron;
92
93pub use dex::{DexClient, DexDataSource, DiscoverToken, TokenSearchResult};
94pub use ethereum::{ApiType, EthereumClient};
95pub use solana::{SolanaClient, validate_solana_address, validate_solana_signature};
96pub use tron::{TronClient, validate_tron_address, validate_tron_tx_hash};
97
98use crate::error::Result;
99use async_trait::async_trait;
100use serde::{Deserialize, Serialize};
101
102/// Trait defining common blockchain client operations.
103///
104/// All chain-specific clients must implement this trait to provide
105/// a consistent interface for blockchain interactions.
106///
107/// ## Core Methods
108///
109/// Every implementation must provide: `chain_name`, `native_token_symbol`,
110/// `get_balance`, `get_transaction`, `get_transactions`, `get_block_number`,
111/// `enrich_balance_usd`, and `get_token_balances`.
112///
113/// ## Token Explorer Methods
114///
115/// The token-explorer methods (`get_token_info`, `get_token_holders`,
116/// `get_token_holder_count`) have default implementations that return
117/// "not supported" errors or empty results. Only chains with block-explorer
118/// support for these endpoints (currently EVM chains) need to override them.
119#[async_trait]
120pub trait ChainClient: Send + Sync {
121    /// Returns the name of the blockchain network.
122    fn chain_name(&self) -> &str;
123
124    /// Returns the native token symbol (e.g., "ETH", "MATIC").
125    fn native_token_symbol(&self) -> &str;
126
127    /// Fetches the native token balance for an address.
128    ///
129    /// # Arguments
130    ///
131    /// * `address` - The blockchain address to query
132    ///
133    /// # Returns
134    ///
135    /// Returns a [`Balance`] containing the balance in multiple formats.
136    async fn get_balance(&self, address: &str) -> Result<Balance>;
137
138    /// Enriches a balance with USD valuation via DexScreener.
139    ///
140    /// # Arguments
141    ///
142    /// * `balance` - The balance to enrich with a USD value
143    async fn enrich_balance_usd(&self, balance: &mut Balance);
144
145    /// Fetches transaction details by hash.
146    ///
147    /// # Arguments
148    ///
149    /// * `hash` - The transaction hash to query
150    ///
151    /// # Returns
152    ///
153    /// Returns [`Transaction`] details or an error if not found.
154    async fn get_transaction(&self, hash: &str) -> Result<Transaction>;
155
156    /// Fetches recent transactions for an address.
157    ///
158    /// # Arguments
159    ///
160    /// * `address` - The address to query
161    /// * `limit` - Maximum number of transactions to return
162    ///
163    /// # Returns
164    ///
165    /// Returns a vector of [`Transaction`] objects.
166    async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>>;
167
168    /// Fetches the current block number.
169    async fn get_block_number(&self) -> Result<u64>;
170
171    /// Fetches token balances for an address.
172    ///
173    /// Returns a unified [`TokenBalance`] list regardless of chain
174    /// (ERC-20, SPL, TRC-20 all map to the same type).
175    async fn get_token_balances(&self, address: &str) -> Result<Vec<TokenBalance>>;
176
177    /// Fetches token information for a contract address.
178    ///
179    /// Default implementation returns "not supported" error.
180    /// Override in chain clients that support token info lookups.
181    async fn get_token_info(&self, _address: &str) -> Result<Token> {
182        Err(crate::error::ScopeError::Chain(
183            "Token info lookup not supported on this chain".to_string(),
184        ))
185    }
186
187    /// Fetches top token holders for a contract address.
188    ///
189    /// Default implementation returns an empty vector.
190    /// Override in chain clients that support holder lookups.
191    async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
192        Ok(Vec::new())
193    }
194
195    /// Fetches total token holder count for a contract address.
196    ///
197    /// Default implementation returns 0.
198    /// Override in chain clients that support holder count lookups.
199    async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
200        Ok(0)
201    }
202}
203
204/// Factory trait for creating chain clients and DEX data sources.
205///
206/// Bundles both chain and DEX client creation so CLI functions
207/// only need one injected dependency instead of two.
208///
209/// # Example
210///
211/// ```rust,no_run
212/// use scope::chains::{ChainClientFactory, DefaultClientFactory};
213/// use scope::Config;
214///
215/// let config = Config::default();
216/// let factory = DefaultClientFactory { chains_config: config.chains.clone() };
217/// let client = factory.create_chain_client("ethereum").unwrap();
218/// ```
219pub trait ChainClientFactory: Send + Sync {
220    /// Creates a chain client for the given blockchain network.
221    ///
222    /// # Arguments
223    ///
224    /// * `chain` - The chain name (e.g., "ethereum", "solana", "tron")
225    fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>>;
226
227    /// Creates a DEX data source client.
228    fn create_dex_client(&self) -> Box<dyn DexDataSource>;
229}
230
231/// Default factory that creates real chain clients from configuration.
232pub struct DefaultClientFactory {
233    /// Chain configuration containing API keys and endpoints.
234    pub chains_config: crate::config::ChainsConfig,
235}
236
237impl ChainClientFactory for DefaultClientFactory {
238    fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>> {
239        match chain.to_lowercase().as_str() {
240            "solana" | "sol" => Ok(Box::new(SolanaClient::new(&self.chains_config)?)),
241            "tron" | "trx" => Ok(Box::new(TronClient::new(&self.chains_config)?)),
242            _ => Ok(Box::new(EthereumClient::for_chain(
243                chain,
244                &self.chains_config,
245            )?)),
246        }
247    }
248
249    fn create_dex_client(&self) -> Box<dyn DexDataSource> {
250        Box::new(DexClient::new())
251    }
252}
253
254/// Balance representation with multiple formats.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct Balance {
257    /// Raw balance in smallest unit (e.g., wei).
258    pub raw: String,
259
260    /// Human-readable formatted balance.
261    pub formatted: String,
262
263    /// Number of decimals for the token.
264    pub decimals: u8,
265
266    /// Token symbol.
267    pub symbol: String,
268
269    /// USD value (if available).
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub usd_value: Option<f64>,
272}
273
274/// Transaction information.
275#[derive(Debug, Clone, Serialize, Deserialize)]
276pub struct Transaction {
277    /// Transaction hash.
278    pub hash: String,
279
280    /// Block number (None if pending).
281    pub block_number: Option<u64>,
282
283    /// Block timestamp (None if pending).
284    pub timestamp: Option<u64>,
285
286    /// Sender address.
287    pub from: String,
288
289    /// Recipient address (None for contract creation).
290    pub to: Option<String>,
291
292    /// Value transferred in native token.
293    pub value: String,
294
295    /// Gas limit.
296    pub gas_limit: u64,
297
298    /// Gas used (None if pending).
299    pub gas_used: Option<u64>,
300
301    /// Gas price in wei.
302    pub gas_price: String,
303
304    /// Transaction nonce.
305    pub nonce: u64,
306
307    /// Input data.
308    pub input: String,
309
310    /// Transaction status (None if pending, Some(true) for success).
311    pub status: Option<bool>,
312}
313
314/// Token information.
315#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct Token {
317    /// Contract address.
318    pub contract_address: String,
319
320    /// Token symbol.
321    pub symbol: String,
322
323    /// Token name.
324    pub name: String,
325
326    /// Decimal places.
327    pub decimals: u8,
328}
329
330/// Token balance for an address.
331#[derive(Debug, Clone, Serialize, Deserialize)]
332pub struct TokenBalance {
333    /// Token information.
334    pub token: Token,
335
336    /// Raw balance.
337    pub balance: String,
338
339    /// Formatted balance.
340    pub formatted_balance: String,
341
342    /// USD value (if available).
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub usd_value: Option<f64>,
345}
346
347// ============================================================================
348// Token Analytics Types
349// ============================================================================
350
351/// A token holder with their balance and percentage of supply.
352#[derive(Debug, Clone, Serialize, Deserialize)]
353pub struct TokenHolder {
354    /// Holder's address.
355    pub address: String,
356
357    /// Raw balance amount.
358    pub balance: String,
359
360    /// Formatted balance with proper decimals.
361    pub formatted_balance: String,
362
363    /// Percentage of total supply held.
364    pub percentage: f64,
365
366    /// Rank among all holders (1 = largest).
367    pub rank: u32,
368}
369
370/// A price data point for historical charting.
371#[derive(Debug, Clone, Serialize, Deserialize)]
372pub struct PricePoint {
373    /// Unix timestamp in seconds.
374    pub timestamp: i64,
375
376    /// Price in USD.
377    pub price: f64,
378}
379
380/// A volume data point for historical charting.
381#[derive(Debug, Clone, Serialize, Deserialize)]
382pub struct VolumePoint {
383    /// Unix timestamp in seconds.
384    pub timestamp: i64,
385
386    /// Volume in USD.
387    pub volume: f64,
388}
389
390/// A holder count data point for historical charting.
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct HolderCountPoint {
393    /// Unix timestamp in seconds.
394    pub timestamp: i64,
395
396    /// Number of holders.
397    pub count: u64,
398}
399
400/// DEX trading pair information.
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct DexPair {
403    /// DEX name (e.g., "Uniswap V3", "SushiSwap").
404    pub dex_name: String,
405
406    /// Pair address on the DEX.
407    pub pair_address: String,
408
409    /// Base token symbol.
410    pub base_token: String,
411
412    /// Quote token symbol.
413    pub quote_token: String,
414
415    /// Current price in USD.
416    pub price_usd: f64,
417
418    /// 24h trading volume in USD.
419    pub volume_24h: f64,
420
421    /// Liquidity in USD.
422    pub liquidity_usd: f64,
423
424    /// Price change percentage in 24h.
425    pub price_change_24h: f64,
426
427    /// Buy transactions in 24h.
428    pub buys_24h: u64,
429
430    /// Sell transactions in 24h.
431    pub sells_24h: u64,
432
433    /// Buy transactions in 6h.
434    pub buys_6h: u64,
435
436    /// Sell transactions in 6h.
437    pub sells_6h: u64,
438
439    /// Buy transactions in 1h.
440    pub buys_1h: u64,
441
442    /// Sell transactions in 1h.
443    pub sells_1h: u64,
444
445    /// Pair creation timestamp.
446    pub pair_created_at: Option<i64>,
447
448    /// Direct URL to this pair on DexScreener.
449    pub url: Option<String>,
450}
451
452/// Comprehensive token analytics data.
453#[derive(Debug, Clone, Serialize, Deserialize)]
454pub struct TokenAnalytics {
455    /// Token information.
456    pub token: Token,
457
458    /// Blockchain network name.
459    pub chain: String,
460
461    /// Top token holders.
462    pub holders: Vec<TokenHolder>,
463
464    /// Total number of holders.
465    pub total_holders: u64,
466
467    /// 24-hour trading volume in USD.
468    pub volume_24h: f64,
469
470    /// 7-day trading volume in USD.
471    pub volume_7d: f64,
472
473    /// Current price in USD.
474    pub price_usd: f64,
475
476    /// 24-hour price change percentage.
477    pub price_change_24h: f64,
478
479    /// 7-day price change percentage.
480    pub price_change_7d: f64,
481
482    /// Total liquidity across DEXs in USD.
483    pub liquidity_usd: f64,
484
485    /// Market capitalization (if available).
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub market_cap: Option<f64>,
488
489    /// Fully diluted valuation (if available).
490    #[serde(skip_serializing_if = "Option::is_none")]
491    pub fdv: Option<f64>,
492
493    /// Total supply.
494    #[serde(skip_serializing_if = "Option::is_none")]
495    pub total_supply: Option<String>,
496
497    /// Circulating supply.
498    #[serde(skip_serializing_if = "Option::is_none")]
499    pub circulating_supply: Option<String>,
500
501    /// Historical price data for charting.
502    pub price_history: Vec<PricePoint>,
503
504    /// Historical volume data for charting.
505    pub volume_history: Vec<VolumePoint>,
506
507    /// Historical holder count data for charting.
508    pub holder_history: Vec<HolderCountPoint>,
509
510    /// DEX trading pairs.
511    pub dex_pairs: Vec<DexPair>,
512
513    /// Timestamp when this data was fetched.
514    pub fetched_at: i64,
515
516    /// Percentage of supply held by top 10 holders.
517    #[serde(skip_serializing_if = "Option::is_none")]
518    pub top_10_concentration: Option<f64>,
519
520    /// Percentage of supply held by top 50 holders.
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub top_50_concentration: Option<f64>,
523
524    /// Percentage of supply held by top 100 holders.
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub top_100_concentration: Option<f64>,
527
528    /// 6-hour price change percentage.
529    pub price_change_6h: f64,
530
531    /// 1-hour price change percentage.
532    pub price_change_1h: f64,
533
534    /// Total buy transactions in 24 hours.
535    pub total_buys_24h: u64,
536
537    /// Total sell transactions in 24 hours.
538    pub total_sells_24h: u64,
539
540    /// Total buy transactions in 6 hours.
541    pub total_buys_6h: u64,
542
543    /// Total sell transactions in 6 hours.
544    pub total_sells_6h: u64,
545
546    /// Total buy transactions in 1 hour.
547    pub total_buys_1h: u64,
548
549    /// Total sell transactions in 1 hour.
550    pub total_sells_1h: u64,
551
552    /// Token age in hours (since earliest pair creation).
553    #[serde(skip_serializing_if = "Option::is_none")]
554    pub token_age_hours: Option<f64>,
555
556    /// Token image URL.
557    #[serde(skip_serializing_if = "Option::is_none")]
558    pub image_url: Option<String>,
559
560    /// Token website URLs.
561    pub websites: Vec<String>,
562
563    /// Token social media links.
564    pub socials: Vec<TokenSocial>,
565
566    /// DexScreener URL for the primary pair.
567    #[serde(skip_serializing_if = "Option::is_none")]
568    pub dexscreener_url: Option<String>,
569}
570
571/// Social media link for a token.
572#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
573pub struct TokenSocial {
574    /// Platform name (twitter, telegram, discord, etc.)
575    pub platform: String,
576    /// URL or handle for the social account.
577    pub url: String,
578}
579
580// ============================================================================
581// Chain Inference
582// ============================================================================
583
584/// Infers the blockchain from an address format.
585///
586/// Returns `Some(chain_name)` if the address format is unambiguous,
587/// or `None` if the format is not recognized.
588///
589/// # Supported Formats
590///
591/// - **EVM** (ethereum): `0x` prefix + 40 hex chars (42 total)
592/// - **Tron**: Starts with `T` + 34 chars (Base58Check)
593/// - **Solana**: Base58, 32-44 chars, decodes to 32 bytes
594///
595/// # Examples
596///
597/// ```
598/// use scope::chains::infer_chain_from_address;
599///
600/// assert_eq!(infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"), Some("ethereum"));
601/// assert_eq!(infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"), Some("tron"));
602/// assert_eq!(infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"), Some("solana"));
603/// assert_eq!(infer_chain_from_address("invalid"), None);
604/// ```
605pub fn infer_chain_from_address(address: &str) -> Option<&'static str> {
606    // Tron: starts with 'T', 34 chars, valid base58
607    if address.starts_with('T') && address.len() == 34 && bs58::decode(address).into_vec().is_ok() {
608        return Some("tron");
609    }
610
611    // EVM: 0x prefix, 42 chars total (40 hex + "0x")
612    if address.starts_with("0x")
613        && address.len() == 42
614        && address[2..].chars().all(|c| c.is_ascii_hexdigit())
615    {
616        return Some("ethereum");
617    }
618
619    // Solana: base58, 32-44 chars, decodes to 32 bytes
620    if address.len() >= 32
621        && address.len() <= 44
622        && let Ok(decoded) = bs58::decode(address).into_vec()
623        && decoded.len() == 32
624    {
625        return Some("solana");
626    }
627
628    None
629}
630
631/// Infers the blockchain from a transaction hash format.
632///
633/// Returns `Some(chain_name)` if the hash format is unambiguous,
634/// or `None` if the format is not recognized.
635///
636/// # Supported Formats
637///
638/// - **EVM** (ethereum): `0x` prefix + 64 hex chars (66 total)
639/// - **Tron**: 64 hex chars (no prefix)
640/// - **Solana**: Base58, 80-90 chars, decodes to 64 bytes
641///
642/// # Examples
643///
644/// ```
645/// use scope::chains::infer_chain_from_hash;
646///
647/// // EVM hash
648/// let evm_hash = "0xabc123def456789012345678901234567890123456789012345678901234abcd";
649/// assert_eq!(infer_chain_from_hash(evm_hash), Some("ethereum"));
650///
651/// // Tron hash (64 hex chars, no 0x prefix)
652/// let tron_hash = "abc123def456789012345678901234567890123456789012345678901234abcd";
653/// assert_eq!(infer_chain_from_hash(tron_hash), Some("tron"));
654/// ```
655pub fn infer_chain_from_hash(hash: &str) -> Option<&'static str> {
656    // EVM: 0x prefix, 66 chars total (64 hex + "0x")
657    if hash.starts_with("0x")
658        && hash.len() == 66
659        && hash[2..].chars().all(|c| c.is_ascii_hexdigit())
660    {
661        return Some("ethereum");
662    }
663
664    // Tron: 64 hex chars, no prefix
665    if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
666        return Some("tron");
667    }
668
669    // Solana: base58, 80-90 chars, decodes to 64 bytes
670    if hash.len() >= 80
671        && hash.len() <= 90
672        && let Ok(decoded) = bs58::decode(hash).into_vec()
673        && decoded.len() == 64
674    {
675        return Some("solana");
676    }
677
678    None
679}
680
681// ============================================================================
682// Unit Tests
683// ============================================================================
684
685#[cfg(test)]
686mod tests {
687    use super::*;
688
689    #[test]
690    fn test_balance_serialization() {
691        let balance = Balance {
692            raw: "1000000000000000000".to_string(),
693            formatted: "1.0".to_string(),
694            decimals: 18,
695            symbol: "ETH".to_string(),
696            usd_value: Some(3500.0),
697        };
698
699        let json = serde_json::to_string(&balance).unwrap();
700        assert!(json.contains("1000000000000000000"));
701        assert!(json.contains("1.0"));
702        assert!(json.contains("ETH"));
703        assert!(json.contains("3500"));
704
705        let deserialized: Balance = serde_json::from_str(&json).unwrap();
706        assert_eq!(deserialized.raw, balance.raw);
707        assert_eq!(deserialized.decimals, 18);
708    }
709
710    #[test]
711    fn test_balance_without_usd() {
712        let balance = Balance {
713            raw: "1000000000000000000".to_string(),
714            formatted: "1.0".to_string(),
715            decimals: 18,
716            symbol: "ETH".to_string(),
717            usd_value: None,
718        };
719
720        let json = serde_json::to_string(&balance).unwrap();
721        assert!(!json.contains("usd_value"));
722    }
723
724    #[test]
725    fn test_transaction_serialization() {
726        let tx = Transaction {
727            hash: "0xabc123".to_string(),
728            block_number: Some(12345678),
729            timestamp: Some(1700000000),
730            from: "0xfrom".to_string(),
731            to: Some("0xto".to_string()),
732            value: "1.0".to_string(),
733            gas_limit: 21000,
734            gas_used: Some(21000),
735            gas_price: "20000000000".to_string(),
736            nonce: 42,
737            input: "0x".to_string(),
738            status: Some(true),
739        };
740
741        let json = serde_json::to_string(&tx).unwrap();
742        assert!(json.contains("0xabc123"));
743        assert!(json.contains("12345678"));
744        assert!(json.contains("0xfrom"));
745        assert!(json.contains("0xto"));
746
747        let deserialized: Transaction = serde_json::from_str(&json).unwrap();
748        assert_eq!(deserialized.hash, tx.hash);
749        assert_eq!(deserialized.nonce, 42);
750    }
751
752    #[test]
753    fn test_pending_transaction_serialization() {
754        let tx = Transaction {
755            hash: "0xpending".to_string(),
756            block_number: None,
757            timestamp: None,
758            from: "0xfrom".to_string(),
759            to: Some("0xto".to_string()),
760            value: "1.0".to_string(),
761            gas_limit: 21000,
762            gas_used: None,
763            gas_price: "20000000000".to_string(),
764            nonce: 0,
765            input: "0x".to_string(),
766            status: None,
767        };
768
769        let json = serde_json::to_string(&tx).unwrap();
770        assert!(json.contains("0xpending"));
771        assert!(json.contains("null")); // None values serialize as null
772
773        let deserialized: Transaction = serde_json::from_str(&json).unwrap();
774        assert!(deserialized.block_number.is_none());
775        assert!(deserialized.status.is_none());
776    }
777
778    #[test]
779    fn test_contract_creation_transaction() {
780        let tx = Transaction {
781            hash: "0xcreate".to_string(),
782            block_number: Some(100),
783            timestamp: Some(1700000000),
784            from: "0xdeployer".to_string(),
785            to: None, // Contract creation
786            value: "0".to_string(),
787            gas_limit: 1000000,
788            gas_used: Some(500000),
789            gas_price: "20000000000".to_string(),
790            nonce: 0,
791            input: "0x608060...".to_string(),
792            status: Some(true),
793        };
794
795        let json = serde_json::to_string(&tx).unwrap();
796        assert!(json.contains("\"to\":null"));
797    }
798
799    #[test]
800    fn test_token_serialization() {
801        let token = Token {
802            contract_address: "0xtoken".to_string(),
803            symbol: "USDC".to_string(),
804            name: "USD Coin".to_string(),
805            decimals: 6,
806        };
807
808        let json = serde_json::to_string(&token).unwrap();
809        assert!(json.contains("USDC"));
810        assert!(json.contains("USD Coin"));
811        assert!(json.contains("\"decimals\":6"));
812
813        let deserialized: Token = serde_json::from_str(&json).unwrap();
814        assert_eq!(deserialized.decimals, 6);
815    }
816
817    #[test]
818    fn test_token_balance_serialization() {
819        let token_balance = TokenBalance {
820            token: Token {
821                contract_address: "0xtoken".to_string(),
822                symbol: "USDC".to_string(),
823                name: "USD Coin".to_string(),
824                decimals: 6,
825            },
826            balance: "1000000".to_string(),
827            formatted_balance: "1.0".to_string(),
828            usd_value: Some(1.0),
829        };
830
831        let json = serde_json::to_string(&token_balance).unwrap();
832        assert!(json.contains("USDC"));
833        assert!(json.contains("1000000"));
834        assert!(json.contains("1.0"));
835    }
836
837    #[test]
838    fn test_balance_debug() {
839        let balance = Balance {
840            raw: "1000".to_string(),
841            formatted: "0.001".to_string(),
842            decimals: 18,
843            symbol: "ETH".to_string(),
844            usd_value: None,
845        };
846
847        let debug_str = format!("{:?}", balance);
848        assert!(debug_str.contains("Balance"));
849        assert!(debug_str.contains("1000"));
850    }
851
852    #[test]
853    fn test_transaction_debug() {
854        let tx = Transaction {
855            hash: "0xtest".to_string(),
856            block_number: Some(1),
857            timestamp: Some(0),
858            from: "0x1".to_string(),
859            to: Some("0x2".to_string()),
860            value: "0".to_string(),
861            gas_limit: 21000,
862            gas_used: Some(21000),
863            gas_price: "0".to_string(),
864            nonce: 0,
865            input: "0x".to_string(),
866            status: Some(true),
867        };
868
869        let debug_str = format!("{:?}", tx);
870        assert!(debug_str.contains("Transaction"));
871        assert!(debug_str.contains("0xtest"));
872    }
873
874    // ============================================================================
875    // Chain Inference Tests
876    // ============================================================================
877
878    #[test]
879    fn test_infer_chain_from_address_evm() {
880        // Valid EVM addresses (0x + 40 hex chars)
881        assert_eq!(
882            super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
883            Some("ethereum")
884        );
885        assert_eq!(
886            super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
887            Some("ethereum")
888        );
889        assert_eq!(
890            super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
891            Some("ethereum")
892        );
893    }
894
895    #[test]
896    fn test_infer_chain_from_address_tron() {
897        // Valid Tron addresses (T + 33 chars = 34 total, base58)
898        assert_eq!(
899            super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
900            Some("tron")
901        );
902        assert_eq!(
903            super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
904            Some("tron")
905        );
906    }
907
908    #[test]
909    fn test_infer_chain_from_address_solana() {
910        // Valid Solana addresses (base58, 32-44 chars, decodes to 32 bytes)
911        assert_eq!(
912            super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
913            Some("solana")
914        );
915        // System program address
916        assert_eq!(
917            super::infer_chain_from_address("11111111111111111111111111111111"),
918            Some("solana")
919        );
920    }
921
922    #[test]
923    fn test_infer_chain_from_address_invalid() {
924        // Too short
925        assert_eq!(super::infer_chain_from_address("0x123"), None);
926        // Invalid characters
927        assert_eq!(super::infer_chain_from_address("not_an_address"), None);
928        // Empty
929        assert_eq!(super::infer_chain_from_address(""), None);
930        // EVM-like but wrong length
931        assert_eq!(super::infer_chain_from_address("0x123456"), None);
932        // Tron-like but not starting with T
933        assert_eq!(
934            super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
935            None
936        );
937    }
938
939    #[test]
940    fn test_infer_chain_from_hash_evm() {
941        // Valid EVM transaction hash (0x + 64 hex chars)
942        assert_eq!(
943            super::infer_chain_from_hash(
944                "0xabc123def456789012345678901234567890123456789012345678901234abcd"
945            ),
946            Some("ethereum")
947        );
948        assert_eq!(
949            super::infer_chain_from_hash(
950                "0x0000000000000000000000000000000000000000000000000000000000000000"
951            ),
952            Some("ethereum")
953        );
954    }
955
956    #[test]
957    fn test_infer_chain_from_hash_tron() {
958        // Valid Tron transaction hash (64 hex chars, no 0x prefix)
959        assert_eq!(
960            super::infer_chain_from_hash(
961                "abc123def456789012345678901234567890123456789012345678901234abcd"
962            ),
963            Some("tron")
964        );
965        assert_eq!(
966            super::infer_chain_from_hash(
967                "0000000000000000000000000000000000000000000000000000000000000000"
968            ),
969            Some("tron")
970        );
971    }
972
973    #[test]
974    fn test_infer_chain_from_hash_solana() {
975        // Valid Solana signature (base58, 80-90 chars, decodes to 64 bytes)
976        // This is a made-up example that fits the pattern
977        let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
978        assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
979    }
980
981    #[test]
982    fn test_infer_chain_from_hash_invalid() {
983        // Too short
984        assert_eq!(super::infer_chain_from_hash("0x123"), None);
985        // Invalid
986        assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
987        // Empty
988        assert_eq!(super::infer_chain_from_hash(""), None);
989        // 64 chars but with invalid hex (contains 'g')
990        assert_eq!(
991            super::infer_chain_from_hash(
992                "abc123gef456789012345678901234567890123456789012345678901234abcd"
993            ),
994            None
995        );
996    }
997
998    // ============================================================================
999    // DefaultClientFactory Tests
1000    // ============================================================================
1001
1002    #[test]
1003    fn test_default_client_factory_create_dex_client() {
1004        let config = crate::config::ChainsConfig::default();
1005        let factory = DefaultClientFactory {
1006            chains_config: config,
1007        };
1008        let dex = factory.create_dex_client();
1009        // Just verify it returns without panicking - the client is a Box<dyn DexDataSource>
1010        let _ = format!("{:?}", std::mem::size_of_val(&dex));
1011    }
1012
1013    #[test]
1014    fn test_default_client_factory_create_ethereum_client() {
1015        let config = crate::config::ChainsConfig::default();
1016        let factory = DefaultClientFactory {
1017            chains_config: config,
1018        };
1019        // ethereum, polygon, etc use EthereumClient::for_chain
1020        let client = factory.create_chain_client("ethereum");
1021        assert!(client.is_ok());
1022        assert_eq!(client.unwrap().chain_name(), "ethereum");
1023    }
1024
1025    #[test]
1026    fn test_default_client_factory_create_polygon_client() {
1027        let config = crate::config::ChainsConfig::default();
1028        let factory = DefaultClientFactory {
1029            chains_config: config,
1030        };
1031        let client = factory.create_chain_client("polygon");
1032        assert!(client.is_ok());
1033        assert_eq!(client.unwrap().chain_name(), "polygon");
1034    }
1035
1036    #[test]
1037    fn test_default_client_factory_create_solana_client() {
1038        let config = crate::config::ChainsConfig::default();
1039        let factory = DefaultClientFactory {
1040            chains_config: config,
1041        };
1042        let client = factory.create_chain_client("solana");
1043        assert!(client.is_ok());
1044        assert_eq!(client.unwrap().chain_name(), "solana");
1045    }
1046
1047    #[test]
1048    fn test_default_client_factory_create_sol_alias() {
1049        let config = crate::config::ChainsConfig::default();
1050        let factory = DefaultClientFactory {
1051            chains_config: config,
1052        };
1053        let client = factory.create_chain_client("sol");
1054        assert!(client.is_ok());
1055        assert_eq!(client.unwrap().chain_name(), "solana");
1056    }
1057
1058    #[test]
1059    fn test_default_client_factory_create_tron_client() {
1060        let config = crate::config::ChainsConfig::default();
1061        let factory = DefaultClientFactory {
1062            chains_config: config,
1063        };
1064        let client = factory.create_chain_client("tron");
1065        assert!(client.is_ok());
1066        assert_eq!(client.unwrap().chain_name(), "tron");
1067    }
1068
1069    #[test]
1070    fn test_default_client_factory_create_trx_alias() {
1071        let config = crate::config::ChainsConfig::default();
1072        let factory = DefaultClientFactory {
1073            chains_config: config,
1074        };
1075        let client = factory.create_chain_client("trx");
1076        assert!(client.is_ok());
1077        assert_eq!(client.unwrap().chain_name(), "tron");
1078    }
1079
1080    // ============================================================================
1081    // ChainClient trait default method tests
1082    // ============================================================================
1083
1084    #[tokio::test]
1085    async fn test_chain_client_default_get_token_info() {
1086        use super::mocks::MockChainClient;
1087        // Create a client without token_info set (None)
1088        let client = MockChainClient::new("ethereum", "ETH");
1089        let result = client.get_token_info("0xsometoken").await;
1090        assert!(result.is_err());
1091    }
1092
1093    #[tokio::test]
1094    async fn test_chain_client_default_get_token_holders() {
1095        use super::mocks::MockChainClient;
1096        let client = MockChainClient::new("ethereum", "ETH");
1097        let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1098        assert!(holders.is_empty());
1099    }
1100
1101    #[tokio::test]
1102    async fn test_chain_client_default_get_token_holder_count() {
1103        use super::mocks::MockChainClient;
1104        let client = MockChainClient::new("ethereum", "ETH");
1105        let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1106        assert_eq!(count, 0);
1107    }
1108
1109    #[tokio::test]
1110    async fn test_mock_client_factory_creates_chain_client() {
1111        use super::mocks::MockClientFactory;
1112        let factory = MockClientFactory::new();
1113        let client = factory.create_chain_client("anything").unwrap();
1114        assert_eq!(client.chain_name(), "ethereum"); // defaults to ethereum mock
1115    }
1116
1117    #[tokio::test]
1118    async fn test_mock_client_factory_creates_dex_client() {
1119        use super::mocks::MockClientFactory;
1120        let factory = MockClientFactory::new();
1121        let dex = factory.create_dex_client();
1122        let price = dex.get_token_price("ethereum", "0xtest").await;
1123        assert_eq!(price, Some(1.0));
1124    }
1125
1126    #[tokio::test]
1127    async fn test_mock_chain_client_balance() {
1128        use super::mocks::MockChainClient;
1129        let client = MockChainClient::new("ethereum", "ETH");
1130        let balance = client.get_balance("0xtest").await.unwrap();
1131        assert_eq!(balance.formatted, "1.0");
1132        assert_eq!(balance.symbol, "ETH");
1133        assert_eq!(balance.usd_value, Some(2500.0));
1134    }
1135
1136    #[tokio::test]
1137    async fn test_mock_chain_client_transaction() {
1138        use super::mocks::MockChainClient;
1139        let client = MockChainClient::new("ethereum", "ETH");
1140        let tx = client.get_transaction("0xanyhash").await.unwrap();
1141        assert_eq!(tx.hash, "0xmocktx");
1142        assert_eq!(tx.nonce, 42);
1143    }
1144
1145    #[tokio::test]
1146    async fn test_mock_chain_client_block_number() {
1147        use super::mocks::MockChainClient;
1148        let client = MockChainClient::new("ethereum", "ETH");
1149        let block = client.get_block_number().await.unwrap();
1150        assert_eq!(block, 12345678);
1151    }
1152
1153    #[tokio::test]
1154    async fn test_mock_dex_source_data() {
1155        use super::mocks::MockDexSource;
1156        let dex = MockDexSource::new();
1157        let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1158        assert_eq!(data.symbol, "MOCK");
1159        assert_eq!(data.price_usd, 1.0);
1160    }
1161
1162    #[tokio::test]
1163    async fn test_mock_dex_source_search() {
1164        use super::mocks::MockDexSource;
1165        let dex = MockDexSource::new();
1166        let results = dex.search_tokens("test", None).await.unwrap();
1167        assert!(results.is_empty());
1168    }
1169
1170    #[tokio::test]
1171    async fn test_mock_dex_source_native_price() {
1172        use super::mocks::MockDexSource;
1173        let dex = MockDexSource::new();
1174        let price = dex.get_native_token_price("ethereum").await;
1175        assert_eq!(price, Some(2500.0));
1176    }
1177
1178    // ========================================================================
1179    // Default ChainClient trait method tests
1180    // ========================================================================
1181
1182    /// Minimal ChainClient impl that uses all default methods.
1183    struct MinimalChainClient;
1184
1185    #[async_trait::async_trait]
1186    impl ChainClient for MinimalChainClient {
1187        fn chain_name(&self) -> &str {
1188            "test"
1189        }
1190
1191        fn native_token_symbol(&self) -> &str {
1192            "TEST"
1193        }
1194
1195        async fn get_balance(&self, _address: &str) -> Result<Balance> {
1196            Ok(Balance {
1197                raw: "0".to_string(),
1198                formatted: "0".to_string(),
1199                decimals: 18,
1200                symbol: "TEST".to_string(),
1201                usd_value: None,
1202            })
1203        }
1204
1205        async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1206            unimplemented!()
1207        }
1208
1209        async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1210            Ok(Vec::new())
1211        }
1212
1213        async fn get_block_number(&self) -> Result<u64> {
1214            Ok(0)
1215        }
1216
1217        async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1218            Ok(Vec::new())
1219        }
1220
1221        async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1222    }
1223
1224    #[tokio::test]
1225    async fn test_default_get_token_info() {
1226        let client = MinimalChainClient;
1227        let result = client.get_token_info("0xtest").await;
1228        assert!(result.is_err());
1229        assert!(result.unwrap_err().to_string().contains("not supported"));
1230    }
1231
1232    #[tokio::test]
1233    async fn test_default_get_token_holders() {
1234        let client = MinimalChainClient;
1235        let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1236        assert!(holders.is_empty());
1237    }
1238
1239    #[tokio::test]
1240    async fn test_default_get_token_holder_count() {
1241        let client = MinimalChainClient;
1242        let count = client.get_token_holder_count("0xtest").await.unwrap();
1243        assert_eq!(count, 0);
1244    }
1245}
1246
1247// ============================================================================
1248// Mock Test Utilities
1249// ============================================================================
1250
1251/// Test helper module providing mock implementations of chain client traits.
1252///
1253/// These mocks are available across all test modules in the crate for
1254/// end-to-end testing of CLI `run()` functions without network calls.
1255#[cfg(test)]
1256pub mod mocks {
1257    use super::*;
1258    use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1259    use async_trait::async_trait;
1260
1261    /// Mock chain client with configurable responses.
1262    #[derive(Debug, Clone)]
1263    pub struct MockChainClient {
1264        pub chain: String,
1265        pub symbol: String,
1266        pub balance: Balance,
1267        pub transaction: Transaction,
1268        pub transactions: Vec<Transaction>,
1269        pub token_balances: Vec<TokenBalance>,
1270        pub block_number: u64,
1271        pub token_info: Option<Token>,
1272        pub token_holders: Vec<TokenHolder>,
1273        pub token_holder_count: u64,
1274    }
1275
1276    impl MockChainClient {
1277        /// Creates a mock client with sensible default test data.
1278        pub fn new(chain: &str, symbol: &str) -> Self {
1279            Self {
1280                chain: chain.to_string(),
1281                symbol: symbol.to_string(),
1282                balance: Balance {
1283                    raw: "1000000000000000000".to_string(),
1284                    formatted: "1.0".to_string(),
1285                    decimals: 18,
1286                    symbol: symbol.to_string(),
1287                    usd_value: Some(2500.0),
1288                },
1289                transaction: Transaction {
1290                    hash: "0xmocktx".to_string(),
1291                    block_number: Some(12345678),
1292                    timestamp: Some(1700000000),
1293                    from: "0xfrom".to_string(),
1294                    to: Some("0xto".to_string()),
1295                    value: "1.0".to_string(),
1296                    gas_limit: 21000,
1297                    gas_used: Some(21000),
1298                    gas_price: "20000000000".to_string(),
1299                    nonce: 42,
1300                    input: "0x".to_string(),
1301                    status: Some(true),
1302                },
1303                transactions: vec![],
1304                token_balances: vec![],
1305                block_number: 12345678,
1306                token_info: None,
1307                token_holders: vec![],
1308                token_holder_count: 0,
1309            }
1310        }
1311    }
1312
1313    #[async_trait]
1314    impl ChainClient for MockChainClient {
1315        fn chain_name(&self) -> &str {
1316            &self.chain
1317        }
1318
1319        fn native_token_symbol(&self) -> &str {
1320            &self.symbol
1321        }
1322
1323        async fn get_balance(&self, _address: &str) -> Result<Balance> {
1324            Ok(self.balance.clone())
1325        }
1326
1327        async fn enrich_balance_usd(&self, _balance: &mut Balance) {
1328            // Mock: no-op, balance already has usd_value set
1329        }
1330
1331        async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1332            Ok(self.transaction.clone())
1333        }
1334
1335        async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1336            Ok(self.transactions.clone())
1337        }
1338
1339        async fn get_block_number(&self) -> Result<u64> {
1340            Ok(self.block_number)
1341        }
1342
1343        async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1344            Ok(self.token_balances.clone())
1345        }
1346
1347        async fn get_token_info(&self, _address: &str) -> Result<Token> {
1348            match &self.token_info {
1349                Some(t) => Ok(t.clone()),
1350                None => Err(crate::error::ScopeError::Chain(
1351                    "Token info not available".to_string(),
1352                )),
1353            }
1354        }
1355
1356        async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
1357            Ok(self.token_holders.clone())
1358        }
1359
1360        async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
1361            Ok(self.token_holder_count)
1362        }
1363    }
1364
1365    /// Mock DEX data source with configurable responses.
1366    #[derive(Debug, Clone)]
1367    pub struct MockDexSource {
1368        pub token_price: Option<f64>,
1369        pub native_price: Option<f64>,
1370        pub token_data: Option<DexTokenData>,
1371        pub search_results: Vec<TokenSearchResult>,
1372    }
1373
1374    impl Default for MockDexSource {
1375        fn default() -> Self {
1376            Self::new()
1377        }
1378    }
1379
1380    impl MockDexSource {
1381        /// Creates a mock DEX source with default test data.
1382        pub fn new() -> Self {
1383            Self {
1384                token_price: Some(1.0),
1385                native_price: Some(2500.0),
1386                token_data: Some(DexTokenData {
1387                    address: "0xmocktoken".to_string(),
1388                    symbol: "MOCK".to_string(),
1389                    name: "Mock Token".to_string(),
1390                    price_usd: 1.0,
1391                    price_change_24h: 5.0,
1392                    price_change_6h: 2.0,
1393                    price_change_1h: 0.5,
1394                    price_change_5m: 0.1,
1395                    volume_24h: 1_000_000.0,
1396                    volume_6h: 250_000.0,
1397                    volume_1h: 50_000.0,
1398                    liquidity_usd: 5_000_000.0,
1399                    market_cap: Some(100_000_000.0),
1400                    fdv: Some(200_000_000.0),
1401                    pairs: vec![],
1402                    price_history: vec![],
1403                    volume_history: vec![],
1404                    total_buys_24h: 500,
1405                    total_sells_24h: 450,
1406                    total_buys_6h: 120,
1407                    total_sells_6h: 110,
1408                    total_buys_1h: 20,
1409                    total_sells_1h: 18,
1410                    earliest_pair_created_at: Some(1690000000),
1411                    image_url: None,
1412                    websites: vec![],
1413                    socials: vec![],
1414                    dexscreener_url: None,
1415                }),
1416                search_results: vec![],
1417            }
1418        }
1419    }
1420
1421    #[async_trait]
1422    impl DexDataSource for MockDexSource {
1423        async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
1424            self.token_price
1425        }
1426
1427        async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
1428            self.native_price
1429        }
1430
1431        async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
1432            match &self.token_data {
1433                Some(data) => Ok(data.clone()),
1434                None => Err(crate::error::ScopeError::NotFound(
1435                    "No DEX data found".to_string(),
1436                )),
1437            }
1438        }
1439
1440        async fn search_tokens(
1441            &self,
1442            _query: &str,
1443            _chain: Option<&str>,
1444        ) -> Result<Vec<TokenSearchResult>> {
1445            Ok(self.search_results.clone())
1446        }
1447    }
1448
1449    /// Mock client factory that returns pre-configured mock clients.
1450    pub struct MockClientFactory {
1451        pub mock_client: MockChainClient,
1452        pub mock_dex: MockDexSource,
1453    }
1454
1455    impl Default for MockClientFactory {
1456        fn default() -> Self {
1457            Self::new()
1458        }
1459    }
1460
1461    impl MockClientFactory {
1462        /// Creates a factory with default mock data for Ethereum.
1463        pub fn new() -> Self {
1464            Self {
1465                mock_client: MockChainClient::new("ethereum", "ETH"),
1466                mock_dex: MockDexSource::new(),
1467            }
1468        }
1469    }
1470
1471    impl ChainClientFactory for MockClientFactory {
1472        fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
1473            Ok(Box::new(self.mock_client.clone()))
1474        }
1475
1476        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
1477            Box::new(self.mock_dex.clone())
1478        }
1479    }
1480}