ckb-cli 0.100.0

ckb command line interface
use std::collections::HashMap;
use std::env;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
use std::process;
use std::sync::Arc;

use ckb_build_info::Version;
use ckb_sdk::{rpc::RawHttpRpcClient, HttpRpcClient};
use ckb_util::RwLock;
use clap::crate_version;
use clap::{App, AppSettings, Arg};
#[cfg(unix)]
use subcommands::{Output, TuiSubCommand};

use interactive::InteractiveEnv;
use plugin::PluginManager;
use subcommands::{
    start_index_thread, AccountSubCommand, ApiServerSubCommand, CliSubCommand, DAOSubCommand,
    IndexSubCommand, MockTxSubCommand, MoleculeSubCommand, PluginSubCommand, PubSubCommand,
    RpcSubCommand, TxSubCommand, UtilSubCommand, WalletSubCommand,
};
use utils::other::get_genesis_info;
use utils::{
    arg_parser::{ArgParser, UrlParser},
    config::GlobalConfig,
    index::IndexThreadState,
    other::{check_alerts, get_key_store, get_network_type, index_dirname},
    printer::{ColorWhen, OutputFormat},
};

mod interactive;
mod plugin;
#[allow(clippy::mutable_key_type)]
mod subcommands;
#[allow(clippy::mutable_key_type)]
mod utils;

