1use 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#[derive(Debug, Clone, Args)]
29#[command(after_help = "\x1b[1mExamples:\x1b[0m
30 scope address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2
31 scope address 0x742d... --include-txs --include-tokens
32 scope address 0x742d... --dossier --report dossier.md
33 scope address DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy --chain solana")]
34pub struct AddressArgs {
35 #[arg(value_name = "ADDRESS")]
40 pub address: String,
41
42 #[arg(short, long, default_value = "ethereum")]
47 pub chain: String,
48
49 #[arg(short, long, value_name = "FORMAT")]
51 pub format: Option<OutputFormat>,
52
53 #[arg(long)]
55 pub include_txs: bool,
56
57 #[arg(long)]
59 pub include_tokens: bool,
60
61 #[arg(long, default_value = "100")]
63 pub limit: u32,
64
65 #[arg(long, value_name = "PATH")]
67 pub report: Option<std::path::PathBuf>,
68
69 #[arg(long, default_value_t = false)]
74 pub dossier: bool,
75}
76
77#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
79pub struct AddressReport {
80 pub address: String,
82
83 pub chain: String,
85
86 pub balance: Balance,
88
89 pub transaction_count: u64,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub transactions: Option<Vec<TransactionSummary>>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub tokens: Option<Vec<TokenBalance>>,
99}
100
101#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
103pub struct Balance {
104 pub raw: String,
106
107 pub formatted: String,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub usd: Option<f64>,
113}
114
115#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
117pub struct TransactionSummary {
118 pub hash: String,
120
121 pub block_number: u64,
123
124 pub timestamp: u64,
126
127 pub from: String,
129
130 pub to: Option<String>,
132
133 pub value: String,
135
136 pub status: bool,
138}
139
140#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
142pub struct TokenBalance {
143 pub contract_address: String,
145
146 pub symbol: String,
148
149 pub name: String,
151
152 pub decimals: u8,
154
155 pub balance: String,
157
158 pub formatted_balance: String,
160}
161
162pub async fn run(
178 mut args: AddressArgs,
179 config: &Config,
180 clients: &dyn ChainClientFactory,
181) -> Result<()> {
182 if args.chain == "ethereum"
184 && let Some(inferred) = crate::chains::infer_chain_from_address(&args.address)
185 && inferred != "ethereum"
186 {
187 tracing::info!("Auto-detected chain: {}", inferred);
188 println!("Auto-detected chain: {}", inferred);
189 args.chain = inferred.to_string();
190 }
191
192 tracing::info!(
193 address = %args.address,
194 chain = %args.chain,
195 "Starting address analysis"
196 );
197
198 validate_address(&args.address, &args.chain)?;
200
201 let mut analysis_args = args.clone();
203 if args.dossier {
204 analysis_args.include_txs = true;
205 analysis_args.include_tokens = true;
206 }
207
208 let sp = crate::cli::progress::Spinner::new(&format!("Analyzing address on {}...", args.chain));
209
210 let client = clients.create_chain_client(&args.chain)?;
211 let report = analyze_address(&analysis_args, client.as_ref()).await?;
212
213 let risk_assessment = if args.dossier {
215 sp.set_message("Running risk assessment...");
216 let engine = match crate::compliance::datasource::BlockchainDataClient::from_env_opt() {
217 Some(client) => crate::compliance::risk::RiskEngine::with_data_client(client),
218 None => crate::compliance::risk::RiskEngine::new(),
219 };
220 engine.assess_address(&args.address, &args.chain).await.ok()
221 } else {
222 None
223 };
224
225 sp.finish("Analysis complete.");
226
227 let format = args.format.unwrap_or(config.output.format);
229 if format == OutputFormat::Markdown {
230 if args.dossier && risk_assessment.as_ref().is_some() {
231 let risk = risk_assessment.as_ref().unwrap();
232 println!(
233 "{}",
234 crate::cli::address_report::generate_dossier_report(&report, risk)
235 );
236 } else {
237 println!(
238 "{}",
239 crate::cli::address_report::generate_address_report(&report)
240 );
241 }
242 } else if args.dossier && risk_assessment.is_some() {
243 let risk = risk_assessment.as_ref().unwrap();
244 output_report(&report, format)?;
245 println!();
246 let risk_output =
247 crate::display::format_risk_report(risk, crate::display::OutputFormat::Table, true);
248 println!("{}", risk_output);
249 } else {
250 output_report(&report, format)?;
251 }
252
253 if let Some(ref report_path) = args.report {
255 let markdown_report = if args.dossier {
256 risk_assessment
257 .as_ref()
258 .map(|r| crate::cli::address_report::generate_dossier_report(&report, r))
259 .unwrap_or_else(|| crate::cli::address_report::generate_address_report(&report))
260 } else {
261 crate::cli::address_report::generate_address_report(&report)
262 };
263 crate::cli::address_report::save_address_report(&markdown_report, report_path)?;
264 println!("\nReport saved to: {}", report_path.display());
265 }
266
267 Ok(())
268}
269
270pub async fn analyze_address(
273 args: &AddressArgs,
274 client: &dyn ChainClient,
275) -> Result<AddressReport> {
276 let mut chain_balance = client.get_balance(&args.address).await?;
278 client.enrich_balance_usd(&mut chain_balance).await;
279
280 let balance = Balance {
281 raw: chain_balance.raw.clone(),
282 formatted: chain_balance.formatted.clone(),
283 usd: chain_balance.usd_value,
284 };
285
286 let transactions = if args.include_txs {
288 match client.get_transactions(&args.address, args.limit).await {
289 Ok(txs) => Some(
290 txs.into_iter()
291 .map(|tx| TransactionSummary {
292 hash: tx.hash,
293 block_number: tx.block_number.unwrap_or(0),
294 timestamp: tx.timestamp.unwrap_or(0),
295 from: tx.from,
296 to: tx.to,
297 value: tx.value,
298 status: tx.status.unwrap_or(true),
299 })
300 .collect(),
301 ),
302 Err(e) => {
303 tracing::warn!("Failed to fetch transactions: {}", e);
304 Some(vec![])
305 }
306 }
307 } else {
308 None
309 };
310
311 let transaction_count = transactions.as_ref().map(|t| t.len() as u64).unwrap_or(0);
313
314 let tokens = if args.include_tokens {
316 match client.get_token_balances(&args.address).await {
317 Ok(token_bals) => Some(
318 token_bals
319 .into_iter()
320 .map(|tb| TokenBalance {
321 contract_address: tb.token.contract_address,
322 symbol: tb.token.symbol,
323 name: tb.token.name,
324 decimals: tb.token.decimals,
325 balance: tb.balance,
326 formatted_balance: tb.formatted_balance,
327 })
328 .collect(),
329 ),
330 Err(e) => {
331 tracing::warn!("Failed to fetch token balances: {}", e);
332 Some(vec![])
333 }
334 }
335 } else {
336 None
337 };
338
339 Ok(AddressReport {
340 address: args.address.clone(),
341 chain: args.chain.clone(),
342 balance,
343 transaction_count,
344 transactions,
345 tokens,
346 })
347}
348
349fn validate_address(address: &str, chain: &str) -> Result<()> {
351 match chain {
352 "ethereum" | "polygon" | "arbitrum" | "optimism" | "base" | "bsc" | "aegis" => {
354 if !address.starts_with("0x") {
355 return Err(crate::error::ScopeError::InvalidAddress(format!(
356 "Address must start with '0x': {}",
357 address
358 )));
359 }
360 if address.len() != 42 {
361 return Err(crate::error::ScopeError::InvalidAddress(format!(
362 "Address must be 42 characters (0x + 40 hex): {}",
363 address
364 )));
365 }
366 if !address[2..].chars().all(|c| c.is_ascii_hexdigit()) {
368 return Err(crate::error::ScopeError::InvalidAddress(format!(
369 "Address contains invalid hex characters: {}",
370 address
371 )));
372 }
373 }
374 "solana" => {
376 validate_solana_address(address)?;
377 }
378 "tron" => {
380 validate_tron_address(address)?;
381 }
382 _ => {
383 return Err(crate::error::ScopeError::Chain(format!(
384 "Unsupported chain: {}. Supported: ethereum, polygon, arbitrum, optimism, base, bsc, solana, tron",
385 chain
386 )));
387 }
388 }
389 Ok(())
390}
391
392fn output_report(report: &AddressReport, format: OutputFormat) -> Result<()> {
394 match format {
395 OutputFormat::Json => {
396 let json = serde_json::to_string_pretty(report)?;
397 println!("{}", json);
398 }
399 OutputFormat::Csv => {
400 println!("address,chain,balance,transaction_count");
402 println!(
403 "{},{},{},{}",
404 report.address, report.chain, report.balance.formatted, report.transaction_count
405 );
406 }
407 OutputFormat::Table => {
408 println!("Address Analysis Report");
409 println!("=======================");
410 println!("Address: {}", report.address);
411 println!("Chain: {}", report.chain);
412 println!("Balance: {}", report.balance.formatted);
413 if let Some(usd) = report.balance.usd {
414 println!("Value (USD): ${:.2}", usd);
415 }
416 println!("Transactions: {}", report.transaction_count);
417
418 if let Some(ref tokens) = report.tokens
419 && !tokens.is_empty()
420 {
421 println!("\nToken Balances:");
422 for token in tokens {
423 println!(
424 " {} ({}): {}",
425 token.name, token.symbol, token.formatted_balance
426 );
427 }
428 }
429 }
430 OutputFormat::Markdown => {
431 println!(
432 "{}",
433 crate::cli::address_report::generate_address_report(report)
434 );
435 }
436 }
437 Ok(())
438}
439
440#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 fn test_validate_address_valid_ethereum() {
450 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "ethereum");
451 assert!(result.is_ok());
452 }
453
454 #[test]
455 fn test_validate_address_valid_lowercase() {
456 let result = validate_address("0x742d35cc6634c0532925a3b844bc9e7595f1b3c2", "ethereum");
457 assert!(result.is_ok());
458 }
459
460 #[test]
461 fn test_validate_address_valid_polygon() {
462 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "polygon");
463 assert!(result.is_ok());
464 }
465
466 #[test]
467 fn test_validate_address_missing_prefix() {
468 let result = validate_address("742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "ethereum");
469 assert!(result.is_err());
470 assert!(result.unwrap_err().to_string().contains("0x"));
471 }
472
473 #[test]
474 fn test_validate_address_too_short() {
475 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3", "ethereum");
476 assert!(result.is_err());
477 assert!(result.unwrap_err().to_string().contains("42 characters"));
478 }
479
480 #[test]
481 fn test_validate_address_too_long() {
482 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2a", "ethereum");
483 assert!(result.is_err());
484 }
485
486 #[test]
487 fn test_validate_address_invalid_hex() {
488 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1bXYZ", "ethereum");
489 assert!(result.is_err());
490 assert!(result.unwrap_err().to_string().contains("invalid hex"));
491 }
492
493 #[test]
494 fn test_validate_address_unsupported_chain() {
495 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "bitcoin");
496 assert!(result.is_err());
497 assert!(
498 result
499 .unwrap_err()
500 .to_string()
501 .contains("Unsupported chain")
502 );
503 }
504
505 #[test]
506 fn test_validate_address_valid_bsc() {
507 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "bsc");
508 assert!(result.is_ok());
509 }
510
511 #[test]
512 fn test_validate_address_valid_aegis() {
513 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "aegis");
514 assert!(result.is_ok());
515 }
516
517 #[test]
518 fn test_validate_address_valid_solana() {
519 let result = validate_address("DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy", "solana");
520 assert!(result.is_ok());
521 }
522
523 #[test]
524 fn test_validate_address_invalid_solana() {
525 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "solana");
527 assert!(result.is_err());
528 }
529
530 #[test]
531 fn test_validate_address_valid_tron() {
532 let result = validate_address("TDqSquXBgUCLYvYC4XZgrprLK589dkhSCf", "tron");
533 assert!(result.is_ok());
534 }
535
536 #[test]
537 fn test_validate_address_invalid_tron() {
538 let result = validate_address("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2", "tron");
540 assert!(result.is_err());
541 }
542
543 #[test]
544 fn test_address_args_default_values() {
545 use clap::Parser;
546
547 #[derive(Parser)]
548 struct TestCli {
549 #[command(flatten)]
550 args: AddressArgs,
551 }
552
553 let cli = TestCli::try_parse_from(["test", "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"])
554 .unwrap();
555
556 assert_eq!(cli.args.chain, "ethereum");
557 assert_eq!(cli.args.limit, 100);
558 assert!(!cli.args.include_txs);
559 assert!(!cli.args.include_tokens);
560 assert!(cli.args.format.is_none());
561 }
562
563 #[test]
564 fn test_address_args_with_options() {
565 use clap::Parser;
566
567 #[derive(Parser)]
568 struct TestCli {
569 #[command(flatten)]
570 args: AddressArgs,
571 }
572
573 let cli = TestCli::try_parse_from([
574 "test",
575 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
576 "--chain",
577 "polygon",
578 "--include-txs",
579 "--include-tokens",
580 "--limit",
581 "50",
582 "--format",
583 "json",
584 ])
585 .unwrap();
586
587 assert_eq!(cli.args.chain, "polygon");
588 assert_eq!(cli.args.limit, 50);
589 assert!(cli.args.include_txs);
590 assert!(cli.args.include_tokens);
591 assert_eq!(cli.args.format, Some(OutputFormat::Json));
592 }
593
594 #[test]
595 fn test_address_report_serialization() {
596 let report = AddressReport {
597 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
598 chain: "ethereum".to_string(),
599 balance: Balance {
600 raw: "1000000000000000000".to_string(),
601 formatted: "1.0".to_string(),
602 usd: Some(3500.0),
603 },
604 transaction_count: 42,
605 transactions: None,
606 tokens: None,
607 };
608
609 let json = serde_json::to_string(&report).unwrap();
610 assert!(json.contains("0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2"));
611 assert!(json.contains("ethereum"));
612 assert!(json.contains("3500"));
613
614 assert!(!json.contains("transactions"));
616 assert!(!json.contains("tokens"));
617 }
618
619 #[test]
620 fn test_balance_serialization() {
621 let balance = Balance {
622 raw: "1000000000000000000".to_string(),
623 formatted: "1.0 ETH".to_string(),
624 usd: None,
625 };
626
627 let json = serde_json::to_string(&balance).unwrap();
628 assert!(json.contains("1000000000000000000"));
629 assert!(json.contains("1.0 ETH"));
630 assert!(!json.contains("usd")); }
632
633 #[test]
634 fn test_transaction_summary_serialization() {
635 let tx = TransactionSummary {
636 hash: "0xabc123".to_string(),
637 block_number: 12345,
638 timestamp: 1700000000,
639 from: "0xfrom".to_string(),
640 to: Some("0xto".to_string()),
641 value: "1.0".to_string(),
642 status: true,
643 };
644
645 let json = serde_json::to_string(&tx).unwrap();
646 assert!(json.contains("0xabc123"));
647 assert!(json.contains("12345"));
648 assert!(json.contains("true"));
649 }
650
651 #[test]
652 fn test_token_balance_serialization() {
653 let token = TokenBalance {
654 contract_address: "0xtoken".to_string(),
655 symbol: "USDC".to_string(),
656 name: "USD Coin".to_string(),
657 decimals: 6,
658 balance: "1000000".to_string(),
659 formatted_balance: "1.0".to_string(),
660 };
661
662 let json = serde_json::to_string(&token).unwrap();
663 assert!(json.contains("USDC"));
664 assert!(json.contains("USD Coin"));
665 assert!(json.contains("\"decimals\":6"));
666 }
667
668 fn make_test_report() -> AddressReport {
673 AddressReport {
674 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
675 chain: "ethereum".to_string(),
676 balance: Balance {
677 raw: "1000000000000000000".to_string(),
678 formatted: "1.0 ETH".to_string(),
679 usd: Some(3500.0),
680 },
681 transaction_count: 42,
682 transactions: Some(vec![TransactionSummary {
683 hash: "0xabc".to_string(),
684 block_number: 12345,
685 timestamp: 1700000000,
686 from: "0xfrom".to_string(),
687 to: Some("0xto".to_string()),
688 value: "1.0".to_string(),
689 status: true,
690 }]),
691 tokens: Some(vec![TokenBalance {
692 contract_address: "0xusdc".to_string(),
693 symbol: "USDC".to_string(),
694 name: "USD Coin".to_string(),
695 decimals: 6,
696 balance: "1000000".to_string(),
697 formatted_balance: "1.0".to_string(),
698 }]),
699 }
700 }
701
702 #[test]
703 fn test_output_report_json() {
704 let report = make_test_report();
705 let result = output_report(&report, OutputFormat::Json);
706 assert!(result.is_ok());
707 }
708
709 #[test]
710 fn test_output_report_csv() {
711 let report = make_test_report();
712 let result = output_report(&report, OutputFormat::Csv);
713 assert!(result.is_ok());
714 }
715
716 #[test]
717 fn test_output_report_table() {
718 let report = make_test_report();
719 let result = output_report(&report, OutputFormat::Table);
720 assert!(result.is_ok());
721 }
722
723 #[test]
724 fn test_output_report_table_no_usd() {
725 let mut report = make_test_report();
726 report.balance.usd = None;
727 let result = output_report(&report, OutputFormat::Table);
728 assert!(result.is_ok());
729 }
730
731 #[test]
732 fn test_output_report_table_no_tokens() {
733 let mut report = make_test_report();
734 report.tokens = None;
735 let result = output_report(&report, OutputFormat::Table);
736 assert!(result.is_ok());
737 }
738
739 #[test]
740 fn test_output_report_table_empty_tokens() {
741 let mut report = make_test_report();
742 report.tokens = Some(vec![]);
743 let result = output_report(&report, OutputFormat::Table);
744 assert!(result.is_ok());
745 }
746
747 use crate::chains::{
752 Balance as ChainBalance, ChainClient, Token as ChainToken,
753 TokenBalance as ChainTokenBalance, Transaction as ChainTransaction,
754 };
755 use async_trait::async_trait;
756
757 struct MockClient;
758
759 #[async_trait]
760 impl ChainClient for MockClient {
761 fn chain_name(&self) -> &str {
762 "ethereum"
763 }
764 fn native_token_symbol(&self) -> &str {
765 "ETH"
766 }
767 async fn get_balance(&self, _addr: &str) -> crate::error::Result<ChainBalance> {
768 Ok(ChainBalance {
769 raw: "1000000000000000000".into(),
770 formatted: "1.0 ETH".into(),
771 decimals: 18,
772 symbol: "ETH".into(),
773 usd_value: Some(2500.0),
774 })
775 }
776 async fn enrich_balance_usd(&self, b: &mut ChainBalance) {
777 b.usd_value = Some(2500.0);
778 }
779 async fn get_transaction(&self, _h: &str) -> crate::error::Result<ChainTransaction> {
780 Err(crate::error::ScopeError::NotFound("mock".into()))
781 }
782 async fn get_transactions(
783 &self,
784 _addr: &str,
785 _lim: u32,
786 ) -> crate::error::Result<Vec<ChainTransaction>> {
787 Ok(vec![ChainTransaction {
788 hash: "0x1234".into(),
789 block_number: Some(100),
790 timestamp: Some(1700000000),
791 from: "0xfrom".into(),
792 to: Some("0xto".into()),
793 value: "1000000000000000000".into(),
794 gas_limit: 21000,
795 gas_used: Some(21000),
796 gas_price: "20000000000".into(),
797 nonce: 1,
798 input: "0x".into(),
799 status: Some(true),
800 }])
801 }
802 async fn get_block_number(&self) -> crate::error::Result<u64> {
803 Ok(12345678)
804 }
805 async fn get_token_balances(
806 &self,
807 _addr: &str,
808 ) -> crate::error::Result<Vec<ChainTokenBalance>> {
809 Ok(vec![ChainTokenBalance {
810 token: ChainToken {
811 contract_address: "0xtoken".into(),
812 symbol: "USDC".into(),
813 name: "USD Coin".into(),
814 decimals: 6,
815 },
816 balance: "1000000".into(),
817 formatted_balance: "1.0".into(),
818 usd_value: Some(1.0),
819 }])
820 }
821 async fn get_code(&self, _addr: &str) -> crate::error::Result<String> {
822 Ok("0x".into())
823 }
824 }
825
826 #[tokio::test]
827 async fn test_analyze_address_with_mock() {
828 let args = AddressArgs {
829 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
830 chain: "ethereum".to_string(),
831 format: None,
832 include_txs: true,
833 include_tokens: true,
834 limit: 10,
835 report: None,
836 dossier: false,
837 };
838 let client = MockClient;
839 let result = analyze_address(&args, &client).await;
840 assert!(result.is_ok());
841 let report = result.unwrap();
842 assert_eq!(report.address, "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2");
843 assert_eq!(report.chain, "ethereum");
844 assert!(report.tokens.is_some());
845 assert!(report.transactions.is_some());
846 }
847
848 #[tokio::test]
849 async fn test_analyze_address_no_txs_no_tokens() {
850 let args = AddressArgs {
851 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
852 chain: "ethereum".to_string(),
853 format: None,
854 include_txs: false,
855 include_tokens: false,
856 limit: 10,
857 report: None,
858 dossier: false,
859 };
860 let client = MockClient;
861 let result = analyze_address(&args, &client).await;
862 assert!(result.is_ok());
863 }
864
865 use crate::chains::mocks::{MockChainClient, MockClientFactory};
870
871 fn mock_factory() -> MockClientFactory {
872 MockClientFactory::new()
873 }
874
875 #[tokio::test]
876 async fn test_run_ethereum_address() {
877 let config = Config::default();
878 let factory = mock_factory();
879 let args = AddressArgs {
880 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
881 chain: "ethereum".to_string(),
882 format: Some(OutputFormat::Json),
883 include_txs: false,
884 include_tokens: false,
885 limit: 10,
886 report: None,
887 dossier: false,
888 };
889 let result = super::run(args, &config, &factory).await;
890 assert!(result.is_ok());
891 }
892
893 #[tokio::test]
894 async fn test_run_with_transactions() {
895 let config = Config::default();
896 let mut factory = mock_factory();
897 factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
898 let args = AddressArgs {
899 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
900 chain: "ethereum".to_string(),
901 format: Some(OutputFormat::Json),
902 include_txs: true,
903 include_tokens: false,
904 limit: 10,
905 report: None,
906 dossier: false,
907 };
908 let result = super::run(args, &config, &factory).await;
909 assert!(result.is_ok());
910 }
911
912 #[tokio::test]
913 async fn test_run_with_tokens() {
914 let config = Config::default();
915 let mut factory = mock_factory();
916 factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
917 token: crate::chains::Token {
918 contract_address: "0xusdc".to_string(),
919 symbol: "USDC".to_string(),
920 name: "USD Coin".to_string(),
921 decimals: 6,
922 },
923 balance: "1000000".to_string(),
924 formatted_balance: "1.0".to_string(),
925 usd_value: Some(1.0),
926 }];
927 let args = AddressArgs {
928 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
929 chain: "ethereum".to_string(),
930 format: Some(OutputFormat::Table),
931 include_txs: false,
932 include_tokens: true,
933 limit: 10,
934 report: None,
935 dossier: false,
936 };
937 let result = super::run(args, &config, &factory).await;
938 assert!(result.is_ok());
939 }
940
941 #[tokio::test]
942 async fn test_run_auto_detect_solana() {
943 let config = Config::default();
944 let mut factory = mock_factory();
945 factory.mock_client = MockChainClient::new("solana", "SOL");
946 let args = AddressArgs {
947 address: "DRpbCBMxVnDK7maPM5tGv6MvB3v1sRMC86PZ8okm21hy".to_string(),
949 chain: "ethereum".to_string(), format: Some(OutputFormat::Json),
951 include_txs: false,
952 include_tokens: false,
953 limit: 10,
954 report: None,
955 dossier: false,
956 };
957 let result = super::run(args, &config, &factory).await;
958 assert!(result.is_ok());
959 }
960
961 #[tokio::test]
962 async fn test_run_csv_format() {
963 let config = Config::default();
964 let factory = mock_factory();
965 let args = AddressArgs {
966 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
967 chain: "ethereum".to_string(),
968 format: Some(OutputFormat::Csv),
969 include_txs: false,
970 include_tokens: false,
971 limit: 10,
972 report: None,
973 dossier: false,
974 };
975 let result = super::run(args, &config, &factory).await;
976 assert!(result.is_ok());
977 }
978
979 #[tokio::test]
980 async fn test_run_all_features() {
981 let config = Config::default();
982 let mut factory = mock_factory();
983 factory.mock_client.transactions = vec![factory.mock_client.transaction.clone()];
984 factory.mock_client.token_balances = vec![crate::chains::TokenBalance {
985 token: crate::chains::Token {
986 contract_address: "0xtoken".to_string(),
987 symbol: "TEST".to_string(),
988 name: "Test Token".to_string(),
989 decimals: 18,
990 },
991 balance: "1000000000000000000".to_string(),
992 formatted_balance: "1.0".to_string(),
993 usd_value: None,
994 }];
995 let args = AddressArgs {
996 address: "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2".to_string(),
997 chain: "ethereum".to_string(),
998 format: Some(OutputFormat::Table),
999 include_txs: true,
1000 include_tokens: true,
1001 limit: 50,
1002 report: None,
1003 dossier: false,
1004 };
1005 let result = super::run(args, &config, &factory).await;
1006 assert!(result.is_ok());
1007 }
1008}