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