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