use anyhow::Result;
use clap::{CommandFactory, Parser};
use clap_complete::generate;
use scope::Config;
use scope::chains::DefaultClientFactory;
use scope::config::OutputFormat;
use scope::http::HttpClient;
use scope_cli::cli::errors::display_error;
use scope_cli::cli::{Cli, Commands};
use std::io::{self, Write};
use std::sync::Arc;
use tracing_subscriber::EnvFilter;
const BANNER: &str = include_str!("../assets/banner.txt");
fn print_banner() {
eprintln!("{}", BANNER);
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
init_logging(cli.verbose);
if cli.verbose > 0 {
print_banner();
}
tracing::debug!("Scope v{} starting", scope::VERSION);
let is_setup_command = matches!(cli.command, Commands::Setup(_));
let mut config = Config::load(cli.config.as_deref()).unwrap_or_else(|e| {
eprintln!(" ⚠ Could not load config, using defaults (use -v for details)");
tracing::debug!("Failed to load config: {}", e);
Config::default()
});
if cli.ai {
config.output.format = OutputFormat::Markdown;
}
if let Commands::Web(ref web_args) = cli.command {
let addr: std::net::SocketAddr = format!("{}:{}", web_args.bind, web_args.port)
.parse()
.unwrap_or_else(|_| {
eprintln!("Invalid bind address: {}:{}", web_args.bind, web_args.port);
std::process::exit(1);
});
if web_args.stop {
if let Err(e) = scope_web::stop_daemon() {
eprintln!("Error: {}", e);
std::process::exit(1);
}
return Ok(());
}
if scope_web::is_daemon_child() || !web_args.daemon {
return scope_web::start_server(addr, config)
.await
.map_err(|e| anyhow::anyhow!(e));
}
if let Err(e) = scope_web::start_daemon(addr, config) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
return Ok(());
}
if !is_setup_command && !config_file_exists(&cli) && prompt_for_setup() {
let setup_args = scope_cli::cli::setup::SetupArgs {
status: false,
key: None,
reset: false,
};
if let Err(e) = scope_cli::cli::setup::run(setup_args, &config).await {
eprintln!("Setup failed: {}", e);
}
let mut config = Config::load(cli.config.as_deref()).unwrap_or_default();
if cli.ai {
config.output.format = OutputFormat::Markdown;
}
return dispatch(cli.command, &config).await;
}
dispatch(cli.command, &config).await
}
fn config_file_exists(cli: &Cli) -> bool {
if let Some(ref path) = cli.config {
return path.exists();
}
Config::config_path().map(|p| p.exists()).unwrap_or(false)
}
fn prompt_for_setup() -> bool {
eprintln!();
eprintln!("Welcome to Scope Blockchain Analysis!");
eprintln!();
eprintln!("No configuration file found. Would you like to run the setup wizard");
eprintln!("to configure API keys and preferences?");
eprintln!();
eprint!("Run setup now? [Y/n]: ");
io::stderr().flush().ok();
let mut input = String::new();
if io::stdin().read_line(&mut input).is_err() {
return false;
}
let response = input.trim().to_lowercase();
response.is_empty() || response == "y" || response == "yes"
}
async fn dispatch(command: Commands, config: &Config) -> Result<()> {
if !command.is_interactive() {
eprintln!("Scope v{}", scope::VERSION);
}
let http: Arc<dyn HttpClient> = create_http_client(config);
let factory = DefaultClientFactory {
chains_config: config.chains.clone(),
http,
};
let result = match command {
Commands::Completions(args) => {
let mut cmd = Cli::command();
generate(args.shell, &mut cmd, "scope", &mut io::stdout());
Ok(())
}
Commands::Address(args) => scope_cli::cli::address::run(args, config, &factory).await,
Commands::Tx(args) => scope_cli::cli::tx::run(args, config, &factory).await,
Commands::Crawl(args) => scope_cli::cli::crawl::run(args, config, &factory).await,
Commands::AddressBook(args) => {
scope_cli::cli::address_book::run(args, config, &factory).await
}
Commands::Export(args) => scope_cli::cli::export::run(args, config, &factory).await,
Commands::Interactive(args) => {
scope_cli::cli::interactive::run(args, config, &factory).await
}
Commands::Monitor(args) => {
scope_cli::cli::monitor::run_direct(args, config, &factory).await
}
Commands::Setup(args) => scope_cli::cli::setup::run(args, config).await,
Commands::Compliance(compliance_cmd) => match compliance_cmd {
scope_cli::cli::compliance::ComplianceCommands::Risk(mut args) => {
if let Some((addr, chain)) =
scope_cli::cli::address_book::resolve_address_book_input(&args.address, config)?
{
args.address = addr;
if args.chain.is_none() {
args.chain = Some(chain);
}
}
scope_cli::cli::compliance::handle_risk(args)
.await
.map_err(|e| scope::error::ScopeError::Compliance(e.to_string()))
}
scope_cli::cli::compliance::ComplianceCommands::Trace(args) => {
scope_cli::cli::compliance::handle_trace(args)
.await
.map_err(|e| scope::error::ScopeError::Compliance(e.to_string()))
}
scope_cli::cli::compliance::ComplianceCommands::Analyze(mut args) => {
if let Some((addr, _chain)) =
scope_cli::cli::address_book::resolve_address_book_input(&args.address, config)?
{
args.address = addr;
}
scope_cli::cli::compliance::handle_analyze(args)
.await
.map_err(|e| scope::error::ScopeError::Compliance(e.to_string()))
}
scope_cli::cli::compliance::ComplianceCommands::ComplianceReport(mut args) => {
if !std::path::Path::new(&args.target).exists()
&& let Some((addr, _chain)) =
scope_cli::cli::address_book::resolve_address_book_input(
&args.target,
config,
)?
{
args.target = addr;
}
scope_cli::cli::compliance::handle_compliance_report(args)
.await
.map_err(|e| scope::error::ScopeError::Compliance(e.to_string()))
}
},
Commands::Market(cmd) => scope_cli::cli::market::run(cmd, config, &factory).await,
Commands::TokenHealth(args) => {
scope_cli::cli::token_health::run(args, config, &factory).await
}
Commands::Venues(cmd) => scope_cli::cli::venues::run(cmd),
Commands::Report(cmd) => scope_cli::cli::report::run(cmd, config, &factory).await,
Commands::Discover(args) => scope_cli::cli::discover::run(args, config.output.format)
.await
.map_err(|e| scope::error::ScopeError::Discovery(e.to_string())),
Commands::Insights(args) => scope_cli::cli::insights::run(args, config, &factory).await,
Commands::Contract(ref args) => scope_cli::cli::contract::run(args, config, &factory).await,
Commands::Web(_) => unreachable!("Web command handled before dispatch"),
};
if let Err(e) = result {
tracing::debug!("Command failed: {}", e);
display_error(&e);
std::process::exit(1);
}
Ok(())
}
fn create_http_client(config: &Config) -> Arc<dyn HttpClient> {
if config.ghola.enabled {
match scope::http::GholaHttpClient::new(config.ghola.stealth, config.ghola.buffer_size) {
Ok(client) => {
tracing::info!("Using Ghola sidecar for HTTP transport");
return Arc::new(client);
}
Err(e) => {
eprintln!(" ⚠ Ghola sidecar enabled but unavailable: {}", e);
eprintln!(" Install: go install github.com/robot-accomplice/ghola@latest");
eprintln!(" Falling back to native HTTP transport");
}
}
}
Arc::new(scope::http::NativeHttpClient::new().expect("Failed to create HTTP client"))
}
fn init_logging(verbosity: u8) {
let level = match verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new(format!("scope={},warn", level)));
let builder = tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.with_thread_ids(false);
if verbosity < 2 {
builder.without_time().with_level(false).init();
} else {
builder.init();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verbosity_levels() {
assert_eq!(
match 0u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"warn"
);
assert_eq!(
match 1u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"info"
);
assert_eq!(
match 2u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"debug"
);
assert_eq!(
match 3u8 {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
},
"trace"
);
}
#[test]
fn test_cli_parsing() {
let result = Cli::try_parse_from([
"scope",
"address",
"0x742d35Cc6634C0532925a3b844Bc9e7595f1b3c2",
]);
assert!(result.is_ok());
}
#[test]
fn test_cli_help_flag() {
let result = Cli::try_parse_from(["scope", "--help"]);
assert!(result.is_err());
}
#[test]
fn test_cli_version_flag() {
let result = Cli::try_parse_from(["scope", "--version"]);
assert!(result.is_err());
}
#[test]
fn test_config_file_exists_with_explicit_path() {
let dir = tempfile::tempdir().unwrap();
let config_path = dir.path().join("test_config.toml");
std::fs::write(&config_path, "").unwrap();
let cli = Cli::try_parse_from([
"scope",
"--config",
config_path.to_str().unwrap(),
"venues",
"list",
])
.unwrap();
assert!(config_file_exists(&cli));
}
#[test]
fn test_config_file_exists_nonexistent() {
let cli = Cli::try_parse_from([
"scope",
"--config",
"/tmp/nonexistent_scope_config_test.toml",
"venues",
"list",
])
.unwrap();
assert!(!config_file_exists(&cli));
}
#[test]
fn test_cli_completions_parsing() {
let result = Cli::try_parse_from(["scope", "completions", "bash"]);
assert!(result.is_ok());
}
#[test]
fn test_create_http_client_default_config() {
let config = Config::default();
let client = create_http_client(&config);
let _: &dyn HttpClient = &*client;
}
#[test]
fn test_create_http_client_ghola_enabled() {
let mut config = Config::default();
config.ghola.enabled = true;
config.ghola.stealth = true;
let client = create_http_client(&config);
let _: &dyn HttpClient = &*client;
}
#[test]
fn test_create_http_client_ghola_no_stealth() {
let mut config = Config::default();
config.ghola.enabled = true;
config.ghola.stealth = false;
let client = create_http_client(&config);
let _: &dyn HttpClient = &*client;
}
}