1pub mod address;
37pub mod address_report;
38pub mod compliance;
39pub mod crawl;
40pub mod discover;
41pub mod export;
42pub mod interactive;
43pub mod market;
44pub mod monitor;
45pub mod portfolio;
46pub mod report;
47pub mod setup;
48pub mod token_health;
49pub mod tx;
50
51use clap::{Parser, Subcommand};
52use std::path::PathBuf;
53
54pub use address::AddressArgs;
55pub use crawl::CrawlArgs;
56pub use export::ExportArgs;
57pub use interactive::InteractiveArgs;
58pub use monitor::MonitorArgs;
59pub use portfolio::PortfolioArgs;
60pub use setup::SetupArgs;
61pub use tx::TxArgs;
62
63#[derive(Debug, Parser)]
69#[command(
70 name = "scope",
71 version,
72 about = "Scope Blockchain Analysis - A tool for blockchain data analysis",
73 long_about = "Scope Blockchain Analysis is a production-grade tool for \
74 blockchain data analysis, portfolio tracking, and transaction investigation.\n\n\
75 Use --help with any subcommand for detailed usage information."
76)]
77pub struct Cli {
78 #[command(subcommand)]
80 pub command: Commands,
81
82 #[arg(long, global = true, value_name = "PATH")]
86 pub config: Option<PathBuf>,
87
88 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
95 pub verbose: u8,
96
97 #[arg(long, global = true)]
99 pub no_color: bool,
100
101 #[arg(long, global = true)]
106 pub ai: bool,
107}
108
109#[derive(Debug, Subcommand)]
111pub enum Commands {
112 #[command(visible_alias = "addr")]
117 Address(AddressArgs),
118
119 #[command(visible_alias = "transaction")]
124 Tx(TxArgs),
125
126 #[command(visible_alias = "token")]
132 Crawl(CrawlArgs),
133
134 #[command(visible_alias = "port")]
139 Portfolio(PortfolioArgs),
140
141 Export(ExportArgs),
146
147 #[command(visible_alias = "shell")]
152 Interactive(InteractiveArgs),
153
154 #[command(visible_alias = "mon")]
159 Monitor(MonitorArgs),
160
161 #[command(visible_alias = "config")]
166 Setup(SetupArgs),
167
168 #[command(subcommand)]
173 Compliance(compliance::ComplianceCommands),
174
175 #[command(subcommand)]
180 Market(market::MarketCommands),
181
182 #[command(visible_alias = "health")]
187 TokenHealth(token_health::TokenHealthArgs),
188
189 #[command(subcommand)]
191 Report(report::ReportCommands),
192
193 #[command(visible_alias = "disc")]
198 Discover(discover::DiscoverArgs),
199}
200
201impl Cli {
202 pub fn parse_args() -> Self {
206 Self::parse()
207 }
208
209 pub fn log_level(&self) -> tracing::Level {
217 match self.verbose {
218 0 => tracing::Level::WARN,
219 1 => tracing::Level::INFO,
220 2 => tracing::Level::DEBUG,
221 _ => tracing::Level::TRACE,
222 }
223 }
224}
225
226#[cfg(test)]
231mod tests {
232 use super::*;
233 use clap::Parser;
234
235 #[test]
236 fn test_cli_parse_address_command() {
237 let cli = Cli::try_parse_from([
238 "scope",
239 "address",
240 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
241 ])
242 .unwrap();
243
244 assert!(matches!(cli.command, Commands::Address(_)));
245 assert!(cli.config.is_none());
246 assert_eq!(cli.verbose, 0);
247 }
248
249 #[test]
250 fn test_cli_parse_address_alias() {
251 let cli = Cli::try_parse_from([
252 "scope",
253 "addr",
254 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
255 ])
256 .unwrap();
257
258 assert!(matches!(cli.command, Commands::Address(_)));
259 }
260
261 #[test]
262 fn test_cli_parse_tx_command() {
263 let cli = Cli::try_parse_from([
264 "scope",
265 "tx",
266 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
267 ])
268 .unwrap();
269
270 assert!(matches!(cli.command, Commands::Tx(_)));
271 }
272
273 #[test]
274 fn test_cli_parse_tx_alias() {
275 let cli = Cli::try_parse_from([
276 "scope",
277 "transaction",
278 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
279 ])
280 .unwrap();
281
282 assert!(matches!(cli.command, Commands::Tx(_)));
283 }
284
285 #[test]
286 fn test_cli_parse_portfolio_command() {
287 let cli = Cli::try_parse_from(["scope", "portfolio", "list"]).unwrap();
288
289 assert!(matches!(cli.command, Commands::Portfolio(_)));
290 }
291
292 #[test]
293 fn test_cli_parse_export_command() {
294 let cli = Cli::try_parse_from([
295 "scope",
296 "export",
297 "--address",
298 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
299 "--output",
300 "data.json",
301 ])
302 .unwrap();
303
304 assert!(matches!(cli.command, Commands::Export(_)));
305 }
306
307 #[test]
308 fn test_cli_parse_interactive_command() {
309 let cli = Cli::try_parse_from(["scope", "interactive"]).unwrap();
310
311 assert!(matches!(cli.command, Commands::Interactive(_)));
312 }
313
314 #[test]
315 fn test_cli_parse_interactive_alias() {
316 let cli = Cli::try_parse_from(["scope", "shell"]).unwrap();
317
318 assert!(matches!(cli.command, Commands::Interactive(_)));
319 }
320
321 #[test]
322 fn test_cli_parse_interactive_no_banner() {
323 let cli = Cli::try_parse_from(["scope", "interactive", "--no-banner"]).unwrap();
324
325 if let Commands::Interactive(args) = cli.command {
326 assert!(args.no_banner);
327 } else {
328 panic!("Expected Interactive command");
329 }
330 }
331
332 #[test]
333 fn test_cli_verbose_flag_counting() {
334 let cli = Cli::try_parse_from([
335 "scope",
336 "-vvv",
337 "address",
338 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
339 ])
340 .unwrap();
341
342 assert_eq!(cli.verbose, 3);
343 }
344
345 #[test]
346 fn test_cli_verbose_separate_flags() {
347 let cli = Cli::try_parse_from([
348 "scope",
349 "-v",
350 "-v",
351 "address",
352 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
353 ])
354 .unwrap();
355
356 assert_eq!(cli.verbose, 2);
357 }
358
359 #[test]
360 fn test_cli_global_config_option() {
361 let cli = Cli::try_parse_from([
362 "scope",
363 "--config",
364 "/custom/path.yaml",
365 "tx",
366 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
367 ])
368 .unwrap();
369
370 assert_eq!(cli.config, Some(PathBuf::from("/custom/path.yaml")));
371 }
372
373 #[test]
374 fn test_cli_config_long_flag() {
375 let cli = Cli::try_parse_from([
376 "scope",
377 "--config",
378 "/custom/config.yaml",
379 "address",
380 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
381 ])
382 .unwrap();
383
384 assert_eq!(cli.config, Some(PathBuf::from("/custom/config.yaml")));
385 }
386
387 #[test]
388 fn test_cli_no_color_flag() {
389 let cli = Cli::try_parse_from([
390 "scope",
391 "--no-color",
392 "address",
393 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
394 ])
395 .unwrap();
396
397 assert!(cli.no_color);
398 }
399
400 #[test]
401 fn test_cli_missing_required_args_fails() {
402 let result = Cli::try_parse_from(["scope", "address"]);
403 assert!(result.is_err());
404 }
405
406 #[test]
407 fn test_cli_invalid_subcommand_fails() {
408 let result = Cli::try_parse_from(["scope", "invalid"]);
409 assert!(result.is_err());
410 }
411
412 #[test]
413 fn test_cli_log_level_default() {
414 let cli = Cli::try_parse_from([
415 "scope",
416 "address",
417 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
418 ])
419 .unwrap();
420
421 assert_eq!(cli.log_level(), tracing::Level::WARN);
422 }
423
424 #[test]
425 fn test_cli_log_level_info() {
426 let cli = Cli::try_parse_from([
427 "scope",
428 "-v",
429 "address",
430 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
431 ])
432 .unwrap();
433
434 assert_eq!(cli.log_level(), tracing::Level::INFO);
435 }
436
437 #[test]
438 fn test_cli_log_level_debug() {
439 let cli = Cli::try_parse_from([
440 "scope",
441 "-vv",
442 "address",
443 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
444 ])
445 .unwrap();
446
447 assert_eq!(cli.log_level(), tracing::Level::DEBUG);
448 }
449
450 #[test]
451 fn test_cli_log_level_trace() {
452 let cli = Cli::try_parse_from([
453 "scope",
454 "-vvvv",
455 "address",
456 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
457 ])
458 .unwrap();
459
460 assert_eq!(cli.log_level(), tracing::Level::TRACE);
461 }
462
463 #[test]
464 fn test_cli_debug_impl() {
465 let cli = Cli::try_parse_from([
466 "scope",
467 "address",
468 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
469 ])
470 .unwrap();
471
472 let debug_str = format!("{:?}", cli);
473 assert!(debug_str.contains("Cli"));
474 assert!(debug_str.contains("Address"));
475 }
476
477 #[test]
482 fn test_cli_parse_monitor_command() {
483 let cli = Cli::try_parse_from([
484 "scope",
485 "monitor",
486 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
487 ])
488 .unwrap();
489
490 assert!(matches!(cli.command, Commands::Monitor(_)));
491 }
492
493 #[test]
494 fn test_cli_parse_monitor_alias_mon() {
495 let cli = Cli::try_parse_from(["scope", "mon", "USDC"]).unwrap();
496
497 assert!(matches!(cli.command, Commands::Monitor(_)));
498 if let Commands::Monitor(args) = cli.command {
499 assert_eq!(args.token, "USDC");
500 assert_eq!(args.chain, "ethereum"); assert!(args.layout.is_none());
502 assert!(args.refresh.is_none());
503 assert!(args.scale.is_none());
504 assert!(args.color_scheme.is_none());
505 assert!(args.export.is_none());
506 }
507 }
508
509 #[test]
510 fn test_cli_parse_monitor_with_chain() {
511 let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--chain", "solana"]).unwrap();
512
513 if let Commands::Monitor(args) = cli.command {
514 assert_eq!(args.token, "USDC");
515 assert_eq!(args.chain, "solana");
516 } else {
517 panic!("Expected Monitor command");
518 }
519 }
520
521 #[test]
522 fn test_cli_parse_monitor_chain_short_flag() {
523 let cli = Cli::try_parse_from(["scope", "monitor", "PEPE", "-c", "ethereum"]).unwrap();
524
525 if let Commands::Monitor(args) = cli.command {
526 assert_eq!(args.token, "PEPE");
527 assert_eq!(args.chain, "ethereum");
528 } else {
529 panic!("Expected Monitor command");
530 }
531 }
532
533 #[test]
534 fn test_cli_parse_monitor_with_layout() {
535 let cli =
536 Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "chart-focus"]).unwrap();
537
538 if let Commands::Monitor(args) = cli.command {
539 assert_eq!(args.layout, Some(monitor::LayoutPreset::ChartFocus));
540 } else {
541 panic!("Expected Monitor command");
542 }
543 }
544
545 #[test]
546 fn test_cli_parse_monitor_with_refresh() {
547 let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--refresh", "3"]).unwrap();
548
549 if let Commands::Monitor(args) = cli.command {
550 assert_eq!(args.refresh, Some(3));
551 } else {
552 panic!("Expected Monitor command");
553 }
554 }
555
556 #[test]
557 fn test_cli_parse_monitor_with_scale() {
558 let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "log"]).unwrap();
559
560 if let Commands::Monitor(args) = cli.command {
561 assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
562 } else {
563 panic!("Expected Monitor command");
564 }
565 }
566
567 #[test]
568 fn test_cli_parse_monitor_with_color_scheme() {
569 let cli =
570 Cli::try_parse_from(["scope", "monitor", "USDC", "--color-scheme", "blue-orange"])
571 .unwrap();
572
573 if let Commands::Monitor(args) = cli.command {
574 assert_eq!(args.color_scheme, Some(monitor::ColorScheme::BlueOrange));
575 } else {
576 panic!("Expected Monitor command");
577 }
578 }
579
580 #[test]
581 fn test_cli_parse_monitor_with_export() {
582 let cli =
583 Cli::try_parse_from(["scope", "monitor", "USDC", "--export", "/tmp/out.csv"]).unwrap();
584
585 if let Commands::Monitor(args) = cli.command {
586 assert_eq!(args.export, Some(std::path::PathBuf::from("/tmp/out.csv")));
587 } else {
588 panic!("Expected Monitor command");
589 }
590 }
591
592 #[test]
593 fn test_cli_parse_monitor_short_flags() {
594 let cli = Cli::try_parse_from([
595 "scope", "mon", "USDC", "-c", "solana", "-l", "compact", "-r", "10", "-s", "log", "-e",
596 "data.csv",
597 ])
598 .unwrap();
599
600 if let Commands::Monitor(args) = cli.command {
601 assert_eq!(args.token, "USDC");
602 assert_eq!(args.chain, "solana");
603 assert_eq!(args.layout, Some(monitor::LayoutPreset::Compact));
604 assert_eq!(args.refresh, Some(10));
605 assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
606 assert_eq!(args.export, Some(std::path::PathBuf::from("data.csv")));
607 } else {
608 panic!("Expected Monitor command");
609 }
610 }
611
612 #[test]
613 fn test_cli_parse_monitor_missing_token_fails() {
614 let result = Cli::try_parse_from(["scope", "monitor"]);
615 assert!(result.is_err());
616 }
617
618 #[test]
619 fn test_cli_parse_monitor_invalid_layout_fails() {
620 let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "invalid"]);
621 assert!(result.is_err());
622 }
623
624 #[test]
625 fn test_cli_parse_monitor_invalid_scale_fails() {
626 let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "quadratic"]);
627 assert!(result.is_err());
628 }
629
630 #[test]
631 fn test_cli_parse_market_summary() {
632 let cli = Cli::try_parse_from(["scope", "market", "summary"]).unwrap();
633 assert!(matches!(cli.command, Commands::Market(_)));
634 }
635
636 #[test]
637 fn test_cli_parse_market_summary_with_pair() {
638 let cli = Cli::try_parse_from(["scope", "market", "summary", "PUSD_USDT"]).unwrap();
639 if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
640 assert_eq!(args.pair, "PUSD_USDT");
641 } else {
642 panic!("Expected Market Summary command");
643 }
644 }
645
646 #[test]
647 fn test_cli_parse_report_batch() {
648 let cli = Cli::try_parse_from([
649 "scope",
650 "report",
651 "batch",
652 "--addresses",
653 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
654 "--output",
655 "report.md",
656 ])
657 .unwrap();
658 assert!(matches!(cli.command, Commands::Report(_)));
659 }
660
661 #[test]
662 fn test_cli_parse_market_summary_with_thresholds() {
663 let cli = Cli::try_parse_from([
664 "scope",
665 "market",
666 "summary",
667 "--peg-range",
668 "0.002",
669 "--min-bid-ask-ratio",
670 "0.1",
671 "--max-bid-ask-ratio",
672 "10",
673 ])
674 .unwrap();
675 if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
676 assert_eq!(args.peg_range, 0.002);
677 assert_eq!(args.min_bid_ask_ratio, 0.1);
678 assert_eq!(args.max_bid_ask_ratio, 10.0);
679 } else {
680 panic!("Expected Market Summary command");
681 }
682 }
683
684 #[test]
685 fn test_cli_parse_token_health() {
686 let cli = Cli::try_parse_from(["scope", "token-health", "USDC"]).unwrap();
687 if let Commands::TokenHealth(args) = cli.command {
688 assert_eq!(args.token, "USDC");
689 assert!(!args.with_market);
690 } else {
691 panic!("Expected TokenHealth command");
692 }
693 }
694
695 #[test]
696 fn test_cli_parse_token_health_alias() {
697 let cli = Cli::try_parse_from(["scope", "health", "USDC", "--with-market"]).unwrap();
698 if let Commands::TokenHealth(args) = cli.command {
699 assert_eq!(args.token, "USDC");
700 assert!(args.with_market);
701 assert!(matches!(
702 args.market_venue,
703 crate::market::MarketVenue::Binance
704 ));
705 } else {
706 panic!("Expected TokenHealth command");
707 }
708 }
709
710 #[test]
711 fn test_cli_parse_token_health_market_venue_biconomy() {
712 let cli = Cli::try_parse_from([
713 "scope",
714 "token-health",
715 "USDC",
716 "--with-market",
717 "--market-venue",
718 "biconomy",
719 ])
720 .unwrap();
721 if let Commands::TokenHealth(args) = cli.command {
722 assert!(matches!(
723 args.market_venue,
724 crate::market::MarketVenue::Biconomy
725 ));
726 } else {
727 panic!("Expected TokenHealth command");
728 }
729 }
730
731 #[test]
732 fn test_cli_parse_token_health_market_venue_eth() {
733 let cli = Cli::try_parse_from([
734 "scope",
735 "token-health",
736 "USDC",
737 "--with-market",
738 "--market-venue",
739 "eth",
740 ])
741 .unwrap();
742 if let Commands::TokenHealth(args) = cli.command {
743 assert!(matches!(
744 args.market_venue,
745 crate::market::MarketVenue::Ethereum
746 ));
747 } else {
748 panic!("Expected TokenHealth command");
749 }
750 }
751
752 #[test]
753 fn test_cli_parse_token_health_market_venue_solana() {
754 let cli = Cli::try_parse_from([
755 "scope",
756 "token-health",
757 "USDC",
758 "--with-market",
759 "--market-venue",
760 "solana",
761 ])
762 .unwrap();
763 if let Commands::TokenHealth(args) = cli.command {
764 assert!(matches!(
765 args.market_venue,
766 crate::market::MarketVenue::Solana
767 ));
768 } else {
769 panic!("Expected TokenHealth command");
770 }
771 }
772
773 #[test]
774 fn test_cli_parse_ai_flag() {
775 let cli = Cli::try_parse_from([
776 "scope",
777 "--ai",
778 "address",
779 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
780 ])
781 .unwrap();
782 assert!(cli.ai);
783 }
784}