fn main() -> Result<(), io::Error> {
    env_logger::init();

    #[cfg(unix)]
    let ansi_support = true;
    #[cfg(not(unix))]
    let ansi_support = ansi_term::enable_ansi_support().is_ok();

    let version = get_version();
    // TODO:
    //   It will not print newline with --version or -V, it's a bug of clap. https://github.com/clap-rs/clap/issues/1960
    //   revisit here when clap updated.
    let version_short = format!("{}\n", version.short());
    let version_long = format!("{}\n", version.long());
    let matches = build_cli(version_short.as_str(), version_long.as_str()).get_matches();

    let mut env_map: HashMap<String, String> = env::vars().collect();
    let api_uri_opt = matches
        .value_of("url")
        .map(ToOwned::to_owned)
        .or_else(|| env_map.remove("API_URL"));

    let ckb_cli_dir = if let Some(dir_string) = env_map.remove("CKB_CLI_HOME") {
        let dir = PathBuf::from(dir_string.as_str());
        if !dir.exists() {
            fs::create_dir_all(&dir)?;
        }
        if dir.exists() && !dir.is_dir() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("{} is not a directory", dir_string),
            ));
        }
        dir
    } else {
        let mut dir = dirs::home_dir().unwrap();
        dir.push(".ckb-cli");
        dir
    };

    let mut index_dir = ckb_cli_dir.clone();
    index_dir.push(index_dirname());
    let index_state = Arc::new(RwLock::new(IndexThreadState::default()));

    let mut config = GlobalConfig::new(api_uri_opt.clone(), Arc::clone(&index_state));
    let mut config_file = ckb_cli_dir.clone();
    config_file.push("config");

    let mut output_format = OutputFormat::Yaml;
    if config_file.as_path().exists() {
        let mut file = fs::File::open(&config_file)?;
        let mut content = String::new();
        file.read_to_string(&mut content)?;
        let configs: serde_json::Value = serde_json::from_str(content.as_str()).unwrap();
        if api_uri_opt.is_none() {
            if let Some(value) = configs["url"].as_str() {
                config.set_url(value.to_string());
            }
        }
        config.set_debug(configs["debug"].as_bool().unwrap_or(false));
        config.set_no_sync(configs["no-sync"].as_bool().unwrap_or(false));
        config.set_color(ansi_support && configs["color"].as_bool().unwrap_or(true));
        output_format =
            OutputFormat::from_str(&configs["output_format"].as_str().unwrap_or("yaml"))
                .unwrap_or(OutputFormat::Yaml);
        config.set_output_format(output_format);
        config.set_completion_style(configs["completion_style"].as_bool().unwrap_or(true));
        config.set_edit_style(configs["edit_style"].as_bool().unwrap_or(true));
    }

    let api_uri = config.get_url().to_string();
    let index_state_clone = Arc::clone(&index_state);
    let index_controller = start_index_thread(api_uri.as_str(), index_dir.clone(), index_state);
    let mut rpc_client = HttpRpcClient::new(api_uri.clone());
    let mut raw_rpc_client = RawHttpRpcClient::new(api_uri.as_str());
    check_alerts(&mut rpc_client);
    config.set_network(get_network_type(&mut rpc_client).ok());

    let color = ColorWhen::new(!matches.is_present("no-color")).color();
    let debug = matches.is_present("debug");
    let wait_for_sync = !matches.is_present("no-sync");

    if let Some(format) = matches.value_of("output-format") {
        output_format = OutputFormat::from_str(format).unwrap();
    }
    let mut key_store = get_key_store(ckb_cli_dir.clone()).map_err(|err| {
        io::Error::new(
            io::ErrorKind::Other,
            format!("Open file based key store error: {}", err),
        )
    })?;
    let mut plugin_mgr = PluginManager::init(&ckb_cli_dir, api_uri.clone()).unwrap();
    let result = match matches.subcommand() {
        #[cfg(unix)]
        ("tui", _) => TuiSubCommand::new(api_uri, index_dir, index_controller.clone())
            .start()
            .map(|s| Output::new_output(serde_json::json!(s))),
        ("rpc", Some(sub_matches)) => match sub_matches.subcommand() {
            ("subscribe", Some(sub_sub_matches)) => {
                PubSubCommand::new(output_format, color).process(&sub_sub_matches, debug)
            }
            _ => RpcSubCommand::new(&mut rpc_client, &mut raw_rpc_client)
                .process(&sub_matches, debug),
        },
        ("account", Some(sub_matches)) => {
            AccountSubCommand::new(&mut plugin_mgr, &mut key_store).process(&sub_matches, debug)
        }
        ("mock-tx", Some(sub_matches)) => {
            MockTxSubCommand::new(&mut rpc_client, &mut plugin_mgr, None)
                .process(&sub_matches, debug)
        }
        ("tx", Some(sub_matches)) => {
            TxSubCommand::new(&mut rpc_client, &mut plugin_mgr, None).process(&sub_matches, debug)
        }
        ("util", Some(sub_matches)) => {
            UtilSubCommand::new(&mut rpc_client, &mut plugin_mgr).process(&sub_matches, debug)
        }
        ("server", Some(sub_matches)) => ApiServerSubCommand::new(
            &mut rpc_client,
            plugin_mgr,
            None,
            index_dir,
            index_controller.clone(),
        )
        .process(&sub_matches, debug),
        ("plugin", Some(sub_matches)) => {
            PluginSubCommand::new(&mut plugin_mgr).process(&sub_matches, debug)
        }
        ("molecule", Some(sub_matches)) => MoleculeSubCommand::new().process(&sub_matches, debug),
        ("index", Some(sub_matches)) => IndexSubCommand::new(
            &mut rpc_client,
            None,
            index_dir,
            index_controller.clone(),
            index_state_clone,
            wait_for_sync,
        )
        .process(&sub_matches, debug),
        ("wallet", Some(sub_matches)) => WalletSubCommand::new(
            &mut rpc_client,
            &mut plugin_mgr,
            None,
            index_dir,
            index_controller.clone(),
            wait_for_sync,
        )
        .process(&sub_matches, debug),
        ("dao", Some(sub_matches)) => {
            get_genesis_info(&None, &mut rpc_client).and_then(|genesis_info| {
                DAOSubCommand::new(
                    &mut rpc_client,
                    &mut plugin_mgr,
                    genesis_info,
                    index_dir.clone(),
                    index_controller.clone(),
                    wait_for_sync,
                )
                .process(&sub_matches, debug)
            })
        }
        _ => {
            if let Err(err) = InteractiveEnv::from_config(
                ckb_cli_dir,
                config,
                plugin_mgr,
                key_store,
                index_controller.clone(),
                index_state_clone,
            )
            .and_then(|mut env| env.start())
            {
                eprintln!("Process error: {}", err);
                index_controller.shutdown();
                process::exit(1);
            }
            index_controller.shutdown();
            process::exit(0)
        }
    };

    match result {
        Ok(output) => {
            output.print(output_format, color);
            index_controller.shutdown();
        }
        Err(err) => {
            eprintln!("{}", err);
            index_controller.shutdown();
            process::exit(1);
        }
    }
    Ok(())
}

pub fn get_version() -> Version {
    let major = env!("CARGO_PKG_VERSION_MAJOR")
        .parse::<u8>()
        .expect("CARGO_PKG_VERSION_MAJOR parse success");
    let minor = env!("CARGO_PKG_VERSION_MINOR")
        .parse::<u8>()
        .expect("CARGO_PKG_VERSION_MINOR parse success");
    let patch = env!("CARGO_PKG_VERSION_PATCH")
        .parse::<u16>()
        .expect("CARGO_PKG_VERSION_PATCH parse success");
    let dash_pre = {
        let pre = env!("CARGO_PKG_VERSION_PRE");
        if pre.is_empty() {
            pre.to_string()
        } else {
            "-".to_string() + pre
        }
    };

    let commit_describe = option_env!("COMMIT_DESCRIBE").map(ToString::to_string);
    #[cfg(docker)]
    let commit_describe = commit_describe.map(|s| s.replace("-dirty", ""));
    let commit_date = option_env!("COMMIT_DATE").map(ToString::to_string);
    Version {
        code_name: None,
        major,
        minor,
        patch,
        dash_pre,
        commit_describe,
        commit_date,
    }
}

