diff --git a/Cargo.lock b/Cargo.lock index 0c95e60e59..4d690a2a97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,11 +638,41 @@ dependencies = [ "atty", "bitflags", "strsim 0.8.0", - "textwrap", + "textwrap 0.11.0", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim 0.10.0", + "termcolor", + "textwrap 0.14.2", +] + +[[package]] +name = "clap_derive" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "client-core" version = "0.12.0" @@ -1061,7 +1091,7 @@ checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" dependencies = [ "atty", "cast", - "clap", + "clap 2.33.3", "criterion-plot", "csv", "itertools", @@ -2481,7 +2511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" dependencies = [ "anyhow", - "heck", + "heck 0.3.3", "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", @@ -2634,7 +2664,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21de1da96dc117443fb03c2e270b2d34b7de98d0a79a19bbb689476173745b79" dependencies = [ "anyhow", - "heck", + "heck 0.3.3", "proc-macro-crate 1.1.0", "proc-macro-error", "proc-macro2", @@ -2746,6 +2776,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -3776,7 +3812,7 @@ dependencies = [ name = "nym-client" version = "0.12.1" dependencies = [ - "clap", + "clap 2.33.3", "client-core", "coconut-interface", "config", @@ -3813,7 +3849,7 @@ dependencies = [ "bandwidth-claim-contract", "bip39", "bs58", - "clap", + "clap 2.33.3", "coconut-interface", "colored", "config", @@ -3853,8 +3889,9 @@ dependencies = [ name = "nym-mixnode" version = "0.12.1" dependencies = [ + "anyhow", "bs58", - "clap", + "clap 3.0.10", "colored", "config", "crypto", @@ -3862,6 +3899,7 @@ dependencies = [ "dotenv", "futures", "humantime-serde", + "lazy_static", "log", "mixnet-client", "mixnode-common", @@ -3887,7 +3925,7 @@ dependencies = [ name = "nym-network-requester" version = "0.12.0" dependencies = [ - "clap", + "clap 2.33.3", "dirs", "futures", "ipnetwork", @@ -3908,7 +3946,7 @@ dependencies = [ name = "nym-socks5-client" version = "0.12.1" dependencies = [ - "clap", + "clap 2.33.3", "client-core", "coconut-interface", "config", @@ -3946,7 +3984,7 @@ dependencies = [ "anyhow", "attohttpc 0.18.0", "cfg-if 1.0.0", - "clap", + "clap 2.33.3", "coconut-interface", "config", "console-subscriber", @@ -4227,6 +4265,15 @@ dependencies = [ "log", ] +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + [[package]] name = "owning_ref" version = "0.4.1" @@ -4818,7 +4865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ "bytes", - "heck", + "heck 0.3.3", "itertools", "lazy_static", "log", @@ -6221,7 +6268,7 @@ dependencies = [ "dotenv", "either", "futures", - "heck", + "heck 0.3.3", "once_cell", "proc-macro2", "quote", @@ -6415,7 +6462,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2", "quote", "syn", @@ -6427,7 +6474,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" dependencies = [ - "heck", + "heck 0.3.3", "proc-macro2", "quote", "syn", @@ -6621,7 +6668,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" dependencies = [ - "heck", + "heck 0.3.3", "pkg-config", "strum 0.18.0", "strum_macros 0.18.0", @@ -6638,7 +6685,7 @@ checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" dependencies = [ "anyhow", "cfg-expr", - "heck", + "heck 0.3.3", "itertools", "pkg-config", "strum 0.21.0", @@ -6983,6 +7030,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" + [[package]] name = "thin-slice" version = "0.1.1" diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index dde982e13b..56ec535e56 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -9,6 +9,7 @@ authors = [ "Jędrzej Stuczyński ", "Drazen Urch ", ] +description = "Implementation of a Loopix-based Mixnode" edition = "2018" rust-version = "1.56" @@ -16,7 +17,7 @@ rust-version = "1.56" [dependencies] bs58 = "0.4.0" -clap = "2.3" +clap = { version = "3.0.10", features = ["cargo", "derive"] } colored = "2.0" dirs = "3.0" dotenv = "0.15.0" @@ -31,6 +32,8 @@ tokio = { version="1.8", features = ["rt-multi-thread", "net", "signal"] } tokio-util = { version="0.6.7", features = ["codec"] } toml = "0.5.8" url = { version = "2.2", features = ["serde"] } +lazy_static = "1.4.0" +anyhow = "1.0.40" ## internal config = { path="../common/config" } diff --git a/mixnode/src/commands/describe.rs b/mixnode/src/commands/describe.rs index 6350f5ac11..93eb9bdf68 100644 --- a/mixnode/src/commands/describe.rs +++ b/mixnode/src/commands/describe.rs @@ -1,35 +1,23 @@ use crate::commands::*; use crate::config::Config; use crate::node::node_description::NodeDescription; -use clap::{App, Arg, ArgMatches}; -use colored::Colorize; use config::NymConfig; use std::io; use std::io::Write; -pub fn command_args<'a, 'b>() -> App<'a, 'b> { - App::new("describe") - .about("Describe your mixnode and tell people why they should delegate stake to you") - .arg( - Arg::with_name(ID_ARG_NAME) - .long(ID_ARG_NAME) - .help("The id of the mixnode you want to describe") - .takes_value(true) - .required(true), - ) +#[derive(Args)] +pub(crate) struct Describe { + /// The id of the mixnode you want to describe + #[clap(long)] + id: String, } -pub fn execute(matches: &ArgMatches) { - // figure out which node the user is describing - let id = matches - .value_of(ID_ARG_NAME) - .expect("Please provide the id of your mixnode"); - +pub(crate) fn execute(args: &Describe) { // ensure that the mixnode has in fact been initialized - match Config::load_from_file(Some(id)) { + match Config::load_from_file(Some(&args.id)) { Ok(cfg) => cfg, Err(err) => { - error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); + error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", &args.id, err); return; } }; @@ -72,7 +60,7 @@ pub fn execute(matches: &ArgMatches) { // save the struct NodeDescription::save_to_file( &node_description, - Config::default_config_directory(Some(id)), + Config::default_config_directory(Some(&args.id)), ) .unwrap() } diff --git a/mixnode/src/commands/init.rs b/mixnode/src/commands/init.rs index 61d3181ff7..1b6850c270 100644 --- a/mixnode/src/commands/init.rs +++ b/mixnode/src/commands/init.rs @@ -5,70 +5,62 @@ use crate::commands::*; use crate::config::persistence::pathfinder::MixNodePathfinder; use crate::config::Config; use crate::node::MixNode; -use clap::{App, Arg, ArgMatches}; use config::NymConfig; use crypto::asymmetric::{encryption, identity}; -pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { - App::new("init") - .about("Initialise the mixnode") - .arg( - Arg::with_name(ID_ARG_NAME) - .long(ID_ARG_NAME) - .help("Id of the nym-mixnode we want to create config for.") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name(HOST_ARG_NAME) - .long(HOST_ARG_NAME) - .help("The host on which the mixnode will be running") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name(MIX_PORT_ARG_NAME) - .long(MIX_PORT_ARG_NAME) - .help("The port on which the mixnode will be listening for mix packets") - .takes_value(true), - ) - .arg( - Arg::with_name(VERLOC_PORT_ARG_NAME) - .long(VERLOC_PORT_ARG_NAME) - .help("The port on which the mixnode will be listening for verloc packets") - .takes_value(true), - ) - .arg( - Arg::with_name(HTTP_API_PORT_ARG_NAME) - .long(HTTP_API_PORT_ARG_NAME) - .help("The port on which the mixnode will be listening for http requests") - .takes_value(true), - ) - .arg( - Arg::with_name(ANNOUNCE_HOST_ARG_NAME) - .long(ANNOUNCE_HOST_ARG_NAME) - .help("The custom host that will be reported to the directory server") - .takes_value(true), - ) - .arg( - Arg::with_name(VALIDATORS_ARG_NAME) - .long(VALIDATORS_ARG_NAME) - .help("Comma separated list of rest endpoints of the validators") - .takes_value(true), - ) - .arg( - Arg::with_name(WALLET_ADDRESS) - .long(WALLET_ADDRESS) - .help("The wallet address you will use to bond this mixnode, e.g. nymt1z9egw0knv47nmur0p8vk4rcx59h9gg4zuxrrr9") - .takes_value(true) - .required(true) - ) +#[derive(Args, Clone)] +pub(crate) struct Init { + /// Initialise the mixnode + #[clap(long)] + id: String, + + /// The host on which the mixnode will be running + #[clap(long)] + host: String, + + /// The port on which the mixnode will be listening for mix packets + #[clap(long)] + mix_port: Option, + + /// The port on which the mixnode will be listening for verloc packets + #[clap(long)] + verloc_port: Option, + + /// The port on which the mixnode will be listening for http requests + #[clap(long)] + http_api_port: Option, + + /// The custom host that will be reported to the directory server + #[clap(long)] + announce_host: Option, + + /// Comma separated list of rest endpoints of the validators + #[clap(long)] + validators: Option, + + /// The wallet address you will use to bond this mixnode, e.g. nymt1z9egw0knv47nmur0p8vk4rcx59h9gg4zuxrrr9 + #[clap(long)] + wallet_address: Option, } -pub async fn execute(matches: ArgMatches<'static>) { - // TODO: this should probably be made implicit by slapping `#[tokio::main]` on our main method - // and then removing runtime from mixnode itself in `run` - let id = matches.value_of(ID_ARG_NAME).unwrap(); +impl From for OverrideConfig { + fn from(init_config: Init) -> Self { + OverrideConfig { + id: init_config.id, + host: Some(init_config.host), + mix_port: init_config.mix_port, + verloc_port: init_config.verloc_port, + http_api_port: init_config.http_api_port, + announce_host: init_config.announce_host, + validators: init_config.validators, + wallet_address: init_config.wallet_address, + } + } +} + +pub(crate) async fn execute(args: &Init) { + let override_config_fields = OverrideConfig::from(args.clone()); + let id = &override_config_fields.id; println!("Initialising mixnode {}...", id); let already_init = if Config::default_config_file_path(Some(id)).exists() { @@ -79,7 +71,7 @@ pub async fn execute(matches: ArgMatches<'static>) { }; let mut config = Config::new(id); - config = override_config(config, &matches); + config = override_config(config, override_config_fields); // if node was already initialised, don't generate new keys if !already_init { diff --git a/mixnode/src/commands/mod.rs b/mixnode/src/commands/mod.rs index 3c8a1a628e..46d5ffe66b 100644 --- a/mixnode/src/commands/mod.rs +++ b/mixnode/src/commands/mod.rs @@ -4,7 +4,7 @@ use std::process; use crate::config::Config; -use clap::ArgMatches; +use clap::{Args, Subcommand}; use colored::Colorize; use crypto::bech32_address_validation; use url::Url; @@ -16,14 +16,38 @@ pub(crate) mod run; pub(crate) mod sign; pub(crate) mod upgrade; -pub(crate) const ID_ARG_NAME: &str = "id"; -pub(crate) const HOST_ARG_NAME: &str = "host"; -pub(crate) const MIX_PORT_ARG_NAME: &str = "mix-port"; -pub(crate) const VERLOC_PORT_ARG_NAME: &str = "verloc-port"; -pub(crate) const HTTP_API_PORT_ARG_NAME: &str = "http-api-port"; -pub(crate) const VALIDATORS_ARG_NAME: &str = "validators"; -pub(crate) const ANNOUNCE_HOST_ARG_NAME: &str = "announce-host"; -pub(crate) const WALLET_ADDRESS: &str = "wallet-address"; +#[derive(Subcommand)] +pub(crate) enum Commands { + /// Describe your mixnode and tell people why they should delegate state to you + Describe(describe::Describe), + + /// Initialise the mixnode + Init(init::Init), + + /// Starts the mixnode + Run(run::Run), + + /// Sign text to prove ownership of this mixnode + Sign(sign::Sign), + + /// Try to upgrade the mixnode + Upgrade(upgrade::Upgrade), + + /// Show details of this mixnode + NodeDetails(node_details::NodeDetails), +} + +// Configuration that can be overridden. +struct OverrideConfig { + id: String, + host: Option, + mix_port: Option, + verloc_port: Option, + http_api_port: Option, + announce_host: Option, + validators: Option, + wallet_address: Option, +} fn parse_validators(raw: &str) -> Vec { raw.split(',') @@ -36,58 +60,37 @@ fn parse_validators(raw: &str) -> Vec { .collect() } -pub(crate) fn override_config(mut config: Config, matches: &ArgMatches) -> Config { +fn override_config(mut config: Config, args: OverrideConfig) -> Config { let mut was_host_overridden = false; - if let Some(host) = matches.value_of(HOST_ARG_NAME) { + if let Some(host) = args.host { config = config.with_listening_address(host); was_host_overridden = true; } - if let Some(port) = matches - .value_of(MIX_PORT_ARG_NAME) - .map(|port| port.parse::()) - { - if let Err(err) = port { - // if port was overridden, it must be parsable - panic!("Invalid mix port value provided - {:?}", err); - } - config = config.with_mix_port(port.unwrap()); + if let Some(port) = args.mix_port { + config = config.with_mix_port(port); } - if let Some(port) = matches - .value_of(VERLOC_PORT_ARG_NAME) - .map(|port| port.parse::()) - { - if let Err(err) = port { - // if port was overridden, it must be parsable - panic!("Invalid verloc port value provided - {:?}", err); - } - config = config.with_verloc_port(port.unwrap()); + if let Some(port) = args.verloc_port { + config = config.with_verloc_port(port); } - if let Some(port) = matches - .value_of(HTTP_API_PORT_ARG_NAME) - .map(|port| port.parse::()) - { - if let Err(err) = port { - // if port was overridden, it must be parsable - panic!("Invalid http api port value provided - {:?}", err); - } - config = config.with_http_api_port(port.unwrap()); + if let Some(port) = args.http_api_port { + config = config.with_http_api_port(port); } - if let Some(raw_validators) = matches.value_of(VALIDATORS_ARG_NAME) { + if let Some(ref raw_validators) = args.validators { config = config.with_custom_validator_apis(parse_validators(raw_validators)); } - if let Some(announce_host) = matches.value_of(ANNOUNCE_HOST_ARG_NAME) { + if let Some(ref announce_host) = args.announce_host { config = config.with_announce_address(announce_host); } else if was_host_overridden { // make sure our 'announce-host' always defaults to 'host' config = config.announce_address_from_listening_address() } - if let Some(wallet_address) = matches.value_of(WALLET_ADDRESS) { + if let Some(ref wallet_address) = args.wallet_address { let trimmed = wallet_address.trim(); validate_bech32_address_or_exit(trimmed); config = config.with_wallet_address(trimmed); diff --git a/mixnode/src/commands/node_details.rs b/mixnode/src/commands/node_details.rs index 8bbecdb599..0dcfadf435 100644 --- a/mixnode/src/commands/node_details.rs +++ b/mixnode/src/commands/node_details.rs @@ -4,28 +4,24 @@ use crate::commands::*; use crate::config::Config; use crate::node::MixNode; -use clap::{App, Arg, ArgMatches}; use config::NymConfig; -pub fn command_args<'a, 'b>() -> App<'a, 'b> { - App::new("node-details") - .about("Show details of this mixnode") - .arg( - Arg::with_name(ID_ARG_NAME) - .long(ID_ARG_NAME) - .help("The id of the mixnode you want to show details for") - .takes_value(true) - .required(true), - ) +#[derive(Args)] +pub(crate) struct NodeDetails { + /// The id of the mixnode you want to show details for + #[clap(long)] + id: String, } -pub fn execute(matches: &ArgMatches) { - let id = matches.value_of(ID_ARG_NAME).unwrap(); - - let config = match Config::load_from_file(Some(id)) { +pub(crate) fn execute(args: &NodeDetails) { + let config = match Config::load_from_file(Some(&args.id)) { Ok(cfg) => cfg, Err(err) => { - error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); + error!( + "Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", + args.id, + err, + ); return; } }; diff --git a/mixnode/src/commands/run.rs b/mixnode/src/commands/run.rs index 70561d53cf..11c7346378 100644 --- a/mixnode/src/commands/run.rs +++ b/mixnode/src/commands/run.rs @@ -4,56 +4,56 @@ use crate::commands::*; use crate::config::Config; use crate::node::MixNode; -use clap::{App, Arg, ArgMatches}; use config::NymConfig; -pub fn command_args<'a, 'b>() -> App<'a, 'b> { - App::new("run") - .about("Starts the mixnode") - .arg( - Arg::with_name(ID_ARG_NAME) - .long(ID_ARG_NAME) - .help("Id of the nym-mixnode we want to run") - .takes_value(true) - .required(true), - ) - // the rest of arguments are optional, they are used to override settings in config file - .arg( - Arg::with_name(HOST_ARG_NAME) - .long(HOST_ARG_NAME) - .help("The custom host on which the mixnode will be running") - .takes_value(true), - ) - .arg( - Arg::with_name(MIX_PORT_ARG_NAME) - .long(MIX_PORT_ARG_NAME) - .help("The port on which the mixnode will be listening for mix packets") - .takes_value(true), - ) - .arg( - Arg::with_name(VERLOC_PORT_ARG_NAME) - .long(VERLOC_PORT_ARG_NAME) - .help("The port on which the mixnode will be listening for verloc packets") - .takes_value(true), - ) - .arg( - Arg::with_name(HTTP_API_PORT_ARG_NAME) - .long(HTTP_API_PORT_ARG_NAME) - .help("The port on which the mixnode will be listening for http requests") - .takes_value(true), - ) - .arg( - Arg::with_name(ANNOUNCE_HOST_ARG_NAME) - .long(ANNOUNCE_HOST_ARG_NAME) - .help("The host that will be reported to the directory server") - .takes_value(true), - ) - .arg( - Arg::with_name(VALIDATORS_ARG_NAME) - .long(VALIDATORS_ARG_NAME) - .help("Comma separated list of rest endpoints of the validators") - .takes_value(true), - ) +#[derive(Args, Clone)] +pub(crate) struct Run { + /// Id of the nym-mixnode we want to run + #[clap(long)] + id: String, + + /// The custom host on which the mixnode will be running + #[clap(long)] + host: Option, + + /// The port on which the mixnode will be listening for mix packets + #[clap(long)] + mix_port: Option, + + /// The port on which the mixnode will be listening for verloc packets + #[clap(long)] + verloc_port: Option, + + /// The port on which the mixnode will be listening for http requests + #[clap(long)] + http_api_port: Option, + + /// The host that will be reported to the directory server + #[clap(long)] + announce_host: Option, + + /// Comma separated list of rest endpoints of the validators + #[clap(long)] + validators: Option, + + /// The wallet address you will use to bond this mixnode, e.g. nymt1z9egw0knv47nmur0p8vk4rcx59h9gg4zuxrrr9 + #[clap(long)] + wallet_address: Option, +} + +impl From for OverrideConfig { + fn from(run_config: Run) -> Self { + OverrideConfig { + id: run_config.id, + host: run_config.host, + mix_port: run_config.mix_port, + verloc_port: run_config.verloc_port, + http_api_port: run_config.http_api_port, + announce_host: run_config.announce_host, + validators: run_config.validators, + wallet_address: run_config.wallet_address, + } + } } fn show_binding_warning(address: String) { @@ -71,20 +71,22 @@ fn special_addresses() -> Vec<&'static str> { vec!["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"] } -pub async fn execute(matches: ArgMatches<'static>) { - let id = matches.value_of(ID_ARG_NAME).unwrap(); +pub(crate) async fn execute(args: &Run) { + println!("Starting mixnode {}...", args.id); - println!("Starting mixnode {}...", id); - - let mut config = match Config::load_from_file(Some(id)) { + let mut config = match Config::load_from_file(Some(&args.id)) { Ok(cfg) => cfg, Err(err) => { - error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); + error!( + "Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", + args.id, + err + ); return; } }; - config = override_config(config, &matches); + config = override_config(config, OverrideConfig::from(args.clone())); if !version_check(&config) { error!("failed the local version check"); diff --git a/mixnode/src/commands/sign.rs b/mixnode/src/commands/sign.rs index 8bd666dd04..c7e697b746 100644 --- a/mixnode/src/commands/sign.rs +++ b/mixnode/src/commands/sign.rs @@ -1,41 +1,52 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::convert::TryFrom; + use crate::commands::*; use crate::config::{persistence::pathfinder::MixNodePathfinder, Config}; -use clap::{App, Arg, ArgMatches}; -use colored::Colorize; +use anyhow::{anyhow, Result}; +use clap::ArgGroup; use config::NymConfig; use crypto::asymmetric::identity; use log::error; -const SIGN_TEXT_ARG_NAME: &str = "text"; -const SIGN_ADDRESS_ARG_NAME: &str = "address"; +#[derive(Args, Clone)] +#[clap(group(ArgGroup::new("sign").required(true).args(&["address", "text"])))] +pub(crate) struct Sign { + /// The id of the mixnode you want to sign with + #[clap(long)] + id: String, -pub fn command_args<'a, 'b>() -> App<'a, 'b> { - App::new("sign") - .about("Sign text to prove ownership of this mixnode") - .arg( - Arg::with_name(ID_ARG_NAME) - .long(ID_ARG_NAME) - .help("The id of the mixnode you want to sign with") - .takes_value(true) - .required(true), - ) - .arg( - Arg::with_name(SIGN_ADDRESS_ARG_NAME) - .long(SIGN_ADDRESS_ARG_NAME) - .help("Signs your blockchain address with your identity key") - .takes_value(true) - .conflicts_with(SIGN_TEXT_ARG_NAME), - ) - .arg( - Arg::with_name(SIGN_TEXT_ARG_NAME) - .long(SIGN_TEXT_ARG_NAME) - .help("Signs an arbitrary piece of text with your identity key") - .takes_value(true) - .conflicts_with(SIGN_ADDRESS_ARG_NAME), - ) + /// Signs your blockchain address with your identity key + #[clap(long)] + address: Option, + + /// Signs an arbitrary piece of text with your identity key + #[clap(long)] + text: Option, +} + +enum SignedTarget { + Text(String), + Address(String), +} + +impl TryFrom for SignedTarget { + type Error = anyhow::Error; + + fn try_from(args: Sign) -> Result { + if let Some(text) = args.text { + Ok(SignedTarget::Text(text)) + } else if let Some(address) = args.address { + Ok(SignedTarget::Address(address)) + } else { + // This is unreachable, and hopefully clap will support it explicitly by outputting an + // enum from the ArgGroup in the future. + // See: https://github.com/clap-rs/clap/issues/2621 + Err(anyhow!("Error: missing signed target flag")) + } + } } pub fn load_identity_keys(pathfinder: &MixNodePathfinder) -> identity::KeyPair { @@ -47,7 +58,7 @@ pub fn load_identity_keys(pathfinder: &MixNodePathfinder) -> identity::KeyPair { identity_keypair } -fn print_signed_address(private_key: &identity::PrivateKey, raw_address: &str) -> String { +fn print_signed_address(private_key: &identity::PrivateKey, raw_address: &str) { let trimmed = raw_address.trim(); validate_bech32_address_or_exit(trimmed); let signature = private_key.sign_text(trimmed); @@ -56,7 +67,6 @@ fn print_signed_address(private_key: &identity::PrivateKey, raw_address: &str) - "The base58-encoded signature on '{}' is: {}", trimmed, signature ); - signature } fn print_signed_text(private_key: &identity::PrivateKey, text: &str) { @@ -70,38 +80,39 @@ fn print_signed_text(private_key: &identity::PrivateKey, text: &str) { println!( "The base58-encoded signature on '{}' is: {}", text, signature - ) + ); } -pub fn execute(matches: &ArgMatches) { - let id = matches.value_of(ID_ARG_NAME).unwrap(); - - let config = match Config::load_from_file(Some(id)) { +pub(crate) fn execute(args: &Sign) { + let config = match Config::load_from_file(Some(&args.id)) { Ok(cfg) => cfg, Err(err) => { - error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); + error!( + "Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", + args.id, + err, + ); return; } }; if !version_check(&config) { - error!("failed the local version check"); + error!("Failed the local version check"); return; } + let signed_target = match SignedTarget::try_from(args.clone()) { + Ok(s) => s, + Err(err) => { + error!("{err}"); + return; + } + }; let pathfinder = MixNodePathfinder::new_from_config(&config); let identity_keypair = load_identity_keys(&pathfinder); - if let Some(text) = matches.value_of(SIGN_TEXT_ARG_NAME) { - print_signed_text(identity_keypair.private_key(), text) - } else if let Some(address) = matches.value_of(SIGN_ADDRESS_ARG_NAME) { - print_signed_address(identity_keypair.private_key(), address); - } else { - let error_message = format!( - "You must specify either '--{}' or '--{}' argument!", - SIGN_TEXT_ARG_NAME, SIGN_ADDRESS_ARG_NAME - ) - .red(); - println!("{}", error_message); + match signed_target { + SignedTarget::Text(text) => print_signed_text(identity_keypair.private_key(), &text), + SignedTarget::Address(addr) => print_signed_address(identity_keypair.private_key(), &addr), } } diff --git a/mixnode/src/commands/upgrade.rs b/mixnode/src/commands/upgrade.rs index 5f4afe8d50..7abbbaf02c 100644 --- a/mixnode/src/commands/upgrade.rs +++ b/mixnode/src/commands/upgrade.rs @@ -3,13 +3,19 @@ use crate::commands::*; use crate::config::{missing_string_value, Config}; -use clap::{App, Arg, ArgMatches}; use config::defaults::default_api_endpoints; use config::NymConfig; use std::fmt::Display; use std::process; use version_checker::Version; +#[derive(Args)] +pub(crate) struct Upgrade { + /// Id of the nym-mixnode we want to upgrade + #[clap(long)] + id: String, +} + #[allow(dead_code)] fn fail_upgrade(from_version: D1, to_version: D2) -> ! { print_failed_upgrade(from_version, to_version); @@ -50,16 +56,6 @@ fn unsupported_upgrade(config_version: &Version, package_version: &Version) -> ! process::exit(1) } -pub fn command_args<'a, 'b>() -> App<'a, 'b> { - App::new("upgrade").about("Try to upgrade the mixnode").arg( - Arg::with_name(ID_ARG_NAME) - .long(ID_ARG_NAME) - .help("Id of the nym-mixnode we want to upgrade") - .takes_value(true) - .required(true), - ) -} - fn parse_config_version(config: &Config) -> Version { let version = Version::parse(config.get_version()).unwrap_or_else(|err| { eprintln!("failed to parse client version! - {:?}", err); @@ -96,7 +92,7 @@ fn parse_package_version() -> Version { fn minor_0_12_upgrade( config: Config, - _matches: &ArgMatches, + _args: &Upgrade, config_version: &Version, package_version: &Version, ) -> Config { @@ -128,7 +124,7 @@ fn minor_0_12_upgrade( upgraded_config } -fn do_upgrade(mut config: Config, matches: &ArgMatches, package_version: Version) { +fn do_upgrade(mut config: Config, args: &Upgrade, package_version: Version) { loop { let config_version = parse_config_version(&config); @@ -140,7 +136,7 @@ fn do_upgrade(mut config: Config, matches: &ArgMatches, package_version: Version config = match config_version.major { 0 => match config_version.minor { 9 | 10 => outdated_upgrade(&config_version, &package_version), - 11 => minor_0_12_upgrade(config, matches, &config_version, &package_version), + 11 => minor_0_12_upgrade(config, args, &config_version, &package_version), _ => unsupported_upgrade(&config_version, &package_version), }, _ => unsupported_upgrade(&config_version, &package_version), @@ -148,12 +144,10 @@ fn do_upgrade(mut config: Config, matches: &ArgMatches, package_version: Version } } -pub fn execute(matches: &ArgMatches) { +pub(crate) fn execute(args: &Upgrade) { let package_version = parse_package_version(); - let id = matches.value_of(ID_ARG_NAME).unwrap(); - - let existing_config = Config::load_from_file(Some(id)).unwrap_or_else(|err| { + let existing_config = Config::load_from_file(Some(&args.id)).unwrap_or_else(|err| { eprintln!("failed to load existing config file! - {:?}", err); process::exit(1) }); @@ -163,5 +157,5 @@ pub fn execute(matches: &ArgMatches) { process::exit(1); } - do_upgrade(existing_config, matches, package_version) + do_upgrade(existing_config, args, package_version) } diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index c9dfb16804..4bf0bf86dd 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -4,50 +4,50 @@ extern crate rocket; // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use clap::{crate_version, App, ArgMatches}; +use clap::{crate_version, Parser}; +use lazy_static::lazy_static; mod commands; mod config; mod node; +lazy_static! { + pub static ref LONG_ABOUT: String = long_version(); +} + +// Helper for passing LONG_ABOUT to clap +fn long_about() -> &'static str { + &LONG_ABOUT +} + +#[derive(Parser)] +#[clap(author = "Nymtech", version, about, long_about = Some(long_about()))] +struct Cli { + #[clap(subcommand)] + command: commands::Commands, +} + #[tokio::main] async fn main() { dotenv::dotenv().ok(); setup_logging(); println!("{}", banner()); - let arg_matches = App::new("Nym Mixnode") - .version(crate_version!()) - .long_version(&*long_version()) - .author("Nymtech") - .about("Implementation of a Loopix-based Mixnode") - .subcommand(commands::describe::command_args()) - .subcommand(commands::init::command_args()) - .subcommand(commands::run::command_args()) - .subcommand(commands::upgrade::command_args()) - .subcommand(commands::sign::command_args()) - .subcommand(commands::node_details::command_args()) - .get_matches(); - - execute(arg_matches).await; + let args = Cli::parse(); + execute(args).await; } -async fn execute(matches: ArgMatches<'static>) { - match matches.subcommand() { - ("describe", Some(m)) => commands::describe::execute(m), - ("init", Some(m)) => commands::init::execute(m.clone()).await, - ("run", Some(m)) => commands::run::execute(m.clone()).await, - ("sign", Some(m)) => commands::sign::execute(m), - ("upgrade", Some(m)) => commands::upgrade::execute(m), - ("node-details", Some(m)) => commands::node_details::execute(m), - _ => println!("{}", usage()), +async fn execute(args: Cli) { + match &args.command { + commands::Commands::Describe(m) => commands::describe::execute(m), + commands::Commands::Init(m) => commands::init::execute(m).await, + commands::Commands::Run(m) => commands::run::execute(m).await, + commands::Commands::Sign(m) => commands::sign::execute(m), + commands::Commands::Upgrade(m) => commands::upgrade::execute(m), + commands::Commands::NodeDetails(m) => commands::node_details::execute(m), } } -fn usage() -> &'static str { - "usage: --help to see available options.\n\n" -} - fn banner() -> String { format!( r#" diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 6b3cd9ea8c..1b7dff0f2d 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -1,7 +1,9 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::commands::sign::load_identity_keys; use crate::commands::validate_bech32_address_or_exit; +use crate::config::persistence::pathfinder::MixNodePathfinder; use crate::config::Config; use crate::node::http::{ description::description, @@ -15,9 +17,6 @@ use crate::node::listener::Listener; use crate::node::node_description::NodeDescription; use crate::node::node_statistics::NodeStatsWrapper; use crate::node::packet_delayforwarder::{DelayForwarder, PacketDelayForwardSender}; -use crate::{ - commands::sign::load_identity_keys, config::persistence::pathfinder::MixNodePathfinder, -}; use ::crypto::asymmetric::{encryption, identity}; use config::NymConfig; use log::{error, info, warn};