Skip to main content

scope/cli/
address.rs

1//! # Address Analysis Command
2//!
3//! This module implements the `bca address` command for analyzing
4//! blockchain addresses. It retrieves balance information, transaction
5//! history, and token holdings.
6//!
7//! ## Usage
8//!
9//! ```bash
10//! # Basic address analysis
11//! bca address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2
12//!
13//! # Specify chain
14//! bca address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --chain ethereum
15//!
16//! # Output as JSON
17//! bca address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2 --format json
18//! ```
19
20use crate::chains::{
21    ChainClient, ChainClientFactory, validate_solana_address, validate_tron_address,
22};
23use crate::config::{Config, OutputFormat};
24use crate::error::Result;
25use clap::Args;
26
27/// Arguments for the address analysis command.
28#[derive(Debug, Clone, Args)]
29pub struct AddressArgs {
30    /// The blockchain address to analyze.
31    ///
32    /// Must be a valid address format for the target chain
33    /// (e.g., 0x-prefixed 40-character hex for Ethereum).
34    #[arg(value_name = "ADDRESS")]
35    pub address: String,
36
37    /// Target blockchain network.
38    ///
39    /// EVM chains: ethereum, polygon, arbitrum, optimism, base, bsc, aegis
40    /// Non-EVM chains: solana, tron
41    #[arg(short, long, default_value = "ethereum")]
42    pub chain: String,
43
44    /// Override output format for this command.
45    #[arg(short, long, value_name = "FORMAT")]
46    pub format: Option<OutputFormat>,
47
48    /// Include full transaction history.
49    #[arg(long)]
50    pub include_txs: bool,
51
52    /// Include token balances (ERC-20, ERC-721).
53    #[arg(long)]
54    pub include_tokens: bool,
55
56    /// Maximum number of transactions to retrieve.
57    #[arg(long, default_value = "100")]
58    pub limit: u32,
59}
60
61/// Result of an address analysis.
62#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
63pub struct AddressReport {
64    /// The analyzed address.
65    pub address: String,
66
67    /// The blockchain network.
68    pub chain: String,
69
70    /// Native token balance.
71    pub balance: Balance,
72
73    /// Transaction count (nonce).
74    pub transaction_count: u64,
75
76    /// Recent transactions (if requested).
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub transactions: Option<Vec<TransactionSummary>>,
79
80    /// Token balances (if requested).
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub tokens: Option<Vec<TokenBalance>>,
83}
84
85/// Balance representation with multiple units.
86#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
87pub struct Balance {
88    /// Raw balance in smallest unit (e.g., wei for Ethereum).
89    pub raw: String,
90
91    /// Human-readable balance in native token (e.g., ETH).
92    pub formatted: String,
93
94    /// Balance in USD (if price available).
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub usd: Option<f64>,
97}
98
99/// Summary of a transaction.
100#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
101pub struct TransactionSummary {
102    /// Transaction hash.
103    pub hash: String,
104
105    /// Block number.
106    pub block_number: u64,
107
108    /// Timestamp (Unix epoch).
109    pub timestamp: u64,
110
111    /// Sender address.
112    pub from: String,
113
114    /// Recipient address.
115    pub to: Option<String>,
116
117    /// Value transferred.
118    pub value: String,
119
120    /// Transaction status (success/failure).
121    pub status: bool,
122}
123
124/// Token balance information.
125#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
126pub struct TokenBalance {
127    /// Token contract address.
128    pub contract_address: String,
129
130    /// Token symbol.
131    pub symbol: String,
132
133    /// Token name.
134    pub name: String,
135
136    /// Token decimals.
137    pub decimals: u8,
138
139    /// Raw balance.
140    pub balance: String,
141
142    /// Formatted balance.
143    pub formatted_balance: String,
144}
145
146/// Executes the address analysis command.
147///
148/// # Arguments
149///
150/// * `args` - The parsed command arguments
151/// * `config` - Application configuration
152///
153/// # Returns
154///
155/// Returns `Ok(())` on success, or an error if the analysis fails.
156///
157/// # Errors
158///
159/// Returns `ScopeError::InvalidAddress` if the address format is invalid.
160/// Returns `ScopeError::Request` if API calls fail.
161pub async fn run(
162    mut args: AddressArgs,
163    config: &Config,
164    clients: &dyn ChainClientFactory,
165) -> Result<()> {
166    // Auto-infer chain if using default and address format is recognizable
167    if args.chain == "ethereum"
168        && let Some(inferred) = crate::chains::infer_chain_from_address(&args.address)
169        && inferred != "ethereum"
170    {
171        tracing::info!("Auto-detected chain: {}", inferred);
172        println!("Auto-detected chain: {}", inferred);
173        args.chain = inferred.to_string();
174    }
175
176    tracing::info!(
177        address = %args.address,
178        chain = %args.chain,
179        "Starting address analysis"
180    );
181
182    // Validate address format
183    validate_address(&args.address, &args.chain)?;
184
185    println!("Analyzing address on {}...", args.chain);
186
187    let client = clients.create_chain_client(&args.chain)?;
188    let report = analyze_address(&args, client.as_ref()).await?;
189
190    // Output based on format
191    let format = args.format.unwrap_or(config.output.format);
192    output_report(&report, format)?;
193
194    Ok(())
195}
196
197/// Analyzes an address using a unified chain client.
198async fn analyze_address(args: &AddressArgs, client: &dyn ChainClient) -> Result<AddressReport> {
199    // Fetch balance
200    let mut chain_balance = client.get_balance(&args.address).await?;
201    client.enrich_balance_usd(&mut chain_balance).await;
202
203    let balance = Balance {
204        raw: chain_balance.raw.clone(),
205        formatted: chain_balance.formatted.clone(),
206        usd: chain_balance.usd_value,
207    };
208
209    // Fetch transactions if requested
210    let transactions = if args.include_txs {
211        match client.get_transactions(&args.address, args.limit).await {
212            Ok(txs) => Some(
213                txs.into_iter()
214                    .map(|tx| TransactionSummary {
215                        hash: tx.hash,
216                        block_number: tx.block_number.unwrap_or(0),
217                        timestamp: tx.timestamp.unwrap_or(0),
218                        from: tx.from,
219                        to: tx.to,
220                        value: tx.value,
221                        status: tx.status.unwrap_or(true),
222                    })
223                    .collect(),
224            ),
225            Err(e) => {
226                tracing::warn!("Failed to fetch transactions: {}", e);
227                Some(vec![])
228            }
229        }
230    } else {
231        None
232    };
233
234    // Transaction count is the number we fetched (or 0)
235    let transaction_count = transactions.as_ref().map(|t| t.len() as u64).unwrap_or(0);
236
237    // Fetch token balances if requested
238    let tokens = if args.include_tokens {
239        match client.get_token_balances(&args.address).await {
240            Ok(token_bals) => Some(
241                token_bals
242                    .into_iter()
243                    .map(|tb| TokenBalance {
244                        contract_address: tb.token.contract_address,
245                        symbol: tb.token.symbol,
246                        name: tb.token.name,
247                        decimals: tb.token.decimals,
248                        balance: tb.balance,
249                        formatted_balance: tb.formatted_balance,
250                    })
251                    .collect(),
252            ),
253            Err(e) => {
254                tracing::warn!("Failed to fetch token balances: {}", e);
255                Some(vec![])
256            }
257        }
258    } else {
259        None
260    };
261
262    Ok(AddressReport {
263        address: args.address.clone(),
264        chain: args.chain.clone(),
265        balance,
266        transaction_count,
267        transactions,
268        tokens,
269    })
270}
271
272/// Validates an address format for the given chain.
273fn validate_address(address: &str, chain: &str) -> Result<()> {
274    match chain {
275        // EVM-compatible chains use 0x-prefixed 40-char hex addresses
276        "ethereum" | "polygon" | "arbitrum" | "optimism" | "base" | "bsc" | "aegis" => {
277            if !address.starts_with("0x") {
278                return Err(crate::error::ScopeError::InvalidAddress(format!(
279                    "Address must start with '0x': {}",
280                    address
281                )));
282            }
283            if address.len() != 42 {
284                return Err(crate::error::ScopeError::InvalidAddress(format!(
285                    "Address must be 42 characters (0x + 40 hex): {}",
286                    address
287                )));
288            }
289            // Validate hex characters
290            if !address[2..].chars().all(|c| c.is_ascii_hexdigit()) {
291                return Err(crate::error::ScopeError::InvalidAddress(format!(
292                    "Address contains invalid hex characters: {}",
293                    address
294                )));
295            }
296        }
297        // Solana uses base58-encoded 32-byte addresses
298        "solana" => {
299            validate_solana_address(address)?;
300        }
301        // Tron uses T-prefixed base58check addresses
302        "tron" => {
303            validate_tron_address(address)?;
304        }
305        _ => {
306            return Err(crate::error::ScopeError::Chain(format!(
307                "Unsupported chain: {}. Supported: ethereum, polygon, arbitrum, optimism, base, bsc, aegis, solana, tron",
308                chain
309            )));
310        }
311    }
312    Ok(())
313}
314
315/// Outputs the address report in the specified format.
316fn output_report(report: &AddressReport, format: OutputFormat) -> Result<()> {
317    match format {
318        OutputFormat::Json => {
319            let json = serde_json::to_string_pretty(report)?;
320            println!("{}", json);
321        }
322        OutputFormat::Csv => {
323            // CSV format for address is a single row
324            println!("address,chain,balance,transaction_count");
325            println!(
326                "{},{},{},{}",
327                report.address, report.chain, report.balance.formatted, report.transaction_count
328            );
329        }
330        OutputFormat::Table => {
331            println!("Address Analysis Report");
332            println!("=======================");
333            println!("Address:      {}", report.address);
334            println!("Chain:        {}", report.chain);
335            println!("Balance:      {}", report.balance.formatted);
336            if let Some(usd) = report.balance.usd {
337                println!("Value (USD):  ${:.2}", usd);
338            }
339            println!("Transactions: {}", report.transaction_count);
340
341            if let Some(ref tokens) = report.tokens
342                && !tokens.is_empty()
343            {
344                println!("\nToken Balances:");
345                for token in tokens {
346                    println!(
347                        "  {} ({}): {}",
348                        token.name, token.symbol, token.formatted_balance
349                    );
350                }
351            }
352        }
353    }
354    Ok(())
355}
356
357// ============================================================================
358// Unit Tests
359// ============================================================================
360
361#[cfg(test)]
362mod tests {
363    use super::*;
364
365    #[test]
366    fn test_validate_address_valid_ethereum() {
367        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "ethereum");
368        assert!(result.is_ok());
369    }
370
371    #[test]
372    fn test_validate_address_valid_lowercase() {
373        let result = validate_address("0x742d35cc6634c0532925a3b844bc9e7595f1b3c2", "ethereum");
374        assert!(result.is_ok());
375    }
376
377    #[test]
378    fn test_validate_address_valid_polygon() {
379        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "polygon");
380        assert!(result.is_ok());
381    }
382
383    #[test]
384    fn test_validate_address_missing_prefix() {
385        let result = validate_address("742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "ethereum");
386        assert!(result.is_err());
387        assert!(result.unwrap_err().to_string().contains("0x"));
388    }
389
390    #[test]
391    fn test_validate_address_too_short() {
392        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3", "ethereum");
393        assert!(result.is_err());
394        assert!(result.unwrap_err().to_string().contains("42 characters"));
395    }
396
397    #[test]
398    fn test_validate_address_too_long() {
399        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2a", "ethereum");
400        assert!(result.is_err());
401    }
402
403    #[test]
404    fn test_validate_address_invalid_hex() {
405        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1bXYZ", "ethereum");
406        assert!(result.is_err());
407        assert!(result.unwrap_err().to_string().contains("invalid hex"));
408    }
409
410    #[test]
411    fn test_validate_address_unsupported_chain() {
412        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "bitcoin");
413        assert!(result.is_err());
414        assert!(
415            result
416                .unwrap_err()
417                .to_string()
418                .contains("Unsupported chain")
419        );
420    }
421
422    #[test]
423    fn test_validate_address_valid_bsc() {
424        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "bsc");
425        assert!(result.is_ok());
426    }
427
428    #[test]
429    fn test_validate_address_valid_aegis() {
430        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "aegis");
431        assert!(result.is_ok());
432    }
433
434    #[test]
435    fn test_validate_address_valid_solana() {
436        let result = validate_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", "solana");
437        assert!(result.is_ok());
438    }
439
440    #[test]
441    fn test_validate_address_invalid_solana() {
442        // EVM address should fail for Solana
443        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "solana");
444        assert!(result.is_err());
445    }
446
447    #[test]
448    fn test_validate_address_valid_tron() {
449        let result = validate_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "tron");
450        assert!(result.is_ok());
451    }
452
453    #[test]
454    fn test_validate_address_invalid_tron() {
455        // EVM address should fail for Tron
456        let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "tron");
457        assert!(result.is_err());
458    }
459
460    #[test]
461    fn test_address_args_default_values() {
462        use clap::Parser;
463
464        #[derive(Parser)]
465        struct TestCli {
466            #[command(flatten)]
467            args: AddressArgs,
468        }
469
470        let cli = TestCli::try_parse_from(["test", "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"])
471            .unwrap();
472
473        assert_eq!(cli.args.chain, "ethereum");
474        assert_eq!(cli.args.limit, 100);
475        assert!(!cli.args.include_txs);
476        assert!(!cli.args.include_tokens);
477        assert!(cli.args.format.is_none());
478    }
479
480    #[test]
481    fn test_address_args_with_options() {
482        use clap::Parser;
483
484        #[derive(Parser)]
485        struct TestCli {
486            #[command(flatten)]
487            args: AddressArgs,
488        }
489
490        let cli = TestCli::try_parse_from([
491            "test",
492            "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
493            "--chain",
494            "polygon",
495            "--include-txs",
496            "--include-tokens",
497            "--limit",
498            "50",
499            "--format",
500            "json",
501        ])
502        .unwrap();
503
504        assert_eq!(cli.args.chain, "polygon");
505        assert_eq!(cli.args.limit, 50);
506        assert!(cli.args.include_txs);
507        assert!(cli.args.include_tokens);
508        assert_eq!(cli.args.format, Some(OutputFormat::Json));
509    }
510
511    #[test]
512    fn test_address_report_serialization() {
513        let report = AddressReport {
514            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
515            chain: "ethereum".to_string(),
516            balance: Balance {
517                raw: "1000000000000000000".to_string(),
518                formatted: "1.0".to_string(),
519                usd: Some(3500.0),
520            },
521            transaction_count: 42,
522            transactions: None,
523            tokens: None,
524        };
525
526        let json = serde_json::to_string(&report).unwrap();
527        assert!(json.contains("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"));
528        assert!(json.contains("ethereum"));
529        assert!(json.contains("3500"));
530
531        // Verify None fields are skipped
532        assert!(!json.contains("transactions"));
533        assert!(!json.contains("tokens"));
534    }
535
536    #[test]
537    fn test_balance_serialization() {
538        let balance = Balance {
539            raw: "1000000000000000000".to_string(),
540            formatted: "1.0 ETH".to_string(),
541            usd: None,
542        };
543
544        let json = serde_json::to_string(&balance).unwrap();
545        assert!(json.contains("1000000000000000000"));
546        assert!(json.contains("1.0 ETH"));
547        assert!(!json.contains("usd")); // None should be skipped
548    }
549
550    #[test]
551    fn test_transaction_summary_serialization() {
552        let tx = TransactionSummary {
553            hash: "0xabc123".to_string(),
554            block_number: 12345,
555            timestamp: 1700000000,
556            from: "0xfrom".to_string(),
557            to: Some("0xto".to_string()),
558            value: "1.0".to_string(),
559            status: true,
560        };
561
562        let json = serde_json::to_string(&tx).unwrap();
563        assert!(json.contains("0xabc123"));
564        assert!(json.contains("12345"));
565        assert!(json.contains("true"));
566    }
567
568    #[test]
569    fn test_token_balance_serialization() {
570        let token = TokenBalance {
571            contract_address: "0xtoken".to_string(),
572            symbol: "USDC".to_string(),
573            name: "USD Coin".to_string(),
574            decimals: 6,
575            balance: "1000000".to_string(),
576            formatted_balance: "1.0".to_string(),
577        };
578
579        let json = serde_json::to_string(&token).unwrap();
580        assert!(json.contains("USDC"));
581        assert!(json.contains("USD Coin"));
582        assert!(json.contains("\"decimals\":6"));
583    }
584
585    // ========================================================================
586    // Output formatting tests
587    // ========================================================================
588
589    fn make_test_report() -> AddressReport {
590        AddressReport {
591            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
592            chain: "ethereum".to_string(),
593            balance: Balance {
594                raw: "1000000000000000000".to_string(),
595                formatted: "1.0 ETH".to_string(),
596                usd: Some(3500.0),
597            },
598            transaction_count: 42,
599            transactions: Some(vec![TransactionSummary {
600                hash: "0xabc".to_string(),
601                block_number: 12345,
602                timestamp: 1700000000,
603                from: "0xfrom".to_string(),
604                to: Some("0xto".to_string()),
605                value: "1.0".to_string(),
606                status: true,
607            }]),
608            tokens: Some(vec![TokenBalance {
609                contract_address: "0xusdc".to_string(),
610                symbol: "USDC".to_string(),
611                name: "USD Coin".to_string(),
612                decimals: 6,
613                balance: "1000000".to_string(),
614                formatted_balance: "1.0".to_string(),
615            }]),
616        }
617    }
618
619    #[test]
620    fn test_output_report_json() {
621        let report = make_test_report();
622        let result = output_report(&report, OutputFormat::Json);
623        assert!(result.is_ok());
624    }
625
626    #[test]
627    fn test_output_report_csv() {
628        let report = make_test_report();
629        let result = output_report(&report, OutputFormat::Csv);
630        assert!(result.is_ok());
631    }
632
633    #[test]
634    fn test_output_report_table() {
635        let report = make_test_report();
636        let result = output_report(&report, OutputFormat::Table);
637        assert!(result.is_ok());
638    }
639
640    #[test]
641    fn test_output_report_table_no_usd() {
642        let mut report = make_test_report();
643        report.balance.usd = None;
644        let result = output_report(&report, OutputFormat::Table);
645        assert!(result.is_ok());
646    }
647
648    #[test]
649    fn test_output_report_table_no_tokens() {
650        let mut report = make_test_report();
651        report.tokens = None;
652        let result = output_report(&report, OutputFormat::Table);
653        assert!(result.is_ok());
654    }
655
656    #[test]
657    fn test_output_report_table_empty_tokens() {
658        let mut report = make_test_report();
659        report.tokens = Some(vec![]);
660        let result = output_report(&report, OutputFormat::Table);
661        assert!(result.is_ok());
662    }
663
664    // ========================================================================
665    // End-to-end tests using MockClientFactory
666    // ========================================================================
667
668    use crate::chains::mocks::{MockChainClient, MockClientFactory};
669
670    fn mock_factory() -> MockClientFactory {
671        MockClientFactory::new()
672    }
673
674    #[tokio::test]
675    async fn test_run_ethereum_address() {
676        let config = Config::default();
677        let factory = mock_factory();
678        let args = AddressArgs {
679            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
680            chain: "ethereum".to_string(),
681            format: Some(OutputFormat::Json),
682            include_txs: false,
683            include_tokens: false,
684            limit: 10,
685        };
686        let result = super::run(args, &config, &factory).await;
687        assert!(result.is_ok());
688    }
689
690    #[tokio::test]
691    async fn test_run_with_transactions() {
692        let config = Config::default();
693        let mut factory = mock_factory();
694        factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
695        let args = AddressArgs {
696            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
697            chain: "ethereum".to_string(),
698            format: Some(OutputFormat::Json),
699            include_txs: true,
700            include_tokens: false,
701            limit: 10,
702        };
703        let result = super::run(args, &config, &factory).await;
704        assert!(result.is_ok());
705    }
706
707    #[tokio::test]
708    async fn test_run_with_tokens() {
709        let config = Config::default();
710        let mut factory = mock_factory();
711        factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
712            token: crate::chains::Token {
713                contract_address: "0xusdc".to_string(),
714                symbol: "USDC".to_string(),
715                name: "USD Coin".to_string(),
716                decimals: 6,
717            },
718            balance: "1000000".to_string(),
719            formatted_balance: "1.0".to_string(),
720            usd_value: Some(1.0),
721        }];
722        let args = AddressArgs {
723            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
724            chain: "ethereum".to_string(),
725            format: Some(OutputFormat::Table),
726            include_txs: false,
727            include_tokens: true,
728            limit: 10,
729        };
730        let result = super::run(args, &config, &factory).await;
731        assert!(result.is_ok());
732    }
733
734    #[tokio::test]
735    async fn test_run_auto_detect_solana() {
736        let config = Config::default();
737        let mut factory = mock_factory();
738        factory.mock_client = MockChainClient::new("solana", "SOL");
739        let args = AddressArgs {
740            // This is a Solana address format
741            address: "DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy".to_string(),
742            chain: "ethereum".to_string(), // Will be auto-detected
743            format: Some(OutputFormat::Json),
744            include_txs: false,
745            include_tokens: false,
746            limit: 10,
747        };
748        let result = super::run(args, &config, &factory).await;
749        assert!(result.is_ok());
750    }
751
752    #[tokio::test]
753    async fn test_run_csv_format() {
754        let config = Config::default();
755        let factory = mock_factory();
756        let args = AddressArgs {
757            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
758            chain: "ethereum".to_string(),
759            format: Some(OutputFormat::Csv),
760            include_txs: false,
761            include_tokens: false,
762            limit: 10,
763        };
764        let result = super::run(args, &config, &factory).await;
765        assert!(result.is_ok());
766    }
767
768    #[tokio::test]
769    async fn test_run_all_features() {
770        let config = Config::default();
771        let mut factory = mock_factory();
772        factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
773        factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
774            token: crate::chains::Token {
775                contract_address: "0xtoken".to_string(),
776                symbol: "TEST".to_string(),
777                name: "Test Token".to_string(),
778                decimals: 18,
779            },
780            balance: "1000000000000000000".to_string(),
781            formatted_balance: "1.0".to_string(),
782            usd_value: None,
783        }];
784        let args = AddressArgs {
785            address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
786            chain: "ethereum".to_string(),
787            format: Some(OutputFormat::Table),
788            include_txs: true,
789            include_tokens: true,
790            limit: 50,
791        };
792        let result = super::run(args, &config, &factory).await;
793        assert!(result.is_ok());
794    }
795}