pub fn build_cli<'a>(version_short: &'a str, version_long: &'a str) -> App<'a> {
    let app = App::new("ckb-cli")
        .version(version_short)
        .long_version(version_long)
        .global_setting(AppSettings::ColoredHelp)
        .global_setting(AppSettings::DeriveDisplayOrder)
        .subcommand(RpcSubCommand::subcommand().subcommand(PubSubCommand::subcommand()))
        .subcommand(AccountSubCommand::subcommand("account"))
        .subcommand(MockTxSubCommand::subcommand("mock-tx"))
        .subcommand(TxSubCommand::subcommand("tx"))
        .subcommand(ApiServerSubCommand::subcommand("server"))
        .subcommand(UtilSubCommand::subcommand("util"))
        .subcommand(PluginSubCommand::subcommand("plugin"))
        .subcommand(MoleculeSubCommand::subcommand("molecule"))
        .subcommand(IndexSubCommand::subcommand("index"))
        .subcommand(WalletSubCommand::subcommand())
        .subcommand(DAOSubCommand::subcommand())
        .arg(
            Arg::with_name("url")
                .long("url")
                .takes_value(true)
                .validator(|input| UrlParser.validate(input))
                .about("RPC API server url"),
        )
        .arg(
            Arg::with_name("output-format")
                .long("output-format")
                .takes_value(true)
                .possible_values(&["yaml", "json"])
                .default_value("yaml")
                .global(true)
                .about("Select output format"),
        )
        .arg(
            Arg::with_name("no-color")
                .long("no-color")
                .global(true)
                .about("Do not highlight(color) output json"),
        )
        .arg(
            Arg::with_name("debug")
                .long("debug")
                .global(true)
                .about("Display request parameters"),
        )
        .arg(
            Arg::with_name("wait-for-sync")
                .long("wait-for-sync")
                .conflicts_with("no-sync")
                .global(true)
                .about(
                    "Ensure the index-store synchronizes completely before command being executed",
                ),
        )
        .arg(
            Arg::with_name("no-sync")
                .long("no-sync")
                .conflicts_with("wait-for-sync")
                .global(true)
                .about("Don't wait index database sync to tip"),
        );

    #[cfg(unix)]
    let app = app.subcommand(App::new("tui").about("Enter TUI mode"));

    app
}

pub fn build_interactive() -> App<'static> {
    App::new("interactive")
        .version(crate_version!())
        .global_setting(AppSettings::NoBinaryName)
        .global_setting(AppSettings::ColoredHelp)
        .global_setting(AppSettings::DeriveDisplayOrder)
        .global_setting(AppSettings::DisableVersion)
        .subcommand(
            App::new("config")
                .about("Config environment")
                .arg(
                    Arg::with_name("url")
                        .long("url")
                        .validator(|input| UrlParser.validate(input))
                        .takes_value(true)
                        .about("Config RPC API url"),
                )
                .arg(
                    Arg::with_name("color")
                        .long("color")
                        .about("Switch color for rpc interface"),
                )
                .arg(
                    Arg::with_name("debug")
                        .long("debug")
                        .about("Switch debug mode"),
                )
                .arg(
                    Arg::with_name("no-sync")
                        .long("no-sync")
                        .about("Switch whether wait index database sync to tip"),
                )
                .arg(
                    Arg::with_name("output-format")
                        .long("output-format")
                        .takes_value(true)
                        .possible_values(&["yaml", "json"])
                        .default_value("yaml")
                        .about("Select output format"),
                )
                .arg(
                    Arg::with_name("completion_style")
                        .long("completion_style")
                        .about("Switch completion style"),
                )
                .arg(
                    Arg::with_name("edit_style")
                        .long("edit_style")
                        .about("Switch edit style"),
                ),
        )
        .subcommand(App::new("info").about("Display global variables"))
        .subcommand(
            App::new("exit")
                .visible_alias("quit")
                .about("Exit the interactive interface"),
        )
        .subcommand(RpcSubCommand::subcommand())
        .subcommand(AccountSubCommand::subcommand("account"))
        .subcommand(MockTxSubCommand::subcommand("mock-tx"))
        .subcommand(TxSubCommand::subcommand("tx"))
        .subcommand(UtilSubCommand::subcommand("util"))
        .subcommand(PluginSubCommand::subcommand("plugin"))
        .subcommand(MoleculeSubCommand::subcommand("molecule"))
        .subcommand(IndexSubCommand::subcommand("index"))
        .subcommand(WalletSubCommand::subcommand())
        .subcommand(DAOSubCommand::subcommand())
}