pub mod address;
pub mod address_book;
pub mod address_report;
pub(crate) mod ansi;
pub mod charts;
pub mod compliance;
pub mod contract;
pub mod crawl;
pub mod discover;
pub mod dispatch;
pub mod errors;
pub mod export;
pub mod health_args;
pub mod insights;
pub mod interactive;
pub mod market;
pub mod monitor;
pub mod output;
pub(crate) mod plain;
pub mod progress;
pub mod prompt;
pub mod repeat;
pub mod report;
pub(crate) mod screen;
pub mod setup;
pub(crate) mod textfit;
pub mod theme;
pub mod token_health;
pub(crate) mod token_pick;
pub mod tui;
pub mod tx;
pub mod venues;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
pub use address::AddressArgs;
pub use address_book::AddressBookArgs;
pub use crawl::CrawlArgs;
pub use export::ExportArgs;
pub use interactive::InteractiveArgs;
pub use monitor::MonitorArgs;
pub use setup::SetupArgs;
pub use tx::TxArgs;
#[derive(Debug, Parser)]
#[command(
name = "scope",
version,
about = "Scope Blockchain Analysis - A tool for blockchain data analysis",
long_about = format!(
"Scope Blockchain Analysis v{}\n\n\
A production-grade tool for blockchain data analysis, address management,\n\
and transaction investigation.\n\n\
Use --help with any subcommand for detailed usage information.",
env!("CARGO_PKG_VERSION")
),
after_help = "\x1b[1mExamples:\x1b[0m\n \
scope address 0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2\n \
scope address @main-wallet \x1b[2m# address book shortcut\x1b[0m\n \
scope contract 0xdAC17F958D2ee523a2206206994597C13D831ec7\n \
scope crawl USDC --chain ethereum\n \
scope insights 0xabc123...\n \
scope monitor USDC\n \
scope compliance risk 0x742d...\n \
scope setup\n\n\
\x1b[2mTip: Use @label in any command to recall an address from the address book.\x1b[0m\n\n\
\x1b[1mDocumentation:\x1b[0m\n \
https://github.com/robot-accomplice/scope-blockchain-analysis\n \
Quickstart guide: docs/QUICKSTART.md"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true, value_name = "PATH")]
pub config: Option<PathBuf>,
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
#[arg(long, global = true)]
pub no_color: bool,
#[arg(long, global = true)]
pub ai: bool,
}
#[derive(Debug, Subcommand)]
#[allow(clippy::large_enum_variant)]
pub enum Commands {
#[command(visible_alias = "addr")]
Address(AddressArgs),
#[command(visible_alias = "transaction")]
Tx(TxArgs),
#[command(visible_alias = "insight")]
Insights(insights::InsightsArgs),
#[command(visible_alias = "ct")]
Contract(contract::ContractArgs),
#[command(visible_alias = "token")]
Crawl(CrawlArgs),
#[command(visible_alias = "health")]
TokenHealth(token_health::TokenHealthArgs),
#[command(visible_alias = "disc")]
Discover(discover::DiscoverArgs),
#[command(visible_alias = "mon")]
Monitor(MonitorArgs),
#[command(subcommand)]
Market(market::MarketCommands),
#[command(subcommand, visible_alias = "ven")]
Venues(venues::VenuesCommands),
#[command(subcommand)]
Compliance(compliance::ComplianceCommands),
#[command(visible_alias = "ab", alias = "portfolio", alias = "port")]
AddressBook(AddressBookArgs),
Export(ExportArgs),
#[command(subcommand)]
Report(report::ReportCommands),
#[command(visible_aliases = ["tui", "shell"])]
Interactive(InteractiveArgs),
#[command(visible_alias = "config")]
Setup(SetupArgs),
Completions(CompletionsArgs),
#[command(visible_alias = "serve")]
Web(WebArgs),
}
impl Commands {
pub fn is_interactive(&self) -> bool {
matches!(
self,
Commands::Interactive(_)
| Commands::Monitor(_)
| Commands::Setup(_)
| Commands::Completions(_)
| Commands::Web(_)
)
}
}
#[derive(Debug, Clone, clap::Args)]
#[command(after_help = "\x1b[1mExamples:\x1b[0m
scope web
scope web --port 3000 --bind 0.0.0.0
scope serve --daemon
scope web --stop")]
pub struct WebArgs {
#[arg(long, short, default_value = "8080")]
pub port: u16,
#[arg(long, default_value = "127.0.0.1")]
pub bind: String,
#[arg(long, short)]
pub daemon: bool,
#[arg(long)]
pub stop: bool,
}
#[derive(Debug, Clone, clap::Args)]
#[command(after_help = "\x1b[1mExamples:\x1b[0m
scope completions bash >> ~/.bashrc
scope completions zsh > ~/.zfunc/_scope
scope completions fish > ~/.config/fish/completions/scope.fish")]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: clap_complete::Shell,
}
impl Cli {
pub fn parse_args() -> Self {
Self::parse()
}
pub fn log_level(&self) -> tracing::Level {
match self.verbose {
0 => tracing::Level::WARN,
1 => tracing::Level::INFO,
2 => tracing::Level::DEBUG,
_ => tracing::Level::TRACE,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
#[test]
fn test_cli_parse_address_command() {
let cli = Cli::try_parse_from([
"scope",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert!(matches!(cli.command, Commands::Address(_)));
assert!(cli.config.is_none());
assert_eq!(cli.verbose, 0);
}
#[test]
fn test_cli_parse_address_alias() {
let cli = Cli::try_parse_from([
"scope",
"addr",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert!(matches!(cli.command, Commands::Address(_)));
}
#[test]
fn test_cli_parse_tx_command() {
let cli = Cli::try_parse_from([
"scope",
"tx",
"0xabc123def456789012345678901234567890123456789012345678901234abcd",
])
.unwrap();
assert!(matches!(cli.command, Commands::Tx(_)));
}
#[test]
fn test_cli_parse_tx_alias() {
let cli = Cli::try_parse_from([
"scope",
"transaction",
"0xabc123def456789012345678901234567890123456789012345678901234abcd",
])
.unwrap();
assert!(matches!(cli.command, Commands::Tx(_)));
}
#[test]
fn test_cli_parse_address_book_command() {
let cli = Cli::try_parse_from(["scope", "address-book", "list"]).unwrap();
assert!(matches!(cli.command, Commands::AddressBook(_)));
}
#[test]
fn test_cli_parse_address_book_portfolio_alias() {
let cli = Cli::try_parse_from(["scope", "portfolio", "list"]).unwrap();
assert!(matches!(cli.command, Commands::AddressBook(_)));
}
#[test]
fn test_cli_parse_export_command() {
let cli = Cli::try_parse_from([
"scope",
"export",
"--address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
"--output",
"data.json",
])
.unwrap();
assert!(matches!(cli.command, Commands::Export(_)));
}
#[test]
fn test_cli_parse_interactive_command() {
let cli = Cli::try_parse_from(["scope", "interactive"]).unwrap();
assert!(matches!(cli.command, Commands::Interactive(_)));
}
#[test]
fn test_cli_parse_tui_alias() {
let cli = Cli::try_parse_from(["scope", "tui"]).unwrap();
assert!(matches!(cli.command, Commands::Interactive(_)));
}
#[test]
fn test_cli_parse_interactive_alias() {
let cli = Cli::try_parse_from(["scope", "shell"]).unwrap();
assert!(matches!(cli.command, Commands::Interactive(_)));
}
#[test]
fn test_cli_parse_interactive_no_banner() {
let cli = Cli::try_parse_from(["scope", "interactive", "--no-banner"]).unwrap();
if let Commands::Interactive(args) = cli.command {
assert!(args.no_banner);
} else {
panic!("Expected Interactive command");
}
}
#[test]
fn test_cli_verbose_flag_counting() {
let cli = Cli::try_parse_from([
"scope",
"-vvv",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.verbose, 3);
}
#[test]
fn test_cli_verbose_separate_flags() {
let cli = Cli::try_parse_from([
"scope",
"-v",
"-v",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.verbose, 2);
}
#[test]
fn test_cli_global_config_option() {
let cli = Cli::try_parse_from([
"scope",
"--config",
"/custom/path.yaml",
"tx",
"0xabc123def456789012345678901234567890123456789012345678901234abcd",
])
.unwrap();
assert_eq!(cli.config, Some(PathBuf::from("/custom/path.yaml")));
}
#[test]
fn test_cli_config_long_flag() {
let cli = Cli::try_parse_from([
"scope",
"--config",
"/custom/config.yaml",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.config, Some(PathBuf::from("/custom/config.yaml")));
}
#[test]
fn test_cli_no_color_flag() {
let cli = Cli::try_parse_from([
"scope",
"--no-color",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert!(cli.no_color);
}
#[test]
fn test_cli_missing_required_args_fails() {
let result = Cli::try_parse_from(["scope", "address"]);
assert!(result.is_err());
}
#[test]
fn test_cli_invalid_subcommand_fails() {
let result = Cli::try_parse_from(["scope", "invalid"]);
assert!(result.is_err());
}
#[test]
fn test_cli_log_level_default() {
let cli = Cli::try_parse_from([
"scope",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.log_level(), tracing::Level::WARN);
}
#[test]
fn test_cli_log_level_info() {
let cli = Cli::try_parse_from([
"scope",
"-v",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.log_level(), tracing::Level::INFO);
}
#[test]
fn test_cli_log_level_debug() {
let cli = Cli::try_parse_from([
"scope",
"-vv",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.log_level(), tracing::Level::DEBUG);
}
#[test]
fn test_cli_log_level_trace() {
let cli = Cli::try_parse_from([
"scope",
"-vvvv",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert_eq!(cli.log_level(), tracing::Level::TRACE);
}
#[test]
fn test_cli_debug_impl() {
let cli = Cli::try_parse_from([
"scope",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
let debug_str = format!("{:?}", cli);
assert!(debug_str.contains("Cli"));
assert!(debug_str.contains("Address"));
}
#[test]
fn test_cli_parse_monitor_command() {
let cli = Cli::try_parse_from([
"scope",
"monitor",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
])
.unwrap();
assert!(matches!(cli.command, Commands::Monitor(_)));
}
#[test]
fn test_cli_parse_monitor_alias_mon() {
let cli = Cli::try_parse_from(["scope", "mon", "USDC"]).unwrap();
assert!(matches!(cli.command, Commands::Monitor(_)));
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.token, "USDC");
assert_eq!(args.chain, "ethereum"); assert!(args.layout.is_none());
assert!(args.refresh.is_none());
assert!(args.scale.is_none());
assert!(args.color_scheme.is_none());
assert!(args.export.is_none());
}
}
#[test]
fn test_cli_parse_monitor_with_chain() {
let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--chain", "solana"]).unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.token, "USDC");
assert_eq!(args.chain, "solana");
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_chain_short_flag() {
let cli = Cli::try_parse_from(["scope", "monitor", "PEPE", "-c", "ethereum"]).unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.token, "PEPE");
assert_eq!(args.chain, "ethereum");
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_with_layout() {
let cli =
Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "chart-focus"]).unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.layout, Some(monitor::LayoutPreset::ChartFocus));
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_with_refresh() {
let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--refresh", "3"]).unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.refresh, Some(3));
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_with_scale() {
let cli = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "log"]).unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_with_color_scheme() {
let cli =
Cli::try_parse_from(["scope", "monitor", "USDC", "--color-scheme", "blue-orange"])
.unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.color_scheme, Some(monitor::ColorScheme::BlueOrange));
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_with_export() {
let cli =
Cli::try_parse_from(["scope", "monitor", "USDC", "--export", "/tmp/out.csv"]).unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.export, Some(std::path::PathBuf::from("/tmp/out.csv")));
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_short_flags() {
let cli = Cli::try_parse_from([
"scope", "mon", "USDC", "-c", "solana", "-l", "compact", "-r", "10", "-s", "log", "-e",
"data.csv",
])
.unwrap();
if let Commands::Monitor(args) = cli.command {
assert_eq!(args.token, "USDC");
assert_eq!(args.chain, "solana");
assert_eq!(args.layout, Some(monitor::LayoutPreset::Compact));
assert_eq!(args.refresh, Some(10));
assert_eq!(args.scale, Some(monitor::ScaleMode::Log));
assert_eq!(args.export, Some(std::path::PathBuf::from("data.csv")));
} else {
panic!("Expected Monitor command");
}
}
#[test]
fn test_cli_parse_monitor_missing_token_fails() {
let result = Cli::try_parse_from(["scope", "monitor"]);
assert!(result.is_err());
}
#[test]
fn test_cli_parse_monitor_invalid_layout_fails() {
let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--layout", "invalid"]);
assert!(result.is_err());
}
#[test]
fn test_cli_parse_monitor_invalid_scale_fails() {
let result = Cli::try_parse_from(["scope", "monitor", "USDC", "--scale", "quadratic"]);
assert!(result.is_err());
}
#[test]
fn test_cli_parse_market_summary() {
let cli = Cli::try_parse_from(["scope", "market", "summary"]).unwrap();
assert!(matches!(cli.command, Commands::Market(_)));
}
#[test]
fn test_cli_parse_market_summary_with_pair() {
let cli = Cli::try_parse_from(["scope", "market", "summary", "DAI_USDT"]).unwrap();
if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
assert_eq!(args.pair, "DAI_USDT");
} else {
panic!("Expected Market Summary command");
}
}
#[test]
fn test_cli_parse_report_batch() {
let cli = Cli::try_parse_from([
"scope",
"report",
"batch",
"--addresses",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
"--output",
"report.md",
])
.unwrap();
assert!(matches!(cli.command, Commands::Report(_)));
}
#[test]
fn test_cli_parse_market_summary_with_thresholds() {
let cli = Cli::try_parse_from([
"scope",
"market",
"summary",
"--peg-range",
"0.002",
"--min-bid-ask-ratio",
"0.1",
"--max-bid-ask-ratio",
"10",
])
.unwrap();
if let Commands::Market(market::MarketCommands::Summary(args)) = cli.command {
assert_eq!(args.health.peg_range, Some(0.002));
assert_eq!(args.health.min_bid_ask_ratio, Some(0.1));
assert_eq!(args.health.max_bid_ask_ratio, Some(10.0));
} else {
panic!("Expected Market Summary command");
}
}
#[test]
fn test_cli_parse_token_health() {
let cli = Cli::try_parse_from(["scope", "token-health", "USDC"]).unwrap();
if let Commands::TokenHealth(args) = cli.command {
assert_eq!(args.token, "USDC");
assert!(!args.with_market);
} else {
panic!("Expected TokenHealth command");
}
}
#[test]
fn test_cli_parse_token_health_alias() {
let cli = Cli::try_parse_from(["scope", "health", "USDC", "--with-market"]).unwrap();
if let Commands::TokenHealth(args) = cli.command {
assert_eq!(args.token, "USDC");
assert!(args.with_market);
assert_eq!(args.venue, "binance"); } else {
panic!("Expected TokenHealth command");
}
}
#[test]
fn test_cli_parse_token_health_venue_biconomy() {
let cli = Cli::try_parse_from([
"scope",
"token-health",
"USDC",
"--with-market",
"--venue",
"biconomy",
])
.unwrap();
if let Commands::TokenHealth(args) = cli.command {
assert_eq!(args.venue, "biconomy");
} else {
panic!("Expected TokenHealth command");
}
}
#[test]
fn test_cli_parse_token_health_venue_eth() {
let cli = Cli::try_parse_from([
"scope",
"token-health",
"USDC",
"--with-market",
"--venue",
"eth",
])
.unwrap();
if let Commands::TokenHealth(args) = cli.command {
assert_eq!(args.venue, "eth");
} else {
panic!("Expected TokenHealth command");
}
}
#[test]
fn test_cli_parse_token_health_venue_solana() {
let cli = Cli::try_parse_from([
"scope",
"token-health",
"USDC",
"--with-market",
"--venue",
"solana",
])
.unwrap();
if let Commands::TokenHealth(args) = cli.command {
assert_eq!(args.venue, "solana");
} else {
panic!("Expected TokenHealth command");
}
}
#[test]
fn test_cli_parse_ai_flag() {
let cli = Cli::try_parse_from([
"scope",
"--ai",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
])
.unwrap();
assert!(cli.ai);
}
}