1pub mod address;
31pub mod compliance;
32pub mod crawl;
33pub mod export;
34pub mod interactive;
35pub mod monitor;
36pub mod portfolio;
37pub mod setup;
38pub mod tx;
39
40use clap::{Parser, Subcommand};
41use std::path::PathBuf;
42
43pub use address::AddressArgs;
44pub use crawl::CrawlArgs;
45pub use export::ExportArgs;
46pub use interactive::InteractiveArgs;
47pub use monitor::MonitorArgs;
48pub use portfolio::PortfolioArgs;
49pub use setup::SetupArgs;
50pub use tx::TxArgs;
51
52#[derive(Debug, Parser)]
58#[command(
59 name = "scope",
60 version,
61 about = "Scope Blockchain Analysis - A tool for blockchain data analysis",
62 long_about = "Scope Blockchain Analysis is a production-grade tool for \
63 blockchain data analysis, portfolio tracking, and transaction investigation.\n\n\
64 Use --help with any subcommand for detailed usage information."
65)]
66pub struct Cli {
67 #[command(subcommand)]
69 pub command: Commands,
70
71 #[arg(long, global = true, value_name = "PATH")]
75 pub config: Option<PathBuf>,
76
77 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
84 pub verbose: u8,
85
86 #[arg(long, global = true)]
88 pub no_color: bool,
89}
90
91#[derive(Debug, Subcommand)]
93pub enum Commands {
94 #[command(visible_alias = "addr")]
99 Address(AddressArgs),
100
101 #[command(visible_alias = "transaction")]
106 Tx(TxArgs),
107
108 #[command(visible_alias = "token")]
114 Crawl(CrawlArgs),
115
116 #[command(visible_alias = "port")]
121 Portfolio(PortfolioArgs),
122
123 Export(ExportArgs),
128
129 #[command(visible_alias = "shell")]
134 Interactive(InteractiveArgs),
135
136 #[command(visible_alias = "mon")]
141 Monitor(MonitorArgs),
142
143 #[command(visible_alias = "config")]
148 Setup(SetupArgs),
149
150 #[command(subcommand)]
155 Compliance(compliance::ComplianceCommands),
156}
157
158impl Cli {
159 pub fn parse_args() -> Self {
163 Self::parse()
164 }
165
166 pub fn log_level(&self) -> tracing::Level {
174 match self.verbose {
175 0 => tracing::Level::WARN,
176 1 => tracing::Level::INFO,
177 2 => tracing::Level::DEBUG,
178 _ => tracing::Level::TRACE,
179 }
180 }
181}
182
183#[cfg(test)]
188mod tests {
189 use super::*;
190 use clap::Parser;
191
192 #[test]
193 fn test_cli_parse_address_command() {
194 let cli = Cli::try_parse_from([
195 "scope",
196 "address",
197 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
198 ])
199 .unwrap();
200
201 assert!(matches!(cli.command, Commands::Address(_)));
202 assert!(cli.config.is_none());
203 assert_eq!(cli.verbose, 0);
204 }
205
206 #[test]
207 fn test_cli_parse_address_alias() {
208 let cli = Cli::try_parse_from([
209 "scope",
210 "addr",
211 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
212 ])
213 .unwrap();
214
215 assert!(matches!(cli.command, Commands::Address(_)));
216 }
217
218 #[test]
219 fn test_cli_parse_tx_command() {
220 let cli = Cli::try_parse_from([
221 "scope",
222 "tx",
223 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
224 ])
225 .unwrap();
226
227 assert!(matches!(cli.command, Commands::Tx(_)));
228 }
229
230 #[test]
231 fn test_cli_parse_tx_alias() {
232 let cli = Cli::try_parse_from([
233 "scope",
234 "transaction",
235 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
236 ])
237 .unwrap();
238
239 assert!(matches!(cli.command, Commands::Tx(_)));
240 }
241
242 #[test]
243 fn test_cli_parse_portfolio_command() {
244 let cli = Cli::try_parse_from(["scope", "portfolio", "list"]).unwrap();
245
246 assert!(matches!(cli.command, Commands::Portfolio(_)));
247 }
248
249 #[test]
250 fn test_cli_parse_export_command() {
251 let cli = Cli::try_parse_from([
252 "scope",
253 "export",
254 "--address",
255 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
256 "--output",
257 "data.json",
258 ])
259 .unwrap();
260
261 assert!(matches!(cli.command, Commands::Export(_)));
262 }
263
264 #[test]
265 fn test_cli_parse_interactive_command() {
266 let cli = Cli::try_parse_from(["scope", "interactive"]).unwrap();
267
268 assert!(matches!(cli.command, Commands::Interactive(_)));
269 }
270
271 #[test]
272 fn test_cli_parse_interactive_alias() {
273 let cli = Cli::try_parse_from(["scope", "shell"]).unwrap();
274
275 assert!(matches!(cli.command, Commands::Interactive(_)));
276 }
277
278 #[test]
279 fn test_cli_parse_interactive_no_banner() {
280 let cli = Cli::try_parse_from(["scope", "interactive", "--no-banner"]).unwrap();
281
282 if let Commands::Interactive(args) = cli.command {
283 assert!(args.no_banner);
284 } else {
285 panic!("Expected Interactive command");
286 }
287 }
288
289 #[test]
290 fn test_cli_verbose_flag_counting() {
291 let cli = Cli::try_parse_from([
292 "scope",
293 "-vvv",
294 "address",
295 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
296 ])
297 .unwrap();
298
299 assert_eq!(cli.verbose, 3);
300 }
301
302 #[test]
303 fn test_cli_verbose_separate_flags() {
304 let cli = Cli::try_parse_from([
305 "scope",
306 "-v",
307 "-v",
308 "address",
309 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
310 ])
311 .unwrap();
312
313 assert_eq!(cli.verbose, 2);
314 }
315
316 #[test]
317 fn test_cli_global_config_option() {
318 let cli = Cli::try_parse_from([
319 "scope",
320 "--config",
321 "/custom/path.yaml",
322 "tx",
323 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
324 ])
325 .unwrap();
326
327 assert_eq!(cli.config, Some(PathBuf::from("/custom/path.yaml")));
328 }
329
330 #[test]
331 fn test_cli_config_long_flag() {
332 let cli = Cli::try_parse_from([
333 "scope",
334 "--config",
335 "/custom/config.yaml",
336 "address",
337 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
338 ])
339 .unwrap();
340
341 assert_eq!(cli.config, Some(PathBuf::from("/custom/config.yaml")));
342 }
343
344 #[test]
345 fn test_cli_no_color_flag() {
346 let cli = Cli::try_parse_from([
347 "scope",
348 "--no-color",
349 "address",
350 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
351 ])
352 .unwrap();
353
354 assert!(cli.no_color);
355 }
356
357 #[test]
358 fn test_cli_missing_required_args_fails() {
359 let result = Cli::try_parse_from(["scope", "address"]);
360 assert!(result.is_err());
361 }
362
363 #[test]
364 fn test_cli_invalid_subcommand_fails() {
365 let result = Cli::try_parse_from(["scope", "invalid"]);
366 assert!(result.is_err());
367 }
368
369 #[test]
370 fn test_cli_log_level_default() {
371 let cli = Cli::try_parse_from([
372 "scope",
373 "address",
374 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
375 ])
376 .unwrap();
377
378 assert_eq!(cli.log_level(), tracing::Level::WARN);
379 }
380
381 #[test]
382 fn test_cli_log_level_info() {
383 let cli = Cli::try_parse_from([
384 "scope",
385 "-v",
386 "address",
387 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
388 ])
389 .unwrap();
390
391 assert_eq!(cli.log_level(), tracing::Level::INFO);
392 }
393
394 #[test]
395 fn test_cli_log_level_debug() {
396 let cli = Cli::try_parse_from([
397 "scope",
398 "-vv",
399 "address",
400 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
401 ])
402 .unwrap();
403
404 assert_eq!(cli.log_level(), tracing::Level::DEBUG);
405 }
406
407 #[test]
408 fn test_cli_log_level_trace() {
409 let cli = Cli::try_parse_from([
410 "scope",
411 "-vvvv",
412 "address",
413 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
414 ])
415 .unwrap();
416
417 assert_eq!(cli.log_level(), tracing::Level::TRACE);
418 }
419
420 #[test]
421 fn test_cli_debug_impl() {
422 let cli = Cli::try_parse_from([
423 "scope",
424 "address",
425 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
426 ])
427 .unwrap();
428
429 let debug_str = format!("{:?}", cli);
430 assert!(debug_str.contains("Cli"));
431 assert!(debug_str.contains("Address"));
432 }
433
434 #[test]
439 fn test_cli_parse_monitor_command() {
440 let cli = Cli::try_parse_from([
441 "scope",
442 "monitor",
443 "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
444 ])
445 .unwrap();
446
447 assert!(matches!(cli.command, Commands::Monitor(_)));
448 }
449
450 #[test]
451 fn test_cli_parse_monitor_alias_mon() {
452 let cli = Cli::try_parse_from(["scope", "mon", "USDC"]).unwrap();
453
454 assert!(matches!(cli.command, Commands::Monitor(_)));
455 if let Commands::Monitor(args) = cli.command {
456 assert_eq!(args.token, "USDC");
457 assert_eq!(args.chain, "ethereum"); assert!(args.layout.is_none());
459 assert!(args.refresh.is_none());
460 assert!(args.scale.is_none());
461 assert!(args.color_scheme.is_none());
462 assert!(args.export.is_none());
463 }
464 }
465
466 #[test]
467 fn test_cli_parse_monitor_with_chain() {
468 let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--chain", "solana"]).unwrap();
469
470 if let Commands::Monitor(args) = cli.command {
471 assert_eq!(args.token, "USDC");
472 assert_eq!(args.chain, "solana");
473 } else {
474 panic!("Expected Monitor command");
475 }
476 }
477
478 #[test]
479 fn test_cli_parse_monitor_chain_short_flag() {
480 let cli = Cli::try_parse_from(["scope", "monitor", "PEPE", "-c", "ethereum"]).unwrap();
481
482 if let Commands::Monitor(args) = cli.command {
483 assert_eq!(args.token, "PEPE");
484 assert_eq!(args.chain, "ethereum");
485 } else {
486 panic!("Expected Monitor command");
487 }
488 }
489
490 #[test]
491 fn test_cli_parse_monitor_with_layout() {
492 let cli =
493 Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "chart-focus"]).unwrap();
494
495 if let Commands::Monitor(args) = cli.command {
496 assert_eq!(args.layout, Some(monitor::LayoutPreset::ChartFocus));
497 } else {
498 panic!("Expected Monitor command");
499 }
500 }
501
502 #[test]
503 fn test_cli_parse_monitor_with_refresh() {
504 let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--refresh", "3"]).unwrap();
505
506 if let Commands::Monitor(args) = cli.command {
507 assert_eq!(args.refresh, Some(3));
508 } else {
509 panic!("Expected Monitor command");
510 }
511 }
512
513 #[test]
514 fn test_cli_parse_monitor_with_scale() {
515 let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "log"]).unwrap();
516
517 if let Commands::Monitor(args) = cli.command {
518 assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
519 } else {
520 panic!("Expected Monitor command");
521 }
522 }
523
524 #[test]
525 fn test_cli_parse_monitor_with_color_scheme() {
526 let cli =
527 Cli::try_parse_from(["scope", "monitor", "USDC", "--color-scheme", "blue-orange"])
528 .unwrap();
529
530 if let Commands::Monitor(args) = cli.command {
531 assert_eq!(args.color_scheme, Some(monitor::ColorScheme::BlueOrange));
532 } else {
533 panic!("Expected Monitor command");
534 }
535 }
536
537 #[test]
538 fn test_cli_parse_monitor_with_export() {
539 let cli =
540 Cli::try_parse_from(["scope", "monitor", "USDC", "--export", "/tmp/out.csv"]).unwrap();
541
542 if let Commands::Monitor(args) = cli.command {
543 assert_eq!(args.export, Some(std::path::PathBuf::from("/tmp/out.csv")));
544 } else {
545 panic!("Expected Monitor command");
546 }
547 }
548
549 #[test]
550 fn test_cli_parse_monitor_short_flags() {
551 let cli = Cli::try_parse_from([
552 "scope", "mon", "USDC", "-c", "solana", "-l", "compact", "-r", "10", "-s", "log", "-e",
553 "data.csv",
554 ])
555 .unwrap();
556
557 if let Commands::Monitor(args) = cli.command {
558 assert_eq!(args.token, "USDC");
559 assert_eq!(args.chain, "solana");
560 assert_eq!(args.layout, Some(monitor::LayoutPreset::Compact));
561 assert_eq!(args.refresh, Some(10));
562 assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
563 assert_eq!(args.export, Some(std::path::PathBuf::from("data.csv")));
564 } else {
565 panic!("Expected Monitor command");
566 }
567 }
568
569 #[test]
570 fn test_cli_parse_monitor_missing_token_fails() {
571 let result = Cli::try_parse_from(["scope", "monitor"]);
572 assert!(result.is_err());
573 }
574
575 #[test]
576 fn test_cli_parse_monitor_invalid_layout_fails() {
577 let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "invalid"]);
578 assert!(result.is_err());
579 }
580
581 #[test]
582 fn test_cli_parse_monitor_invalid_scale_fails() {
583 let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "quadratic"]);
584 assert!(result.is_err());
585 }
586}