1pub mod address;
27pub mod compliance;
28pub mod crawl;
29pub mod export;
30pub mod interactive;
31pub mod monitor;
32pub mod portfolio;
33pub mod setup;
34pub mod tx;
35
36use clap::{Parser, Subcommand};
37use std::path::PathBuf;
38
39pub use address::AddressArgs;
40pub use crawl::CrawlArgs;
41pub use export::ExportArgs;
42pub use interactive::InteractiveArgs;
43pub use portfolio::PortfolioArgs;
44pub use setup::SetupArgs;
45pub use tx::TxArgs;
46
47#[derive(Debug, Parser)]
53#[command(
54 name = "scope",
55 version,
56 about = "Scope Blockchain Analysis - A tool for blockchain data analysis",
57 long_about = "Scope Blockchain Analysis is a production-grade tool for \
58 blockchain data analysis, portfolio tracking, and transaction investigation.\n\n\
59 Use --help with any subcommand for detailed usage information."
60)]
61pub struct Cli {
62 #[command(subcommand)]
64 pub command: Commands,
65
66 #[arg(long, global = true, value_name = "PATH")]
70 pub config: Option<PathBuf>,
71
72 #[arg(short, long, global = true, action = clap::ArgAction::Count)]
79 pub verbose: u8,
80
81 #[arg(long, global = true)]
83 pub no_color: bool,
84}
85
86#[derive(Debug, Subcommand)]
88pub enum Commands {
89 #[command(visible_alias = "addr")]
94 Address(AddressArgs),
95
96 #[command(visible_alias = "transaction")]
101 Tx(TxArgs),
102
103 #[command(visible_alias = "token")]
109 Crawl(CrawlArgs),
110
111 #[command(visible_alias = "port")]
116 Portfolio(PortfolioArgs),
117
118 Export(ExportArgs),
123
124 #[command(visible_alias = "shell")]
129 Interactive(InteractiveArgs),
130
131 #[command(visible_alias = "config")]
136 Setup(SetupArgs),
137
138 #[command(subcommand)]
143 Compliance(compliance::ComplianceCommands),
144}
145
146impl Cli {
147 pub fn parse_args() -> Self {
151 Self::parse()
152 }
153
154 pub fn log_level(&self) -> tracing::Level {
162 match self.verbose {
163 0 => tracing::Level::WARN,
164 1 => tracing::Level::INFO,
165 2 => tracing::Level::DEBUG,
166 _ => tracing::Level::TRACE,
167 }
168 }
169}
170
171#[cfg(test)]
176mod tests {
177 use super::*;
178 use clap::Parser;
179
180 #[test]
181 fn test_cli_parse_address_command() {
182 let cli = Cli::try_parse_from([
183 "scope",
184 "address",
185 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
186 ])
187 .unwrap();
188
189 assert!(matches!(cli.command, Commands::Address(_)));
190 assert!(cli.config.is_none());
191 assert_eq!(cli.verbose, 0);
192 }
193
194 #[test]
195 fn test_cli_parse_address_alias() {
196 let cli = Cli::try_parse_from([
197 "scope",
198 "addr",
199 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
200 ])
201 .unwrap();
202
203 assert!(matches!(cli.command, Commands::Address(_)));
204 }
205
206 #[test]
207 fn test_cli_parse_tx_command() {
208 let cli = Cli::try_parse_from([
209 "scope",
210 "tx",
211 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
212 ])
213 .unwrap();
214
215 assert!(matches!(cli.command, Commands::Tx(_)));
216 }
217
218 #[test]
219 fn test_cli_parse_tx_alias() {
220 let cli = Cli::try_parse_from([
221 "scope",
222 "transaction",
223 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
224 ])
225 .unwrap();
226
227 assert!(matches!(cli.command, Commands::Tx(_)));
228 }
229
230 #[test]
231 fn test_cli_parse_portfolio_command() {
232 let cli = Cli::try_parse_from(["scope", "portfolio", "list"]).unwrap();
233
234 assert!(matches!(cli.command, Commands::Portfolio(_)));
235 }
236
237 #[test]
238 fn test_cli_parse_export_command() {
239 let cli = Cli::try_parse_from([
240 "scope",
241 "export",
242 "--address",
243 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
244 "--output",
245 "data.json",
246 ])
247 .unwrap();
248
249 assert!(matches!(cli.command, Commands::Export(_)));
250 }
251
252 #[test]
253 fn test_cli_parse_interactive_command() {
254 let cli = Cli::try_parse_from(["scope", "interactive"]).unwrap();
255
256 assert!(matches!(cli.command, Commands::Interactive(_)));
257 }
258
259 #[test]
260 fn test_cli_parse_interactive_alias() {
261 let cli = Cli::try_parse_from(["scope", "shell"]).unwrap();
262
263 assert!(matches!(cli.command, Commands::Interactive(_)));
264 }
265
266 #[test]
267 fn test_cli_parse_interactive_no_banner() {
268 let cli = Cli::try_parse_from(["scope", "interactive", "--no-banner"]).unwrap();
269
270 if let Commands::Interactive(args) = cli.command {
271 assert!(args.no_banner);
272 } else {
273 panic!("Expected Interactive command");
274 }
275 }
276
277 #[test]
278 fn test_cli_verbose_flag_counting() {
279 let cli = Cli::try_parse_from([
280 "scope",
281 "-vvv",
282 "address",
283 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
284 ])
285 .unwrap();
286
287 assert_eq!(cli.verbose, 3);
288 }
289
290 #[test]
291 fn test_cli_verbose_separate_flags() {
292 let cli = Cli::try_parse_from([
293 "scope",
294 "-v",
295 "-v",
296 "address",
297 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
298 ])
299 .unwrap();
300
301 assert_eq!(cli.verbose, 2);
302 }
303
304 #[test]
305 fn test_cli_global_config_option() {
306 let cli = Cli::try_parse_from([
307 "scope",
308 "--config",
309 "/custom/path.yaml",
310 "tx",
311 "0xabc123def456789012345678901234567890123456789012345678901234abcd",
312 ])
313 .unwrap();
314
315 assert_eq!(cli.config, Some(PathBuf::from("/custom/path.yaml")));
316 }
317
318 #[test]
319 fn test_cli_config_long_flag() {
320 let cli = Cli::try_parse_from([
321 "scope",
322 "--config",
323 "/custom/config.yaml",
324 "address",
325 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
326 ])
327 .unwrap();
328
329 assert_eq!(cli.config, Some(PathBuf::from("/custom/config.yaml")));
330 }
331
332 #[test]
333 fn test_cli_no_color_flag() {
334 let cli = Cli::try_parse_from([
335 "scope",
336 "--no-color",
337 "address",
338 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
339 ])
340 .unwrap();
341
342 assert!(cli.no_color);
343 }
344
345 #[test]
346 fn test_cli_missing_required_args_fails() {
347 let result = Cli::try_parse_from(["scope", "address"]);
348 assert!(result.is_err());
349 }
350
351 #[test]
352 fn test_cli_invalid_subcommand_fails() {
353 let result = Cli::try_parse_from(["scope", "invalid"]);
354 assert!(result.is_err());
355 }
356
357 #[test]
358 fn test_cli_log_level_default() {
359 let cli = Cli::try_parse_from([
360 "scope",
361 "address",
362 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
363 ])
364 .unwrap();
365
366 assert_eq!(cli.log_level(), tracing::Level::WARN);
367 }
368
369 #[test]
370 fn test_cli_log_level_info() {
371 let cli = Cli::try_parse_from([
372 "scope",
373 "-v",
374 "address",
375 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
376 ])
377 .unwrap();
378
379 assert_eq!(cli.log_level(), tracing::Level::INFO);
380 }
381
382 #[test]
383 fn test_cli_log_level_debug() {
384 let cli = Cli::try_parse_from([
385 "scope",
386 "-vv",
387 "address",
388 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
389 ])
390 .unwrap();
391
392 assert_eq!(cli.log_level(), tracing::Level::DEBUG);
393 }
394
395 #[test]
396 fn test_cli_log_level_trace() {
397 let cli = Cli::try_parse_from([
398 "scope",
399 "-vvvv",
400 "address",
401 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
402 ])
403 .unwrap();
404
405 assert_eq!(cli.log_level(), tracing::Level::TRACE);
406 }
407
408 #[test]
409 fn test_cli_debug_impl() {
410 let cli = Cli::try_parse_from([
411 "scope",
412 "address",
413 "0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
414 ])
415 .unwrap();
416
417 let debug_str = format!("{:?}", cli);
418 assert!(debug_str.contains("Cli"));
419 assert!(debug_str.contains("Address"));
420 }
421}