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 address book 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 crate::http::HttpClient;
100use async_trait::async_trait;
101use serde::{Deserialize, Serialize};
102use std::sync::Arc;
103
104/// Trait defining common blockchain client operations.
105///
106/// All chain-specific clients must implement this trait to provide
107/// a consistent interface for blockchain interactions.
108///
109/// ## Core Methods
110///
111/// Every implementation must provide: `chain_name`, `native_token_symbol`,
112/// `get_balance`, `get_transaction`, `get_transactions`, `get_block_number`,
113/// `enrich_balance_usd`, and `get_token_balances`.
114///
115/// ## Token Explorer Methods
116///
117/// The token-explorer methods (`get_token_info`, `get_token_holders`,
118/// `get_token_holder_count`) have default implementations that return
119/// "not supported" errors or empty results. Only chains with block-explorer
120/// support for these endpoints (currently EVM chains) need to override them.
121#[async_trait]
122pub trait ChainClient: Send + Sync {
123    /// Returns the name of the blockchain network.
124    fn chain_name(&self) -> &str;
125
126    /// Returns the native token symbol (e.g., "ETH", "MATIC").
127    fn native_token_symbol(&self) -> &str;
128
129    /// Fetches the native token balance for an address.
130    ///
131    /// # Arguments
132    ///
133    /// * `address` - The blockchain address to query
134    ///
135    /// # Returns
136    ///
137    /// Returns a [`Balance`] containing the balance in multiple formats.
138    async fn get_balance(&self, address: &str) -> Result<Balance>;
139
140    /// Enriches a balance with USD valuation via DexScreener.
141    ///
142    /// # Arguments
143    ///
144    /// * `balance` - The balance to enrich with a USD value
145    async fn enrich_balance_usd(&self, balance: &mut Balance);
146
147    /// Fetches transaction details by hash.
148    ///
149    /// # Arguments
150    ///
151    /// * `hash` - The transaction hash to query
152    ///
153    /// # Returns
154    ///
155    /// Returns [`Transaction`] details or an error if not found.
156    async fn get_transaction(&self, hash: &str) -> Result<Transaction>;
157
158    /// Fetches recent transactions for an address.
159    ///
160    /// # Arguments
161    ///
162    /// * `address` - The address to query
163    /// * `limit` - Maximum number of transactions to return
164    ///
165    /// # Returns
166    ///
167    /// Returns a vector of [`Transaction`] objects.
168    async fn get_transactions(&self, address: &str, limit: u32) -> Result<Vec<Transaction>>;
169
170    /// Fetches the current block number.
171    async fn get_block_number(&self) -> Result<u64>;
172
173    /// Fetches token balances for an address.
174    ///
175    /// Returns a unified [`TokenBalance`] list regardless of chain
176    /// (ERC-20, SPL, TRC-20 all map to the same type).
177    async fn get_token_balances(&self, address: &str) -> Result<Vec<TokenBalance>>;
178
179    /// Fetches token information for a contract address.
180    ///
181    /// Default implementation returns "not supported" error.
182    /// Override in chain clients that support token info lookups.
183    async fn get_token_info(&self, _address: &str) -> Result<Token> {
184        Err(crate::error::ScopeError::Chain(
185            "Token info lookup not supported on this chain".to_string(),
186        ))
187    }
188
189    /// Fetches top token holders for a contract address.
190    ///
191    /// Default implementation returns an empty vector.
192    /// Override in chain clients that support holder lookups.
193    async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
194        Ok(Vec::new())
195    }
196
197    /// Fetches total token holder count for a contract address.
198    ///
199    /// Default implementation returns 0.
200    /// Override in chain clients that support holder count lookups.
201    async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
202        Ok(0)
203    }
204
205    /// Fetches bytecode at address (EVM: eth_getCode).
206    /// Returns "0x" for EOA, non-empty hex for contracts.
207    /// Default: not supported.
208    async fn get_code(&self, _address: &str) -> Result<String> {
209        Err(crate::error::ScopeError::Chain(
210            "Code lookup not supported on this chain".to_string(),
211        ))
212    }
213
214    /// Reads a storage slot value at a contract address (EVM: eth_getStorageAt).
215    /// Returns the 32-byte hex value at the given slot position.
216    /// Default: not supported.
217    async fn get_storage_at(&self, _address: &str, _slot: &str) -> Result<String> {
218        Err(crate::error::ScopeError::Chain(
219            "Storage lookup not supported on this chain".to_string(),
220        ))
221    }
222}
223
224/// Factory trait for creating chain clients and DEX data sources.
225///
226/// Bundles both chain and DEX client creation so CLI functions
227/// only need one injected dependency instead of two.
228///
229/// # Example
230///
231/// ```rust,no_run
232/// use scope::chains::{ChainClientFactory, DefaultClientFactory};
233/// use scope::http::NativeHttpClient;
234/// use scope::Config;
235/// use std::sync::Arc;
236///
237/// let config = Config::default();
238/// let http = Arc::new(NativeHttpClient::new().unwrap());
239/// let factory = DefaultClientFactory { chains_config: config.chains.clone(), http };
240/// let client = factory.create_chain_client("ethereum").unwrap();
241/// ```
242pub trait ChainClientFactory: Send + Sync {
243    /// Creates a chain client for the given blockchain network.
244    ///
245    /// # Arguments
246    ///
247    /// * `chain` - The chain name (e.g., "ethereum", "solana", "tron")
248    fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>>;
249
250    /// Creates a DEX data source client.
251    fn create_dex_client(&self) -> Box<dyn DexDataSource>;
252}
253
254/// Default factory that creates real chain clients from configuration.
255///
256/// Holds a shared [`HttpClient`] transport so all chain clients route
257/// through the same backend (native `reqwest` or Ghola sidecar).
258pub struct DefaultClientFactory {
259    /// Chain configuration containing API keys and endpoints.
260    pub chains_config: crate::config::ChainsConfig,
261    /// Shared HTTP transport injected into every client.
262    pub http: Arc<dyn HttpClient>,
263}
264
265impl ChainClientFactory for DefaultClientFactory {
266    fn create_chain_client(&self, chain: &str) -> Result<Box<dyn ChainClient>> {
267        match chain.to_lowercase().as_str() {
268            "solana" | "sol" => Ok(Box::new(SolanaClient::new_with_http(
269                &self.chains_config,
270                self.http.clone(),
271            )?)),
272            "tron" | "trx" => Ok(Box::new(TronClient::new_with_http(
273                &self.chains_config,
274                self.http.clone(),
275            )?)),
276            _ => Ok(Box::new(EthereumClient::for_chain_with_http(
277                chain,
278                &self.chains_config,
279                self.http.clone(),
280            )?)),
281        }
282    }
283
284    fn create_dex_client(&self) -> Box<dyn DexDataSource> {
285        Box::new(DexClient::new_with_http(self.http.clone()))
286    }
287}
288
289/// Balance representation with multiple formats.
290#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct Balance {
292    /// Raw balance in smallest unit (e.g., wei).
293    pub raw: String,
294
295    /// Human-readable formatted balance.
296    pub formatted: String,
297
298    /// Number of decimals for the token.
299    pub decimals: u8,
300
301    /// Token symbol.
302    pub symbol: String,
303
304    /// USD value (if available).
305    #[serde(skip_serializing_if = "Option::is_none")]
306    pub usd_value: Option<f64>,
307}
308
309/// Transaction information.
310#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct Transaction {
312    /// Transaction hash.
313    pub hash: String,
314
315    /// Block number (None if pending).
316    pub block_number: Option<u64>,
317
318    /// Block timestamp (None if pending).
319    pub timestamp: Option<u64>,
320
321    /// Sender address.
322    pub from: String,
323
324    /// Recipient address (None for contract creation).
325    pub to: Option<String>,
326
327    /// Value transferred in native token.
328    pub value: String,
329
330    /// Gas limit.
331    pub gas_limit: u64,
332
333    /// Gas used (None if pending).
334    pub gas_used: Option<u64>,
335
336    /// Gas price in wei.
337    pub gas_price: String,
338
339    /// Transaction nonce.
340    pub nonce: u64,
341
342    /// Input data.
343    pub input: String,
344
345    /// Transaction status (None if pending, Some(true) for success).
346    pub status: Option<bool>,
347}
348
349/// Token information.
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct Token {
352    /// Contract address.
353    pub contract_address: String,
354
355    /// Token symbol.
356    pub symbol: String,
357
358    /// Token name.
359    pub name: String,
360
361    /// Decimal places.
362    pub decimals: u8,
363}
364
365/// Token balance for an address.
366#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct TokenBalance {
368    /// Token information.
369    pub token: Token,
370
371    /// Raw balance.
372    pub balance: String,
373
374    /// Formatted balance.
375    pub formatted_balance: String,
376
377    /// USD value (if available).
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub usd_value: Option<f64>,
380}
381
382// ============================================================================
383// Token Analytics Types
384// ============================================================================
385
386/// A token holder with their balance and percentage of supply.
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct TokenHolder {
389    /// Holder's address.
390    pub address: String,
391
392    /// Raw balance amount.
393    pub balance: String,
394
395    /// Formatted balance with proper decimals.
396    pub formatted_balance: String,
397
398    /// Percentage of total supply held.
399    pub percentage: f64,
400
401    /// Rank among all holders (1 = largest).
402    pub rank: u32,
403}
404
405/// A price data point for historical charting.
406#[derive(Debug, Clone, Serialize, Deserialize)]
407pub struct PricePoint {
408    /// Unix timestamp in seconds.
409    pub timestamp: i64,
410
411    /// Price in USD.
412    pub price: f64,
413}
414
415/// A volume data point for historical charting.
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct VolumePoint {
418    /// Unix timestamp in seconds.
419    pub timestamp: i64,
420
421    /// Volume in USD.
422    pub volume: f64,
423}
424
425/// A holder count data point for historical charting.
426#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct HolderCountPoint {
428    /// Unix timestamp in seconds.
429    pub timestamp: i64,
430
431    /// Number of holders.
432    pub count: u64,
433}
434
435/// DEX trading pair information.
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct DexPair {
438    /// DEX name (e.g., "Uniswap V3", "SushiSwap").
439    pub dex_name: String,
440
441    /// Pair address on the DEX.
442    pub pair_address: String,
443
444    /// Base token symbol.
445    pub base_token: String,
446
447    /// Quote token symbol.
448    pub quote_token: String,
449
450    /// Current price in USD.
451    pub price_usd: f64,
452
453    /// 24h trading volume in USD.
454    pub volume_24h: f64,
455
456    /// Liquidity in USD.
457    pub liquidity_usd: f64,
458
459    /// Price change percentage in 24h.
460    pub price_change_24h: f64,
461
462    /// Buy transactions in 24h.
463    pub buys_24h: u64,
464
465    /// Sell transactions in 24h.
466    pub sells_24h: u64,
467
468    /// Buy transactions in 6h.
469    pub buys_6h: u64,
470
471    /// Sell transactions in 6h.
472    pub sells_6h: u64,
473
474    /// Buy transactions in 1h.
475    pub buys_1h: u64,
476
477    /// Sell transactions in 1h.
478    pub sells_1h: u64,
479
480    /// Pair creation timestamp.
481    pub pair_created_at: Option<i64>,
482
483    /// Direct URL to this pair on DexScreener.
484    pub url: Option<String>,
485}
486
487/// Comprehensive token analytics data.
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct TokenAnalytics {
490    /// Token information.
491    pub token: Token,
492
493    /// Blockchain network name.
494    pub chain: String,
495
496    /// Top token holders.
497    pub holders: Vec<TokenHolder>,
498
499    /// Total number of holders.
500    pub total_holders: u64,
501
502    /// 24-hour trading volume in USD.
503    pub volume_24h: f64,
504
505    /// 7-day trading volume in USD.
506    pub volume_7d: f64,
507
508    /// Current price in USD.
509    pub price_usd: f64,
510
511    /// 24-hour price change percentage.
512    pub price_change_24h: f64,
513
514    /// 7-day price change percentage.
515    pub price_change_7d: f64,
516
517    /// Total liquidity across DEXs in USD.
518    pub liquidity_usd: f64,
519
520    /// Market capitalization (if available).
521    #[serde(skip_serializing_if = "Option::is_none")]
522    pub market_cap: Option<f64>,
523
524    /// Fully diluted valuation (if available).
525    #[serde(skip_serializing_if = "Option::is_none")]
526    pub fdv: Option<f64>,
527
528    /// Total supply.
529    #[serde(skip_serializing_if = "Option::is_none")]
530    pub total_supply: Option<String>,
531
532    /// Circulating supply.
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub circulating_supply: Option<String>,
535
536    /// Historical price data for charting.
537    pub price_history: Vec<PricePoint>,
538
539    /// Historical volume data for charting.
540    pub volume_history: Vec<VolumePoint>,
541
542    /// Historical holder count data for charting.
543    pub holder_history: Vec<HolderCountPoint>,
544
545    /// DEX trading pairs.
546    pub dex_pairs: Vec<DexPair>,
547
548    /// Timestamp when this data was fetched.
549    pub fetched_at: i64,
550
551    /// Percentage of supply held by top 10 holders.
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub top_10_concentration: Option<f64>,
554
555    /// Percentage of supply held by top 50 holders.
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub top_50_concentration: Option<f64>,
558
559    /// Percentage of supply held by top 100 holders.
560    #[serde(skip_serializing_if = "Option::is_none")]
561    pub top_100_concentration: Option<f64>,
562
563    /// 6-hour price change percentage.
564    pub price_change_6h: f64,
565
566    /// 1-hour price change percentage.
567    pub price_change_1h: f64,
568
569    /// Total buy transactions in 24 hours.
570    pub total_buys_24h: u64,
571
572    /// Total sell transactions in 24 hours.
573    pub total_sells_24h: u64,
574
575    /// Total buy transactions in 6 hours.
576    pub total_buys_6h: u64,
577
578    /// Total sell transactions in 6 hours.
579    pub total_sells_6h: u64,
580
581    /// Total buy transactions in 1 hour.
582    pub total_buys_1h: u64,
583
584    /// Total sell transactions in 1 hour.
585    pub total_sells_1h: u64,
586
587    /// Token age in hours (since earliest pair creation).
588    #[serde(skip_serializing_if = "Option::is_none")]
589    pub token_age_hours: Option<f64>,
590
591    /// Token image URL.
592    #[serde(skip_serializing_if = "Option::is_none")]
593    pub image_url: Option<String>,
594
595    /// Token website URLs.
596    pub websites: Vec<String>,
597
598    /// Token social media links.
599    pub socials: Vec<TokenSocial>,
600
601    /// DexScreener URL for the primary pair.
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub dexscreener_url: Option<String>,
604}
605
606/// Social media link for a token.
607#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
608pub struct TokenSocial {
609    /// Platform name (twitter, telegram, discord, etc.)
610    pub platform: String,
611    /// URL or handle for the social account.
612    pub url: String,
613}
614
615// ============================================================================
616// NFT Types
617// ============================================================================
618
619/// NFT token metadata.
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct NftMetadata {
622    /// Token ID.
623    pub token_id: String,
624    /// NFT name.
625    pub name: Option<String>,
626    /// NFT description.
627    pub description: Option<String>,
628    /// Image URL.
629    pub image_url: Option<String>,
630    /// Token URI (metadata JSON location).
631    pub token_uri: Option<String>,
632    /// Token standard (ERC-721 or ERC-1155).
633    pub standard: String,
634    /// Collection/contract name.
635    pub collection_name: Option<String>,
636    /// Additional attributes.
637    pub attributes: Vec<NftAttribute>,
638}
639
640/// An NFT attribute (trait).
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct NftAttribute {
643    /// Trait type/name.
644    pub trait_type: String,
645    /// Trait value.
646    pub value: String,
647}
648
649// ============================================================================
650// Gas Analysis Types
651// ============================================================================
652
653/// Gas usage analysis for a contract or address.
654#[derive(Debug, Clone, Serialize, Deserialize)]
655pub struct GasAnalysis {
656    /// Average gas used per transaction.
657    pub avg_gas_used: u64,
658    /// Maximum gas used in a single transaction.
659    pub max_gas_used: u64,
660    /// Minimum gas used in a single transaction.
661    pub min_gas_used: u64,
662    /// Total gas spent (sum of gas_used * gas_price).
663    pub total_gas_cost_wei: String,
664    /// Total gas cost in ETH/native token.
665    pub total_gas_cost_formatted: String,
666    /// Number of transactions analyzed.
667    pub tx_count: u64,
668    /// Gas usage by function selector (top callers).
669    pub gas_by_function: Vec<GasByFunction>,
670    /// Failed transaction count and wasted gas.
671    pub failed_tx_count: u64,
672    /// Gas wasted on failed transactions.
673    pub wasted_gas: u64,
674}
675
676/// Gas usage breakdown by function.
677#[derive(Debug, Clone, Serialize, Deserialize)]
678pub struct GasByFunction {
679    /// Function selector or name.
680    pub function: String,
681    /// Number of calls.
682    pub call_count: u64,
683    /// Average gas per call.
684    pub avg_gas: u64,
685    /// Total gas for this function.
686    pub total_gas: u64,
687}
688
689// ============================================================================
690// Chain Metadata
691// ============================================================================
692
693/// Metadata for a blockchain network (symbol, decimals, explorer URLs).
694///
695/// Used for normalized presentation across all chains.
696#[derive(Debug, Clone)]
697pub struct ChainMetadata {
698    /// Canonical chain identifier.
699    pub chain_id: &'static str,
700    /// Native token symbol (e.g., ETH, SOL, TRX).
701    pub native_symbol: &'static str,
702    /// Native token decimals.
703    pub native_decimals: u8,
704    /// Block explorer base URL for token pages.
705    pub explorer_token_base: &'static str,
706}
707
708/// Returns chain metadata for display and formatting.
709///
710/// Returns `None` for unknown chains.
711pub fn chain_metadata(chain: &str) -> Option<ChainMetadata> {
712    match chain.to_lowercase().as_str() {
713        "ethereum" | "eth" => Some(ChainMetadata {
714            chain_id: "ethereum",
715            native_symbol: "ETH",
716            native_decimals: 18,
717            explorer_token_base: "https://etherscan.io/token",
718        }),
719        "polygon" => Some(ChainMetadata {
720            chain_id: "polygon",
721            native_symbol: "MATIC",
722            native_decimals: 18,
723            explorer_token_base: "https://polygonscan.com/token",
724        }),
725        "arbitrum" => Some(ChainMetadata {
726            chain_id: "arbitrum",
727            native_symbol: "ETH",
728            native_decimals: 18,
729            explorer_token_base: "https://arbiscan.io/token",
730        }),
731        "optimism" => Some(ChainMetadata {
732            chain_id: "optimism",
733            native_symbol: "ETH",
734            native_decimals: 18,
735            explorer_token_base: "https://optimistic.etherscan.io/token",
736        }),
737        "base" => Some(ChainMetadata {
738            chain_id: "base",
739            native_symbol: "ETH",
740            native_decimals: 18,
741            explorer_token_base: "https://basescan.org/token",
742        }),
743        "bsc" => Some(ChainMetadata {
744            chain_id: "bsc",
745            native_symbol: "BNB",
746            native_decimals: 18,
747            explorer_token_base: "https://bscscan.com/token",
748        }),
749        "solana" | "sol" => Some(ChainMetadata {
750            chain_id: "solana",
751            native_symbol: "SOL",
752            native_decimals: 9,
753            explorer_token_base: "https://solscan.io/token",
754        }),
755        "tron" | "trx" => Some(ChainMetadata {
756            chain_id: "tron",
757            native_symbol: "TRX",
758            native_decimals: 6,
759            explorer_token_base: "https://tronscan.org/#/token20",
760        }),
761        _ => None,
762    }
763}
764
765/// Returns the native token symbol for a chain, or "???" if unknown.
766pub fn native_symbol(chain: &str) -> &'static str {
767    chain_metadata(chain)
768        .map(|m| m.native_symbol)
769        .unwrap_or("???")
770}
771
772// ============================================================================
773// Chain Inference
774// ============================================================================
775
776/// Infers the blockchain from an address format.
777///
778/// Returns `Some(chain_name)` if the address format is unambiguous,
779/// or `None` if the format is not recognized.
780///
781/// # Supported Formats
782///
783/// - **EVM** (ethereum): `0x` prefix + 40 hex chars (42 total)
784/// - **Tron**: Starts with `T` + 34 chars (Base58Check)
785/// - **Solana**: Base58, 32-44 chars, decodes to 32 bytes
786///
787/// # Examples
788///
789/// ```
790/// use scope::chains::infer_chain_from_address;
791///
792/// assert_eq!(infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"), Some("ethereum"));
793/// assert_eq!(infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"), Some("tron"));
794/// assert_eq!(infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"), Some("solana"));
795/// assert_eq!(infer_chain_from_address("invalid"), None);
796/// ```
797pub fn infer_chain_from_address(address: &str) -> Option<&'static str> {
798    // Tron: starts with 'T', 34 chars, valid base58
799    if address.starts_with('T') && address.len() == 34 && bs58::decode(address).into_vec().is_ok() {
800        return Some("tron");
801    }
802
803    // EVM: 0x prefix, 42 chars total (40 hex + "0x")
804    if address.starts_with("0x")
805        && address.len() == 42
806        && address[2..].chars().all(|c| c.is_ascii_hexdigit())
807    {
808        return Some("ethereum");
809    }
810
811    // Solana: base58, 32-44 chars, decodes to 32 bytes
812    if address.len() >= 32
813        && address.len() <= 44
814        && let Ok(decoded) = bs58::decode(address).into_vec()
815        && decoded.len() == 32
816    {
817        return Some("solana");
818    }
819
820    None
821}
822
823/// Infers the blockchain from a transaction hash format.
824///
825/// Returns `Some(chain_name)` if the hash format is unambiguous,
826/// or `None` if the format is not recognized.
827///
828/// # Supported Formats
829///
830/// - **EVM** (ethereum): `0x` prefix + 64 hex chars (66 total)
831/// - **Tron**: 64 hex chars (no prefix)
832/// - **Solana**: Base58, 80-90 chars, decodes to 64 bytes
833///
834/// # Examples
835///
836/// ```
837/// use scope::chains::infer_chain_from_hash;
838///
839/// // EVM hash
840/// let evm_hash = "0xabc123def456789012345678901234567890123456789012345678901234abcd";
841/// assert_eq!(infer_chain_from_hash(evm_hash), Some("ethereum"));
842///
843/// // Tron hash (64 hex chars, no 0x prefix)
844/// let tron_hash = "abc123def456789012345678901234567890123456789012345678901234abcd";
845/// assert_eq!(infer_chain_from_hash(tron_hash), Some("tron"));
846/// ```
847pub fn infer_chain_from_hash(hash: &str) -> Option<&'static str> {
848    // EVM: 0x prefix, 66 chars total (64 hex + "0x")
849    if hash.starts_with("0x")
850        && hash.len() == 66
851        && hash[2..].chars().all(|c| c.is_ascii_hexdigit())
852    {
853        return Some("ethereum");
854    }
855
856    // Tron: 64 hex chars, no prefix
857    if hash.len() == 64 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
858        return Some("tron");
859    }
860
861    // Solana: base58, 80-90 chars, decodes to 64 bytes
862    if hash.len() >= 80
863        && hash.len() <= 90
864        && let Ok(decoded) = bs58::decode(hash).into_vec()
865        && decoded.len() == 64
866    {
867        return Some("solana");
868    }
869
870    None
871}
872
873/// Analyze gas usage from a set of transactions.
874///
875/// Computes statistics on gas consumption, identifies gas-heavy functions,
876/// and flags failed transactions.
877pub fn analyze_gas_usage(transactions: &[Transaction]) -> GasAnalysis {
878    if transactions.is_empty() {
879        return GasAnalysis {
880            avg_gas_used: 0,
881            max_gas_used: 0,
882            min_gas_used: 0,
883            total_gas_cost_wei: "0".to_string(),
884            total_gas_cost_formatted: "0".to_string(),
885            tx_count: 0,
886            gas_by_function: vec![],
887            failed_tx_count: 0,
888            wasted_gas: 0,
889        };
890    }
891
892    let mut total_gas: u64 = 0;
893    let mut max_gas: u64 = 0;
894    let mut min_gas: u64 = u64::MAX;
895    let mut failed_count: u64 = 0;
896    let mut wasted_gas: u64 = 0;
897    let mut function_gas: std::collections::HashMap<String, (u64, u64)> =
898        std::collections::HashMap::new();
899
900    for tx in transactions {
901        let gas_used = tx.gas_used.unwrap_or(0);
902        total_gas += gas_used;
903        if gas_used > max_gas {
904            max_gas = gas_used;
905        }
906        if gas_used < min_gas {
907            min_gas = gas_used;
908        }
909
910        // Track failed transactions
911        if tx.status == Some(false) {
912            failed_count += 1;
913            wasted_gas += gas_used;
914        }
915
916        // Group by function selector
917        let selector = if tx.input.len() >= 10 {
918            tx.input[..10].to_string()
919        } else if tx.input.is_empty() || tx.input == "0x" {
920            "transfer()".to_string()
921        } else {
922            tx.input.clone()
923        };
924
925        let entry = function_gas.entry(selector).or_insert((0, 0));
926        entry.0 += 1; // call count
927        entry.1 += gas_used; // total gas
928    }
929
930    let tx_count = transactions.len() as u64;
931    let avg_gas = if tx_count > 0 {
932        total_gas / tx_count
933    } else {
934        0
935    };
936
937    if min_gas == u64::MAX {
938        min_gas = 0;
939    }
940
941    // Sort functions by total gas usage
942    let mut gas_by_function: Vec<GasByFunction> = function_gas
943        .into_iter()
944        .map(|(function, (call_count, total_gas_fn))| GasByFunction {
945            function,
946            call_count,
947            avg_gas: if call_count > 0 {
948                total_gas_fn / call_count
949            } else {
950                0
951            },
952            total_gas: total_gas_fn,
953        })
954        .collect();
955    gas_by_function.sort_by(|a, b| b.total_gas.cmp(&a.total_gas));
956
957    // Format total gas cost (rough estimate using average gas price)
958    let total_gas_cost_formatted = format!("{} gas units", total_gas);
959
960    GasAnalysis {
961        avg_gas_used: avg_gas,
962        max_gas_used: max_gas,
963        min_gas_used: min_gas,
964        total_gas_cost_wei: total_gas.to_string(),
965        total_gas_cost_formatted,
966        tx_count,
967        gas_by_function,
968        failed_tx_count: failed_count,
969        wasted_gas,
970    }
971}
972
973// ============================================================================
974// Unit Tests
975// ============================================================================
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980
981    #[test]
982    fn test_balance_serialization() {
983        let balance = Balance {
984            raw: "1000000000000000000".to_string(),
985            formatted: "1.0".to_string(),
986            decimals: 18,
987            symbol: "ETH".to_string(),
988            usd_value: Some(3500.0),
989        };
990
991        let json = serde_json::to_string(&balance).unwrap();
992        assert!(json.contains("1000000000000000000"));
993        assert!(json.contains("1.0"));
994        assert!(json.contains("ETH"));
995        assert!(json.contains("3500"));
996
997        let deserialized: Balance = serde_json::from_str(&json).unwrap();
998        assert_eq!(deserialized.raw, balance.raw);
999        assert_eq!(deserialized.decimals, 18);
1000    }
1001
1002    #[test]
1003    fn test_balance_without_usd() {
1004        let balance = Balance {
1005            raw: "1000000000000000000".to_string(),
1006            formatted: "1.0".to_string(),
1007            decimals: 18,
1008            symbol: "ETH".to_string(),
1009            usd_value: None,
1010        };
1011
1012        let json = serde_json::to_string(&balance).unwrap();
1013        assert!(!json.contains("usd_value"));
1014    }
1015
1016    #[test]
1017    fn test_transaction_serialization() {
1018        let tx = Transaction {
1019            hash: "0xabc123".to_string(),
1020            block_number: Some(12345678),
1021            timestamp: Some(1700000000),
1022            from: "0xfrom".to_string(),
1023            to: Some("0xto".to_string()),
1024            value: "1.0".to_string(),
1025            gas_limit: 21000,
1026            gas_used: Some(21000),
1027            gas_price: "20000000000".to_string(),
1028            nonce: 42,
1029            input: "0x".to_string(),
1030            status: Some(true),
1031        };
1032
1033        let json = serde_json::to_string(&tx).unwrap();
1034        assert!(json.contains("0xabc123"));
1035        assert!(json.contains("12345678"));
1036        assert!(json.contains("0xfrom"));
1037        assert!(json.contains("0xto"));
1038
1039        let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1040        assert_eq!(deserialized.hash, tx.hash);
1041        assert_eq!(deserialized.nonce, 42);
1042    }
1043
1044    #[test]
1045    fn test_pending_transaction_serialization() {
1046        let tx = Transaction {
1047            hash: "0xpending".to_string(),
1048            block_number: None,
1049            timestamp: None,
1050            from: "0xfrom".to_string(),
1051            to: Some("0xto".to_string()),
1052            value: "1.0".to_string(),
1053            gas_limit: 21000,
1054            gas_used: None,
1055            gas_price: "20000000000".to_string(),
1056            nonce: 0,
1057            input: "0x".to_string(),
1058            status: None,
1059        };
1060
1061        let json = serde_json::to_string(&tx).unwrap();
1062        assert!(json.contains("0xpending"));
1063        assert!(json.contains("null")); // None values serialize as null
1064
1065        let deserialized: Transaction = serde_json::from_str(&json).unwrap();
1066        assert!(deserialized.block_number.is_none());
1067        assert!(deserialized.status.is_none());
1068    }
1069
1070    #[test]
1071    fn test_contract_creation_transaction() {
1072        let tx = Transaction {
1073            hash: "0xcreate".to_string(),
1074            block_number: Some(100),
1075            timestamp: Some(1700000000),
1076            from: "0xdeployer".to_string(),
1077            to: None, // Contract creation
1078            value: "0".to_string(),
1079            gas_limit: 1000000,
1080            gas_used: Some(500000),
1081            gas_price: "20000000000".to_string(),
1082            nonce: 0,
1083            input: "0x608060...".to_string(),
1084            status: Some(true),
1085        };
1086
1087        let json = serde_json::to_string(&tx).unwrap();
1088        assert!(json.contains("\"to\":null"));
1089    }
1090
1091    #[test]
1092    fn test_token_serialization() {
1093        let token = Token {
1094            contract_address: "0xtoken".to_string(),
1095            symbol: "USDC".to_string(),
1096            name: "USD Coin".to_string(),
1097            decimals: 6,
1098        };
1099
1100        let json = serde_json::to_string(&token).unwrap();
1101        assert!(json.contains("USDC"));
1102        assert!(json.contains("USD Coin"));
1103        assert!(json.contains("\"decimals\":6"));
1104
1105        let deserialized: Token = serde_json::from_str(&json).unwrap();
1106        assert_eq!(deserialized.decimals, 6);
1107    }
1108
1109    #[test]
1110    fn test_token_balance_serialization() {
1111        let token_balance = TokenBalance {
1112            token: Token {
1113                contract_address: "0xtoken".to_string(),
1114                symbol: "USDC".to_string(),
1115                name: "USD Coin".to_string(),
1116                decimals: 6,
1117            },
1118            balance: "1000000".to_string(),
1119            formatted_balance: "1.0".to_string(),
1120            usd_value: Some(1.0),
1121        };
1122
1123        let json = serde_json::to_string(&token_balance).unwrap();
1124        assert!(json.contains("USDC"));
1125        assert!(json.contains("1000000"));
1126        assert!(json.contains("1.0"));
1127    }
1128
1129    #[test]
1130    fn test_balance_debug() {
1131        let balance = Balance {
1132            raw: "1000".to_string(),
1133            formatted: "0.001".to_string(),
1134            decimals: 18,
1135            symbol: "ETH".to_string(),
1136            usd_value: None,
1137        };
1138
1139        let debug_str = format!("{:?}", balance);
1140        assert!(debug_str.contains("Balance"));
1141        assert!(debug_str.contains("1000"));
1142    }
1143
1144    #[test]
1145    fn test_transaction_debug() {
1146        let tx = Transaction {
1147            hash: "0xtest".to_string(),
1148            block_number: Some(1),
1149            timestamp: Some(0),
1150            from: "0x1".to_string(),
1151            to: Some("0x2".to_string()),
1152            value: "0".to_string(),
1153            gas_limit: 21000,
1154            gas_used: Some(21000),
1155            gas_price: "0".to_string(),
1156            nonce: 0,
1157            input: "0x".to_string(),
1158            status: Some(true),
1159        };
1160
1161        let debug_str = format!("{:?}", tx);
1162        assert!(debug_str.contains("Transaction"));
1163        assert!(debug_str.contains("0xtest"));
1164    }
1165
1166    // ============================================================================
1167    // Chain Inference Tests
1168    // ============================================================================
1169
1170    #[test]
1171    fn test_infer_chain_from_address_evm() {
1172        // Valid EVM addresses (0x + 40 hex chars)
1173        assert_eq!(
1174            super::infer_chain_from_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"),
1175            Some("ethereum")
1176        );
1177        assert_eq!(
1178            super::infer_chain_from_address("0x0000000000000000000000000000000000000000"),
1179            Some("ethereum")
1180        );
1181        assert_eq!(
1182            super::infer_chain_from_address("0xABCDEF1234567890abcdef1234567890ABCDEF12"),
1183            Some("ethereum")
1184        );
1185    }
1186
1187    #[test]
1188    fn test_infer_chain_from_address_tron() {
1189        // Valid Tron addresses (T + 33 chars = 34 total, base58)
1190        assert_eq!(
1191            super::infer_chain_from_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1192            Some("tron")
1193        );
1194        assert_eq!(
1195            super::infer_chain_from_address("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"),
1196            Some("tron")
1197        );
1198    }
1199
1200    #[test]
1201    fn test_infer_chain_from_address_solana() {
1202        // Valid Solana addresses (base58, 32-44 chars, decodes to 32 bytes)
1203        assert_eq!(
1204            super::infer_chain_from_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy"),
1205            Some("solana")
1206        );
1207        // System program address
1208        assert_eq!(
1209            super::infer_chain_from_address("11111111111111111111111111111111"),
1210            Some("solana")
1211        );
1212    }
1213
1214    #[test]
1215    fn test_infer_chain_from_address_invalid() {
1216        // Too short
1217        assert_eq!(super::infer_chain_from_address("0x123"), None);
1218        // Invalid characters
1219        assert_eq!(super::infer_chain_from_address("not_an_address"), None);
1220        // Empty
1221        assert_eq!(super::infer_chain_from_address(""), None);
1222        // EVM-like but wrong length
1223        assert_eq!(super::infer_chain_from_address("0x123456"), None);
1224        // Tron-like but not starting with T
1225        assert_eq!(
1226            super::infer_chain_from_address("ADqSquXBgUCLYvYC4XZgrprLK589dkhSCf"),
1227            None
1228        );
1229    }
1230
1231    #[test]
1232    fn test_infer_chain_from_hash_evm() {
1233        // Valid EVM transaction hash (0x + 64 hex chars)
1234        assert_eq!(
1235            super::infer_chain_from_hash(
1236                "0xabc123def456789012345678901234567890123456789012345678901234abcd"
1237            ),
1238            Some("ethereum")
1239        );
1240        assert_eq!(
1241            super::infer_chain_from_hash(
1242                "0x0000000000000000000000000000000000000000000000000000000000000000"
1243            ),
1244            Some("ethereum")
1245        );
1246    }
1247
1248    #[test]
1249    fn test_infer_chain_from_hash_tron() {
1250        // Valid Tron transaction hash (64 hex chars, no 0x prefix)
1251        assert_eq!(
1252            super::infer_chain_from_hash(
1253                "abc123def456789012345678901234567890123456789012345678901234abcd"
1254            ),
1255            Some("tron")
1256        );
1257        assert_eq!(
1258            super::infer_chain_from_hash(
1259                "0000000000000000000000000000000000000000000000000000000000000000"
1260            ),
1261            Some("tron")
1262        );
1263    }
1264
1265    #[test]
1266    fn test_infer_chain_from_hash_solana() {
1267        // Valid Solana signature (base58, 80-90 chars, decodes to 64 bytes)
1268        // This is a made-up example that fits the pattern
1269        let solana_sig = "5VERv8NMvzbJMEkV8xnrLkEaWRtSz9CosKDYjCJjBRnbJLgp8uirBgmQpjKhoR4tjF3ZpRzrFmBV6UjKdiSZkQUW";
1270        assert_eq!(super::infer_chain_from_hash(solana_sig), Some("solana"));
1271    }
1272
1273    #[test]
1274    fn test_infer_chain_from_hash_invalid() {
1275        // Too short
1276        assert_eq!(super::infer_chain_from_hash("0x123"), None);
1277        // Invalid
1278        assert_eq!(super::infer_chain_from_hash("not_a_hash"), None);
1279        // Empty
1280        assert_eq!(super::infer_chain_from_hash(""), None);
1281        // 64 chars but with invalid hex (contains 'g')
1282        assert_eq!(
1283            super::infer_chain_from_hash(
1284                "abc123gef456789012345678901234567890123456789012345678901234abcd"
1285            ),
1286            None
1287        );
1288    }
1289
1290    // ============================================================================
1291    // DefaultClientFactory Tests
1292    // ============================================================================
1293
1294    #[test]
1295    fn test_default_client_factory_create_dex_client() {
1296        let config = crate::config::ChainsConfig::default();
1297        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1298        let factory = DefaultClientFactory {
1299            chains_config: config,
1300            http,
1301        };
1302        let dex = factory.create_dex_client();
1303        // Just verify it returns without panicking - the client is a Box<dyn DexDataSource>
1304        let _ = format!("{:?}", std::mem::size_of_val(&dex));
1305    }
1306
1307    #[test]
1308    fn test_default_client_factory_create_ethereum_client() {
1309        let config = crate::config::ChainsConfig::default();
1310        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1311        let factory = DefaultClientFactory {
1312            chains_config: config,
1313            http,
1314        };
1315        // ethereum, polygon, etc use EthereumClient::for_chain
1316        let client = factory.create_chain_client("ethereum");
1317        assert!(client.is_ok());
1318        assert_eq!(client.unwrap().chain_name(), "ethereum");
1319    }
1320
1321    #[test]
1322    fn test_default_client_factory_create_polygon_client() {
1323        let config = crate::config::ChainsConfig::default();
1324        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1325        let factory = DefaultClientFactory {
1326            chains_config: config,
1327            http,
1328        };
1329        let client = factory.create_chain_client("polygon");
1330        assert!(client.is_ok());
1331        assert_eq!(client.unwrap().chain_name(), "polygon");
1332    }
1333
1334    #[test]
1335    fn test_default_client_factory_create_solana_client() {
1336        let config = crate::config::ChainsConfig::default();
1337        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1338        let factory = DefaultClientFactory {
1339            chains_config: config,
1340            http,
1341        };
1342        let client = factory.create_chain_client("solana");
1343        assert!(client.is_ok());
1344        assert_eq!(client.unwrap().chain_name(), "solana");
1345    }
1346
1347    #[test]
1348    fn test_default_client_factory_create_sol_alias() {
1349        let config = crate::config::ChainsConfig::default();
1350        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1351        let factory = DefaultClientFactory {
1352            chains_config: config,
1353            http,
1354        };
1355        let client = factory.create_chain_client("sol");
1356        assert!(client.is_ok());
1357        assert_eq!(client.unwrap().chain_name(), "solana");
1358    }
1359
1360    #[test]
1361    fn test_default_client_factory_create_tron_client() {
1362        let config = crate::config::ChainsConfig::default();
1363        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1364        let factory = DefaultClientFactory {
1365            chains_config: config,
1366            http,
1367        };
1368        let client = factory.create_chain_client("tron");
1369        assert!(client.is_ok());
1370        assert_eq!(client.unwrap().chain_name(), "tron");
1371    }
1372
1373    #[test]
1374    fn test_default_client_factory_create_trx_alias() {
1375        let config = crate::config::ChainsConfig::default();
1376        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1377        let factory = DefaultClientFactory {
1378            chains_config: config,
1379            http,
1380        };
1381        let client = factory.create_chain_client("trx");
1382        assert!(client.is_ok());
1383        assert_eq!(client.unwrap().chain_name(), "tron");
1384    }
1385
1386    #[test]
1387    fn test_default_client_factory_create_arbitrum_client() {
1388        let config = crate::config::ChainsConfig::default();
1389        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1390        let factory = DefaultClientFactory {
1391            chains_config: config,
1392            http,
1393        };
1394        let client = factory.create_chain_client("arbitrum");
1395        assert!(client.is_ok());
1396        assert_eq!(client.unwrap().chain_name(), "arbitrum");
1397    }
1398
1399    #[test]
1400    fn test_default_client_factory_create_optimism_client() {
1401        let config = crate::config::ChainsConfig::default();
1402        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1403        let factory = DefaultClientFactory {
1404            chains_config: config,
1405            http,
1406        };
1407        let client = factory.create_chain_client("optimism");
1408        assert!(client.is_ok());
1409        assert_eq!(client.unwrap().chain_name(), "optimism");
1410    }
1411
1412    #[test]
1413    fn test_default_client_factory_create_base_client() {
1414        let config = crate::config::ChainsConfig::default();
1415        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1416        let factory = DefaultClientFactory {
1417            chains_config: config,
1418            http,
1419        };
1420        let client = factory.create_chain_client("base");
1421        assert!(client.is_ok());
1422        assert_eq!(client.unwrap().chain_name(), "base");
1423    }
1424
1425    #[test]
1426    fn test_default_client_factory_create_unsupported_chain_returns_err() {
1427        let config = crate::config::ChainsConfig::default();
1428        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1429        let factory = DefaultClientFactory {
1430            chains_config: config,
1431            http,
1432        };
1433        let client = factory.create_chain_client("avalanche");
1434        match &client {
1435            Err(e) => assert!(e.to_string().contains("Unsupported")),
1436            Ok(_) => panic!("expected Err for unsupported chain"),
1437        }
1438    }
1439
1440    // ============================================================================
1441    // HttpClient injection tests
1442    // ============================================================================
1443
1444    #[test]
1445    fn test_solana_client_new_with_http() {
1446        let config = crate::config::ChainsConfig::default();
1447        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1448        let client = SolanaClient::new_with_http(&config, http);
1449        assert!(client.is_ok());
1450        assert_eq!(client.unwrap().chain_name(), "solana");
1451    }
1452
1453    #[test]
1454    fn test_tron_client_new_with_http() {
1455        let config = crate::config::ChainsConfig::default();
1456        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1457        let client = TronClient::new_with_http(&config, http);
1458        assert!(client.is_ok());
1459        assert_eq!(client.unwrap().chain_name(), "tron");
1460    }
1461
1462    #[test]
1463    fn test_dex_client_new_with_http() {
1464        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1465        let client = DexClient::new_with_http(http);
1466        let _ = format!("{}", std::mem::size_of_val(&client));
1467    }
1468
1469    #[test]
1470    fn test_factory_shares_http_transport() {
1471        let config = crate::config::ChainsConfig::default();
1472        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1473        let weak = Arc::downgrade(&http);
1474        let factory = DefaultClientFactory {
1475            chains_config: config,
1476            http,
1477        };
1478        // Factory holds a clone of the Arc; original strong count was 1,
1479        // factory clone makes 2 minus the 1 we moved = factory has it.
1480        // Creating clients bumps the count via Arc::clone inside each
1481        // constructor.
1482        let _eth = factory.create_chain_client("ethereum").unwrap();
1483        let _sol = factory.create_chain_client("solana").unwrap();
1484        let _trx = factory.create_chain_client("tron").unwrap();
1485        let _dex = factory.create_dex_client();
1486        // The weak ref should still be valid (Arc not fully dropped)
1487        assert!(weak.upgrade().is_some());
1488    }
1489
1490    #[test]
1491    fn test_ethereum_client_new_with_http() {
1492        let config = crate::config::ChainsConfig::default();
1493        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1494        let client = EthereumClient::new_with_http(&config, http);
1495        assert!(client.is_ok());
1496        assert_eq!(client.unwrap().chain_name(), "ethereum");
1497    }
1498
1499    #[test]
1500    fn test_ethereum_client_for_chain_with_http() {
1501        let config = crate::config::ChainsConfig::default();
1502        let http: Arc<dyn HttpClient> = Arc::new(crate::http::NativeHttpClient::new().unwrap());
1503        let client = EthereumClient::for_chain_with_http("polygon", &config, http);
1504        assert!(client.is_ok());
1505        assert_eq!(client.unwrap().chain_name(), "polygon");
1506    }
1507
1508    // ============================================================================
1509    // ChainClient trait default method tests
1510    // ============================================================================
1511
1512    #[tokio::test]
1513    async fn test_chain_client_default_get_token_info() {
1514        use super::mocks::MockChainClient;
1515        // Create a client without token_info set (None)
1516        let client = MockChainClient::new("ethereum", "ETH");
1517        let result = client.get_token_info("0xsometoken").await;
1518        assert!(result.is_err());
1519    }
1520
1521    #[tokio::test]
1522    async fn test_chain_client_default_get_token_holders() {
1523        use super::mocks::MockChainClient;
1524        let client = MockChainClient::new("ethereum", "ETH");
1525        let holders = client.get_token_holders("0xsometoken", 10).await.unwrap();
1526        assert!(holders.is_empty());
1527    }
1528
1529    #[tokio::test]
1530    async fn test_chain_client_default_get_token_holder_count() {
1531        use super::mocks::MockChainClient;
1532        let client = MockChainClient::new("ethereum", "ETH");
1533        let count = client.get_token_holder_count("0xsometoken").await.unwrap();
1534        assert_eq!(count, 0);
1535    }
1536
1537    #[tokio::test]
1538    async fn test_mock_client_factory_creates_chain_client() {
1539        use super::mocks::MockClientFactory;
1540        let factory = MockClientFactory::new();
1541        let client = factory.create_chain_client("anything").unwrap();
1542        assert_eq!(client.chain_name(), "ethereum"); // defaults to ethereum mock
1543    }
1544
1545    #[tokio::test]
1546    async fn test_mock_client_factory_creates_dex_client() {
1547        use super::mocks::MockClientFactory;
1548        let factory = MockClientFactory::new();
1549        let dex = factory.create_dex_client();
1550        let price = dex.get_token_price("ethereum", "0xtest").await;
1551        assert_eq!(price, Some(1.0));
1552    }
1553
1554    #[tokio::test]
1555    async fn test_mock_chain_client_balance() {
1556        use super::mocks::MockChainClient;
1557        let client = MockChainClient::new("ethereum", "ETH");
1558        let balance = client.get_balance("0xtest").await.unwrap();
1559        assert_eq!(balance.formatted, "1.0");
1560        assert_eq!(balance.symbol, "ETH");
1561        assert_eq!(balance.usd_value, Some(2500.0));
1562    }
1563
1564    #[tokio::test]
1565    async fn test_mock_chain_client_transaction() {
1566        use super::mocks::MockChainClient;
1567        let client = MockChainClient::new("ethereum", "ETH");
1568        let tx = client.get_transaction("0xanyhash").await.unwrap();
1569        assert_eq!(tx.hash, "0xmocktx");
1570        assert_eq!(tx.nonce, 42);
1571    }
1572
1573    #[tokio::test]
1574    async fn test_mock_chain_client_block_number() {
1575        use super::mocks::MockChainClient;
1576        let client = MockChainClient::new("ethereum", "ETH");
1577        let block = client.get_block_number().await.unwrap();
1578        assert_eq!(block, 12345678);
1579    }
1580
1581    #[tokio::test]
1582    async fn test_mock_dex_source_data() {
1583        use super::mocks::MockDexSource;
1584        let dex = MockDexSource::new();
1585        let data = dex.get_token_data("ethereum", "0xtest").await.unwrap();
1586        assert_eq!(data.symbol, "MOCK");
1587        assert_eq!(data.price_usd, 1.0);
1588    }
1589
1590    #[tokio::test]
1591    async fn test_mock_dex_source_search() {
1592        use super::mocks::MockDexSource;
1593        let dex = MockDexSource::new();
1594        let results = dex.search_tokens("test", None).await.unwrap();
1595        assert!(results.is_empty());
1596    }
1597
1598    #[tokio::test]
1599    async fn test_mock_dex_source_native_price() {
1600        use super::mocks::MockDexSource;
1601        let dex = MockDexSource::new();
1602        let price = dex.get_native_token_price("ethereum").await;
1603        assert_eq!(price, Some(2500.0));
1604    }
1605
1606    // ========================================================================
1607    // Default ChainClient trait method tests
1608    // ========================================================================
1609
1610    /// Minimal ChainClient impl that uses all default methods.
1611    struct MinimalChainClient;
1612
1613    #[async_trait::async_trait]
1614    impl ChainClient for MinimalChainClient {
1615        fn chain_name(&self) -> &str {
1616            "test"
1617        }
1618
1619        fn native_token_symbol(&self) -> &str {
1620            "TEST"
1621        }
1622
1623        async fn get_balance(&self, _address: &str) -> Result<Balance> {
1624            Ok(Balance {
1625                raw: "0".to_string(),
1626                formatted: "0".to_string(),
1627                decimals: 18,
1628                symbol: "TEST".to_string(),
1629                usd_value: None,
1630            })
1631        }
1632
1633        async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
1634            unimplemented!()
1635        }
1636
1637        async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
1638            Ok(Vec::new())
1639        }
1640
1641        async fn get_block_number(&self) -> Result<u64> {
1642            Ok(0)
1643        }
1644
1645        async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
1646            Ok(Vec::new())
1647        }
1648
1649        async fn enrich_balance_usd(&self, _balance: &mut Balance) {}
1650    }
1651
1652    #[tokio::test]
1653    async fn test_default_get_token_info() {
1654        let client = MinimalChainClient;
1655        let result = client.get_token_info("0xtest").await;
1656        assert!(result.is_err());
1657        assert!(result.unwrap_err().to_string().contains("not supported"));
1658    }
1659
1660    #[tokio::test]
1661    async fn test_default_get_token_holders() {
1662        let client = MinimalChainClient;
1663        let holders = client.get_token_holders("0xtest", 10).await.unwrap();
1664        assert!(holders.is_empty());
1665    }
1666
1667    #[tokio::test]
1668    async fn test_default_get_token_holder_count() {
1669        let client = MinimalChainClient;
1670        let count = client.get_token_holder_count("0xtest").await.unwrap();
1671        assert_eq!(count, 0);
1672    }
1673
1674    // ============================================================================
1675    // Chain Metadata Tests
1676    // ============================================================================
1677
1678    #[test]
1679    fn test_chain_metadata_ethereum() {
1680        let meta = chain_metadata("ethereum").unwrap();
1681        assert_eq!(meta.chain_id, "ethereum");
1682        assert_eq!(meta.native_symbol, "ETH");
1683        assert_eq!(meta.native_decimals, 18);
1684        assert_eq!(meta.explorer_token_base, "https://etherscan.io/token");
1685    }
1686
1687    #[test]
1688    fn test_chain_metadata_ethereum_alias() {
1689        let meta = chain_metadata("eth").unwrap();
1690        assert_eq!(meta.chain_id, "ethereum");
1691        assert_eq!(meta.native_symbol, "ETH");
1692    }
1693
1694    #[test]
1695    fn test_chain_metadata_polygon() {
1696        let meta = chain_metadata("polygon").unwrap();
1697        assert_eq!(meta.chain_id, "polygon");
1698        assert_eq!(meta.native_symbol, "MATIC");
1699        assert_eq!(meta.native_decimals, 18);
1700        assert_eq!(meta.explorer_token_base, "https://polygonscan.com/token");
1701    }
1702
1703    #[test]
1704    fn test_chain_metadata_bsc() {
1705        let meta = chain_metadata("bsc").unwrap();
1706        assert_eq!(meta.chain_id, "bsc");
1707        assert_eq!(meta.native_symbol, "BNB");
1708        assert_eq!(meta.native_decimals, 18);
1709        assert_eq!(meta.explorer_token_base, "https://bscscan.com/token");
1710    }
1711
1712    #[test]
1713    fn test_chain_metadata_solana() {
1714        let meta = chain_metadata("solana").unwrap();
1715        assert_eq!(meta.chain_id, "solana");
1716        assert_eq!(meta.native_symbol, "SOL");
1717        assert_eq!(meta.native_decimals, 9);
1718        assert_eq!(meta.explorer_token_base, "https://solscan.io/token");
1719    }
1720
1721    #[test]
1722    fn test_chain_metadata_solana_alias() {
1723        let meta = chain_metadata("sol").unwrap();
1724        assert_eq!(meta.chain_id, "solana");
1725        assert_eq!(meta.native_symbol, "SOL");
1726    }
1727
1728    #[test]
1729    fn test_chain_metadata_tron() {
1730        let meta = chain_metadata("tron").unwrap();
1731        assert_eq!(meta.chain_id, "tron");
1732        assert_eq!(meta.native_symbol, "TRX");
1733        assert_eq!(meta.native_decimals, 6);
1734        assert_eq!(meta.explorer_token_base, "https://tronscan.org/#/token20");
1735    }
1736
1737    #[test]
1738    fn test_chain_metadata_tron_alias() {
1739        let meta = chain_metadata("trx").unwrap();
1740        assert_eq!(meta.chain_id, "tron");
1741        assert_eq!(meta.native_symbol, "TRX");
1742    }
1743
1744    #[test]
1745    fn test_chain_metadata_arbitrum() {
1746        let meta = chain_metadata("arbitrum").unwrap();
1747        assert_eq!(meta.chain_id, "arbitrum");
1748        assert_eq!(meta.native_symbol, "ETH");
1749        assert_eq!(meta.native_decimals, 18);
1750        assert_eq!(meta.explorer_token_base, "https://arbiscan.io/token");
1751    }
1752
1753    #[test]
1754    fn test_chain_metadata_optimism() {
1755        let meta = chain_metadata("optimism").unwrap();
1756        assert_eq!(meta.chain_id, "optimism");
1757        assert_eq!(meta.native_symbol, "ETH");
1758        assert_eq!(meta.native_decimals, 18);
1759        assert_eq!(
1760            meta.explorer_token_base,
1761            "https://optimistic.etherscan.io/token"
1762        );
1763    }
1764
1765    #[test]
1766    fn test_chain_metadata_base() {
1767        let meta = chain_metadata("base").unwrap();
1768        assert_eq!(meta.chain_id, "base");
1769        assert_eq!(meta.native_symbol, "ETH");
1770        assert_eq!(meta.native_decimals, 18);
1771        assert_eq!(meta.explorer_token_base, "https://basescan.org/token");
1772    }
1773
1774    #[test]
1775    fn test_chain_metadata_case_insensitive() {
1776        let meta1 = chain_metadata("ETHEREUM").unwrap();
1777        let meta2 = chain_metadata("Ethereum").unwrap();
1778        let meta3 = chain_metadata("ethereum").unwrap();
1779        assert_eq!(meta1.chain_id, meta2.chain_id);
1780        assert_eq!(meta2.chain_id, meta3.chain_id);
1781    }
1782
1783    #[test]
1784    fn test_chain_metadata_unknown() {
1785        assert!(chain_metadata("bitcoin").is_none());
1786        assert!(chain_metadata("litecoin").is_none());
1787        assert!(chain_metadata("unknown").is_none());
1788        assert!(chain_metadata("").is_none());
1789    }
1790
1791    #[test]
1792    fn test_native_symbol_ethereum() {
1793        assert_eq!(native_symbol("ethereum"), "ETH");
1794        assert_eq!(native_symbol("eth"), "ETH");
1795    }
1796
1797    #[test]
1798    fn test_native_symbol_polygon() {
1799        assert_eq!(native_symbol("polygon"), "MATIC");
1800    }
1801
1802    #[test]
1803    fn test_native_symbol_bsc() {
1804        assert_eq!(native_symbol("bsc"), "BNB");
1805    }
1806
1807    #[test]
1808    fn test_native_symbol_solana() {
1809        assert_eq!(native_symbol("solana"), "SOL");
1810        assert_eq!(native_symbol("sol"), "SOL");
1811    }
1812
1813    #[test]
1814    fn test_native_symbol_tron() {
1815        assert_eq!(native_symbol("tron"), "TRX");
1816        assert_eq!(native_symbol("trx"), "TRX");
1817    }
1818
1819    #[test]
1820    fn test_native_symbol_arbitrum() {
1821        assert_eq!(native_symbol("arbitrum"), "ETH");
1822    }
1823
1824    #[test]
1825    fn test_native_symbol_optimism() {
1826        assert_eq!(native_symbol("optimism"), "ETH");
1827    }
1828
1829    #[test]
1830    fn test_native_symbol_base() {
1831        assert_eq!(native_symbol("base"), "ETH");
1832    }
1833
1834    #[test]
1835    fn test_native_symbol_unknown() {
1836        assert_eq!(native_symbol("unknown"), "???");
1837        assert_eq!(native_symbol("bitcoin"), "???");
1838        assert_eq!(native_symbol(""), "???");
1839    }
1840
1841    #[test]
1842    fn test_native_symbol_case_insensitive() {
1843        assert_eq!(native_symbol("ETHEREUM"), "ETH");
1844        assert_eq!(native_symbol("Ethereum"), "ETH");
1845        assert_eq!(native_symbol("ethereum"), "ETH");
1846    }
1847
1848    #[tokio::test]
1849    async fn test_chain_client_default_get_code() {
1850        let client = MinimalChainClient;
1851        let result = client.get_code("0x1234").await;
1852        assert!(result.is_err());
1853        let err_msg = result.unwrap_err().to_string();
1854        assert!(err_msg.contains("not supported"));
1855    }
1856
1857    // ============================================================================
1858    // analyze_gas_usage Tests
1859    // ============================================================================
1860
1861    fn tx(hash: &str, gas_used: Option<u64>, input: &str, status: Option<bool>) -> Transaction {
1862        Transaction {
1863            hash: hash.to_string(),
1864            block_number: Some(1),
1865            timestamp: Some(1700000000),
1866            from: "0xfrom".to_string(),
1867            to: Some("0xto".to_string()),
1868            value: "0".to_string(),
1869            gas_limit: 21000,
1870            gas_used,
1871            gas_price: "20000000000".to_string(),
1872            nonce: 0,
1873            input: input.to_string(),
1874            status,
1875        }
1876    }
1877
1878    #[test]
1879    fn test_analyze_gas_usage_empty_transactions() {
1880        let txs: Vec<Transaction> = vec![];
1881        let result = super::analyze_gas_usage(&txs);
1882        assert_eq!(result.avg_gas_used, 0);
1883        assert_eq!(result.max_gas_used, 0);
1884        assert_eq!(result.min_gas_used, 0);
1885        assert_eq!(result.tx_count, 0);
1886        assert_eq!(result.failed_tx_count, 0);
1887        assert_eq!(result.wasted_gas, 0);
1888        assert!(result.gas_by_function.is_empty());
1889    }
1890
1891    #[test]
1892    fn test_analyze_gas_usage_single_tx() {
1893        let txs = vec![tx("0x1", Some(100_000), "0x", Some(true))];
1894        let result = super::analyze_gas_usage(&txs);
1895        assert_eq!(result.avg_gas_used, 100_000);
1896        assert_eq!(result.max_gas_used, 100_000);
1897        assert_eq!(result.min_gas_used, 100_000);
1898        assert_eq!(result.tx_count, 1);
1899    }
1900
1901    #[test]
1902    fn test_analyze_gas_usage_multiple_txs() {
1903        let txs = vec![
1904            tx("0x1", Some(50_000), "0xa9059cbb", Some(true)),
1905            tx("0x2", Some(150_000), "0xa9059cbb", Some(true)),
1906            tx("0x3", Some(100_000), "0xa9059cbb", Some(true)),
1907        ];
1908        let result = super::analyze_gas_usage(&txs);
1909        assert_eq!(result.avg_gas_used, 100_000); // (50+150+100)/3
1910        assert_eq!(result.max_gas_used, 150_000);
1911        assert_eq!(result.min_gas_used, 50_000);
1912        assert_eq!(result.tx_count, 3);
1913    }
1914
1915    #[test]
1916    fn test_analyze_gas_usage_failed_tx() {
1917        let txs = vec![
1918            tx("0x1", Some(80_000), "0x", Some(true)),
1919            tx("0x2", Some(120_000), "0x", Some(false)),
1920        ];
1921        let result = super::analyze_gas_usage(&txs);
1922        assert_eq!(result.failed_tx_count, 1);
1923        assert_eq!(result.wasted_gas, 120_000);
1924    }
1925
1926    #[test]
1927    fn test_analyze_gas_usage_gas_by_function() {
1928        // Selector is first 10 chars (0x + 8 hex) of input
1929        let txs = vec![
1930            tx("0x1", Some(100_000), "0xa9059cbb0000", Some(true)),
1931            tx("0x2", Some(200_000), "0xa9059cbb0000", Some(true)),
1932            tx("0x3", Some(50_000), "0x095ea7b30000", Some(true)),
1933        ];
1934        let result = super::analyze_gas_usage(&txs);
1935        assert_eq!(result.gas_by_function.len(), 2);
1936        let by_sel: std::collections::HashMap<_, _> = result
1937            .gas_by_function
1938            .iter()
1939            .map(|g| (g.function.as_str(), g))
1940            .collect();
1941        let transfer = by_sel.get("0xa9059cbb").unwrap();
1942        assert_eq!(transfer.call_count, 2);
1943        assert_eq!(transfer.total_gas, 300_000);
1944        assert_eq!(transfer.avg_gas, 150_000);
1945        let approve = by_sel.get("0x095ea7b3").unwrap();
1946        assert_eq!(approve.call_count, 1);
1947        assert_eq!(approve.total_gas, 50_000);
1948    }
1949
1950    #[test]
1951    fn test_analyze_gas_usage_input_0x_transfer() {
1952        let txs = vec![tx("0x1", Some(21_000), "0x", Some(true))];
1953        let result = super::analyze_gas_usage(&txs);
1954        assert_eq!(result.gas_by_function.len(), 1);
1955        assert_eq!(result.gas_by_function[0].function, "transfer()");
1956    }
1957
1958    #[test]
1959    fn test_analyze_gas_usage_input_empty_transfer() {
1960        let txs = vec![tx("0x1", Some(21_000), "", Some(true))];
1961        let result = super::analyze_gas_usage(&txs);
1962        assert_eq!(result.gas_by_function.len(), 1);
1963        assert_eq!(result.gas_by_function[0].function, "transfer()");
1964    }
1965
1966    #[test]
1967    fn test_analyze_gas_usage_gas_used_none() {
1968        let txs = vec![tx("0x1", None, "0x", Some(true))];
1969        let result = super::analyze_gas_usage(&txs);
1970        assert_eq!(result.avg_gas_used, 0);
1971        assert_eq!(result.max_gas_used, 0);
1972        assert_eq!(result.min_gas_used, 0);
1973    }
1974
1975    #[test]
1976    fn test_analyze_gas_usage_short_input_uses_full_input_as_selector() {
1977        let txs = vec![tx("0x1", Some(50_000), "0x1234567", Some(true))];
1978        let result = super::analyze_gas_usage(&txs);
1979        assert_eq!(result.gas_by_function.len(), 1);
1980        assert_eq!(result.gas_by_function[0].function, "0x1234567");
1981    }
1982}
1983
1984// ============================================================================
1985// Mock Test Utilities
1986// ============================================================================
1987
1988/// Test helper module providing mock implementations of chain client traits.
1989///
1990/// These mocks are available across all test modules in the crate for
1991/// end-to-end testing of CLI `run()` functions without network calls.
1992#[cfg(test)]
1993pub mod mocks {
1994    use super::*;
1995    use crate::chains::dex::{DexDataSource, DexTokenData, TokenSearchResult};
1996    use async_trait::async_trait;
1997
1998    /// Mock chain client with configurable responses.
1999    #[derive(Debug, Clone)]
2000    pub struct MockChainClient {
2001        pub chain: String,
2002        pub symbol: String,
2003        pub balance: Balance,
2004        pub transaction: Transaction,
2005        pub transactions: Vec<Transaction>,
2006        pub token_balances: Vec<TokenBalance>,
2007        pub block_number: u64,
2008        pub token_info: Option<Token>,
2009        pub token_holders: Vec<TokenHolder>,
2010        pub token_holder_count: u64,
2011    }
2012
2013    impl MockChainClient {
2014        /// Creates a mock client with sensible default test data.
2015        pub fn new(chain: &str, symbol: &str) -> Self {
2016            Self {
2017                chain: chain.to_string(),
2018                symbol: symbol.to_string(),
2019                balance: Balance {
2020                    raw: "1000000000000000000".to_string(),
2021                    formatted: "1.0".to_string(),
2022                    decimals: 18,
2023                    symbol: symbol.to_string(),
2024                    usd_value: Some(2500.0),
2025                },
2026                transaction: Transaction {
2027                    hash: "0xmocktx".to_string(),
2028                    block_number: Some(12345678),
2029                    timestamp: Some(1700000000),
2030                    from: "0xfrom".to_string(),
2031                    to: Some("0xto".to_string()),
2032                    value: "1.0".to_string(),
2033                    gas_limit: 21000,
2034                    gas_used: Some(21000),
2035                    gas_price: "20000000000".to_string(),
2036                    nonce: 42,
2037                    input: "0x".to_string(),
2038                    status: Some(true),
2039                },
2040                transactions: vec![],
2041                token_balances: vec![],
2042                block_number: 12345678,
2043                token_info: None,
2044                token_holders: vec![],
2045                token_holder_count: 0,
2046            }
2047        }
2048    }
2049
2050    #[async_trait]
2051    impl ChainClient for MockChainClient {
2052        fn chain_name(&self) -> &str {
2053            &self.chain
2054        }
2055
2056        fn native_token_symbol(&self) -> &str {
2057            &self.symbol
2058        }
2059
2060        async fn get_balance(&self, _address: &str) -> Result<Balance> {
2061            Ok(self.balance.clone())
2062        }
2063
2064        async fn enrich_balance_usd(&self, _balance: &mut Balance) {
2065            // Mock: no-op, balance already has usd_value set
2066        }
2067
2068        async fn get_transaction(&self, _hash: &str) -> Result<Transaction> {
2069            Ok(self.transaction.clone())
2070        }
2071
2072        async fn get_transactions(&self, _address: &str, _limit: u32) -> Result<Vec<Transaction>> {
2073            Ok(self.transactions.clone())
2074        }
2075
2076        async fn get_block_number(&self) -> Result<u64> {
2077            Ok(self.block_number)
2078        }
2079
2080        async fn get_token_balances(&self, _address: &str) -> Result<Vec<TokenBalance>> {
2081            Ok(self.token_balances.clone())
2082        }
2083
2084        async fn get_token_info(&self, _address: &str) -> Result<Token> {
2085            match &self.token_info {
2086                Some(t) => Ok(t.clone()),
2087                None => Err(crate::error::ScopeError::Chain(
2088                    "Token info not available".to_string(),
2089                )),
2090            }
2091        }
2092
2093        async fn get_token_holders(&self, _address: &str, _limit: u32) -> Result<Vec<TokenHolder>> {
2094            Ok(self.token_holders.clone())
2095        }
2096
2097        async fn get_token_holder_count(&self, _address: &str) -> Result<u64> {
2098            Ok(self.token_holder_count)
2099        }
2100    }
2101
2102    /// Mock DEX data source with configurable responses.
2103    #[derive(Debug, Clone)]
2104    pub struct MockDexSource {
2105        pub token_price: Option<f64>,
2106        pub native_price: Option<f64>,
2107        pub token_data: Option<DexTokenData>,
2108        pub search_results: Vec<TokenSearchResult>,
2109    }
2110
2111    impl Default for MockDexSource {
2112        fn default() -> Self {
2113            Self::new()
2114        }
2115    }
2116
2117    impl MockDexSource {
2118        /// Creates a mock DEX source with default test data.
2119        pub fn new() -> Self {
2120            Self {
2121                token_price: Some(1.0),
2122                native_price: Some(2500.0),
2123                token_data: Some(DexTokenData {
2124                    address: "0xmocktoken".to_string(),
2125                    symbol: "MOCK".to_string(),
2126                    name: "Mock Token".to_string(),
2127                    price_usd: 1.0,
2128                    price_change_24h: 5.0,
2129                    price_change_6h: 2.0,
2130                    price_change_1h: 0.5,
2131                    price_change_5m: 0.1,
2132                    volume_24h: 1_000_000.0,
2133                    volume_6h: 250_000.0,
2134                    volume_1h: 50_000.0,
2135                    liquidity_usd: 5_000_000.0,
2136                    market_cap: Some(100_000_000.0),
2137                    fdv: Some(200_000_000.0),
2138                    pairs: vec![],
2139                    price_history: vec![],
2140                    volume_history: vec![],
2141                    total_buys_24h: 500,
2142                    total_sells_24h: 450,
2143                    total_buys_6h: 120,
2144                    total_sells_6h: 110,
2145                    total_buys_1h: 20,
2146                    total_sells_1h: 18,
2147                    earliest_pair_created_at: Some(1690000000),
2148                    image_url: None,
2149                    websites: vec![],
2150                    socials: vec![],
2151                    dexscreener_url: None,
2152                }),
2153                search_results: vec![],
2154            }
2155        }
2156    }
2157
2158    #[async_trait]
2159    impl DexDataSource for MockDexSource {
2160        async fn get_token_price(&self, _chain: &str, _address: &str) -> Option<f64> {
2161            self.token_price
2162        }
2163
2164        async fn get_native_token_price(&self, _chain: &str) -> Option<f64> {
2165            self.native_price
2166        }
2167
2168        async fn get_token_data(&self, _chain: &str, _address: &str) -> Result<DexTokenData> {
2169            match &self.token_data {
2170                Some(data) => Ok(data.clone()),
2171                None => Err(crate::error::ScopeError::NotFound(
2172                    "No DEX data found".to_string(),
2173                )),
2174            }
2175        }
2176
2177        async fn search_tokens(
2178            &self,
2179            _query: &str,
2180            _chain: Option<&str>,
2181        ) -> Result<Vec<TokenSearchResult>> {
2182            Ok(self.search_results.clone())
2183        }
2184    }
2185
2186    /// Mock client factory that returns pre-configured mock clients.
2187    pub struct MockClientFactory {
2188        pub mock_client: MockChainClient,
2189        pub mock_dex: MockDexSource,
2190    }
2191
2192    impl Default for MockClientFactory {
2193        fn default() -> Self {
2194            Self::new()
2195        }
2196    }
2197
2198    impl MockClientFactory {
2199        /// Creates a factory with default mock data for Ethereum.
2200        pub fn new() -> Self {
2201            Self {
2202                mock_client: MockChainClient::new("ethereum", "ETH"),
2203                mock_dex: MockDexSource::new(),
2204            }
2205        }
2206    }
2207
2208    impl ChainClientFactory for MockClientFactory {
2209        fn create_chain_client(&self, _chain: &str) -> Result<Box<dyn ChainClient>> {
2210            Ok(Box::new(self.mock_client.clone()))
2211        }
2212
2213        fn create_dex_client(&self) -> Box<dyn DexDataSource> {
2214            Box::new(self.mock_dex.clone())
2215        }
2216    }
2217}