diff --git a/Cargo.lock b/Cargo.lock index 48a39cf304d..93cd2439388 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,7 @@ dependencies = [ "filesystem", "safe_arith", "sensitive_url", + "serde", "serde_json", "slashing_protection", "slot_clock", @@ -2959,8 +2960,10 @@ dependencies = [ name = "eth2_wallet_manager" version = "0.1.0" dependencies = [ + "clap", "eth2_wallet", "lockfile", + "serde", "tempfile", ] diff --git a/account_manager/Cargo.toml b/account_manager/Cargo.toml index 071e2681dd1..937e7622b55 100644 --- a/account_manager/Cargo.toml +++ b/account_manager/Cargo.toml @@ -22,6 +22,7 @@ eth2_wallet_manager = { path = "../common/eth2_wallet_manager" } filesystem = { workspace = true } safe_arith = { workspace = true } sensitive_url = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } slashing_protection = { workspace = true } slot_clock = { workspace = true } diff --git a/account_manager/src/lib.rs b/account_manager/src/lib.rs index 44ec638a09d..e2808dcb117 100644 --- a/account_manager/src/lib.rs +++ b/account_manager/src/lib.rs @@ -3,8 +3,10 @@ pub mod validator; pub mod wallet; use clap::ArgMatches; -use clap::Command; +use clap::Parser; use environment::Environment; +use serde::Deserialize; +use serde::Serialize; use types::EthSpec; pub const CMD: &str = "account_manager"; @@ -13,27 +15,31 @@ pub const VALIDATOR_DIR_FLAG: &str = "validator-dir"; pub const VALIDATOR_DIR_FLAG_ALIAS: &str = "validators-dir"; pub const WALLETS_DIR_FLAG: &str = "wallets-dir"; -pub fn cli_app() -> Command { - Command::new(CMD) - .visible_aliases(["a", "am", "account"]) - .about("Utilities for generating and managing Ethereum 2.0 accounts.") - .display_order(0) - .subcommand(wallet::cli_app()) - .subcommand(validator::cli_app()) +#[derive(Clone, Deserialize, Serialize, Debug, Parser)] +#[clap(visible_aliases = &["a", "am", "account"], about = "Utilities for generating and managing Ethereum 2.0 accounts.")] +pub struct AccountManager { + #[clap(subcommand)] + pub subcommand: AccountManagerSubcommand, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(rename_all = "kebab-case")] +pub enum AccountManagerSubcommand { + Wallet(wallet::cli::Wallet), + Validator(validator::cli::Validator), } /// Run the account manager, returning an error if the operation did not succeed. -pub fn run(matches: &ArgMatches, env: Environment) -> Result<(), String> { - match matches.subcommand() { - Some((wallet::CMD, matches)) => wallet::cli_run(matches)?, - Some((validator::CMD, matches)) => validator::cli_run(matches, env)?, - Some((unknown, _)) => { - return Err(format!( - "{} is not a valid {} command. See --help.", - unknown, CMD - )); +pub fn run( + matches: &ArgMatches, + account_manager: &AccountManager, + env: Environment, +) -> Result<(), String> { + match &account_manager.subcommand { + AccountManagerSubcommand::Wallet(wallet_config) => wallet::cli_run(wallet_config, matches)?, + AccountManagerSubcommand::Validator(validator_config) => { + validator::cli_run(validator_config, matches, env)? } - _ => return Err("No subcommand provided, see --help for options".to_string()), } Ok(()) diff --git a/account_manager/src/validator/cli.rs b/account_manager/src/validator/cli.rs new file mode 100644 index 00000000000..7ec6115e940 --- /dev/null +++ b/account_manager/src/validator/cli.rs @@ -0,0 +1,317 @@ +use bls::PublicKey; +pub use clap::Parser; +use clap::Subcommand; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +use super::exit::DEFAULT_BEACON_NODE; + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Provides commands for managing Eth2 validators.")] +pub struct Validator { + #[clap( + long, + value_name = "VALIDATOR_DIRECTORY", + conflicts_with = "datadir", + help = "The path to search for validator directories. \ + Defaults to ~/.lighthouse/{network}/validators" + )] + pub validator_dir: Option, + #[clap(subcommand)] + pub subcommand: ValidatorSubcommand, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(rename_all = "kebab-case")] +pub enum ValidatorSubcommand { + Create(Create), + Exit(Exit), + Import(Import), + List(List), + Recover(Recover), + #[clap(subcommand)] + Modify(Modify), + #[clap(subcommand)] + SlashingProtection(SlashingProtection), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + about = "Creates new validators from an existing EIP-2386 wallet using the EIP-2333 HD key \ + derivation scheme." +)] +pub struct Create { + #[clap( + long, + value_name = "WALLET_NAME", + help = "Use the wallet identified by this name" + )] + pub wallet_name: Option, + + #[clap( + long, + value_name = "WALLET_PASSWORD_PATH", + help = "A path to a file containing the password which will unlock the wallet." + )] + pub wallet_password: Option, + + #[clap( + long, + value_name = "WALLETS_DIR", + conflicts_with = "datadir", + help = "A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets" + )] + pub wallets_dir: Option, + + #[clap( + long, + value_name = "SECRETS_DIR", + help = "The path where the validator keystore passwords will be stored. \ + Defaults to ~/.lighthouse/{network}/secrets", + conflicts_with = "datadir" + )] + pub secrets_dir: Option, + + #[clap( + long, + value_name = "DEPOSIT_GWEI", + help = "The GWEI value of the deposit amount. Defaults to the minimum amount \ + required for an active validator (MAX_EFFECTIVE_BALANCE)" + )] + pub deposit_gwei: Option, + + #[clap( + long, + help = "If present, the withdrawal keystore will be stored alongside the voting \ + keypair. It is generally recommended to *not* store the withdrawal key and \ + instead generate them from the wallet seed when required." + )] + pub store_withdrawal_keystore: bool, + + #[clap( + long, + value_name = "VALIDATOR_COUNT", + conflicts_with = "at_most", + help = "The number of validators to create, regardless of how many already exist" + )] + pub count: Option, + + #[clap( + long, + value_name = "AT_MOST_VALIDATORS", + conflicts_with = "count", + help = "Observe the number of validators in --validator-dir, only creating enough to \ + reach the given count. Never deletes an existing validator." + )] + pub at_most: Option, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Submits a VoluntaryExit to the beacon chain for a given validator keystore.")] +pub struct Exit { + #[clap( + long, + value_name = "KEYSTORE_PATH", + help = "The path to the EIP-2335 voting keystore for the validator" + )] + pub keystore: PathBuf, + + #[clap( + long, + value_name = "PASSWORD_FILE_PATH", + help = "The path to the password file which unlocks the validator voting keystore" + )] + pub password_file: Option, + + #[clap( + long, + value_name = "NETWORK_ADDRESS", + default_value_t = String::from(DEFAULT_BEACON_NODE), + help = "Address to a beacon node HTTP API", + )] + pub beacon_node: String, + + #[clap( + long, + help = "Exits after publishing the voluntary exit without waiting for confirmation that the exit was included in the beacon chain" + )] + pub no_wait: bool, + + #[clap( + long, + help = "Exits without prompting for confirmation that you understand the implications of a voluntary exit. This should be used with caution" + )] + pub no_confirmation: bool, + + #[clap( + long, + help = "Only presign the voluntary exit message without publishing it" + )] + pub presign: bool, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + about = "Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \ + requesting passwords interactively. The directory flag provides a convenient \ + method for importing a directory of keys generated by the eth2-deposit-cli \ + Python utility." +)] +pub struct Import { + #[clap( + long, + value_name = "KEYSTORE_PATH", + help = "Path to a single keystore to be imported.", + conflicts_with = "directory", + required_unless_present = "directory" + )] + pub keystore: Option, + + #[clap( + long, + value_name = "KEYSTORES_DIRECTORY", + conflicts_with = "keystore", + required_unless_present = "keystore", + help = "Path to a directory which contains zero or more keystores \ + for import. This directory and all sub-directories will be \ + searched and any file name which contains 'keystore' and \ + has the '.json' extension will be attempted to be imported." + )] + pub directory: Option, + + #[clap( + long, + help = "If present, the same password will be used for all imported keystores." + )] + pub reuse_password: bool, + + #[clap( + long, + value_name = "KEYSTORE_PASSWORD_PATH", + requires = "reuse_password", + help = "The path to the file containing the password which will unlock all \ + keystores being imported. This flag must be used with `--reuse-password`. \ + The password will be copied to the `validator_definitions.yml` file, so after \ + import we strongly recommend you delete the file at KEYSTORE_PASSWORD_PATH." + )] + pub password_file: Option, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Lists the public keys of all validators.")] +pub struct List {} + +#[derive(Subcommand, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Modify validator status in validator_definitions.yml.")] +#[clap(rename_all = "snake_case")] +pub enum Modify { + Enable(Enable), + Disable(Disable), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Enable validator(s) in validator_definitions.yml.")] +pub struct Enable { + #[clap(long, value_name = "PUBKEY", help = "Validator pubkey to enable")] + pub pubkey: Option, + + #[clap( + long, + help = "Enable all validators in the validator directory", + conflicts_with = "pubkey" + )] + pub all: bool, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Disable validator(s) in validator_definitions.yml.")] +pub struct Disable { + #[clap(long, value_name = "PUBKEY", help = "Validator pubkey to disable")] + pub pubkey: Option, + + #[clap( + long, + help = "Disable all validators in the validator directory", + conflicts_with = "pubkey" + )] + pub all: bool, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap( + about = "Recovers validator private keys given a BIP-39 mnemonic phrase. \ + If you did not specify a `--first-index` or count `--count`, by default this will \ + only recover the keys associated with the validator at index 0 for an HD wallet \ + in accordance with the EIP-2333 spec." +)] +pub struct Recover { + #[clap( + long, + value_name = "FIRST_INDEX", + help = "The first of consecutive key indexes you wish to recover.", + default_value_t = 0 + )] + pub first_index: u32, + #[clap( + long, + value_name = "COUNT", + help = "The number of validator keys you wish to recover. Counted consecutively from the provided `--first_index`.", + default_value_t = 1 + )] + pub count: u32, + #[clap( + long, + value_name = "MNEMONIC_PATH", + help = "If present, the mnemonic will be read in from this file." + )] + pub mnemonic_path: Option, + #[clap( + long, + value_name = "SECRETS_DIR", + help = "The path where the validator keystore passwords will be stored. \ + Defaults to ~/.lighthouse/{network}/secrets" + )] + pub secrets_dir: Option, + #[clap( + long, + help = "If present, the withdrawal keystore will be stored alongside the voting \ + keypair. It is generally recommended to *not* store the withdrawal key and \ + instead generate them from the wallet seed when required." + )] + pub store_withdrawal_keystore: bool, +} + +#[derive(Subcommand, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Import or export slashing protection data to or from another client")] +pub enum SlashingProtection { + Import(SlashingProtectionImport), + Export(SlashingProtectionExport), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Import an interchange file")] +pub struct SlashingProtectionImport { + #[clap( + value_name = "FILE", + help = "The slashing protection interchange file to import (.json)" + )] + pub import_file: PathBuf, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Export an interchange file")] +pub struct SlashingProtectionExport { + #[clap( + value_name = "FILE", + help = "The filename to export the interchange file to" + )] + pub export_file: PathBuf, + #[clap( + long, + value_name = "PUBKEYS", + value_delimiter = ',', + help = "List of public keys to export history for. Keys should be 0x-prefixed, \ + comma-separated. All known keys will be exported if omitted" + )] + pub pubkeys: Option>, +} diff --git a/account_manager/src/validator/create.rs b/account_manager/src/validator/create.rs index 3db8c3f152d..671e357b93b 100644 --- a/account_manager/src/validator/create.rs +++ b/account_manager/src/validator/create.rs @@ -1,12 +1,11 @@ use crate::common::read_wallet_name_from_cli; -use crate::{SECRETS_DIR_FLAG, WALLETS_DIR_FLAG}; +use crate::WALLETS_DIR_FLAG; use account_utils::{ random_password, read_password_from_user, strip_off_newlines, validator_definitions, PlainText, STDIN_INPUTS_FLAG, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; -use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; +use clap::ArgMatches; +use directory::{DEFAULT_SECRET_DIR, DEFAULT_WALLET_DIR}; use environment::Environment; use eth2_wallet_manager::WalletManager; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; @@ -17,131 +16,51 @@ use std::path::{Path, PathBuf}; use types::EthSpec; use validator_dir::Builder as ValidatorDirBuilder; -pub const CMD: &str = "create"; -pub const WALLET_NAME_FLAG: &str = "wallet-name"; -pub const WALLET_PASSWORD_FLAG: &str = "wallet-password"; -pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; -pub const STORE_WITHDRAW_FLAG: &str = "store-withdrawal-keystore"; +use super::cli::Create; + pub const COUNT_FLAG: &str = "count"; pub const AT_MOST_FLAG: &str = "at-most"; +pub const WALLET_NAME_FLAG: &str = "wallet-name"; +pub const WALLET_PASSWORD_FLAG: &str = "wallet-password"; pub const WALLET_PASSWORD_PROMPT: &str = "Enter your wallet's password:"; - -pub fn cli_app() -> Command { - Command::new(CMD) - .about( - "Creates new validators from an existing EIP-2386 wallet using the EIP-2333 HD key \ - derivation scheme.", - ) - .arg( - Arg::new(WALLET_NAME_FLAG) - .long(WALLET_NAME_FLAG) - .value_name("WALLET_NAME") - .help("Use the wallet identified by this name") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(WALLET_PASSWORD_FLAG) - .long(WALLET_PASSWORD_FLAG) - .value_name("WALLET_PASSWORD_PATH") - .help("A path to a file containing the password which will unlock the wallet.") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(WALLETS_DIR_FLAG) - .long(WALLETS_DIR_FLAG) - .value_name(WALLETS_DIR_FLAG) - .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets") - .action(ArgAction::Set) - .conflicts_with("datadir") - .display_order(0) - ) - .arg( - Arg::new(SECRETS_DIR_FLAG) - .long(SECRETS_DIR_FLAG) - .value_name("SECRETS_DIR") - .help( - "The path where the validator keystore passwords will be stored. \ - Defaults to ~/.lighthouse/{network}/secrets", - ) - .conflicts_with("datadir") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(DEPOSIT_GWEI_FLAG) - .long(DEPOSIT_GWEI_FLAG) - .value_name("DEPOSIT_GWEI") - .help( - "The GWEI value of the deposit amount. Defaults to the minimum amount \ - required for an active validator (MAX_EFFECTIVE_BALANCE)", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(STORE_WITHDRAW_FLAG) - .long(STORE_WITHDRAW_FLAG) - .help( - "If present, the withdrawal keystore will be stored alongside the voting \ - keypair. It is generally recommended to *not* store the withdrawal key and \ - instead generate them from the wallet seed when required.", - ) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new(COUNT_FLAG) - .long(COUNT_FLAG) - .value_name("VALIDATOR_COUNT") - .help("The number of validators to create, regardless of how many already exist") - .conflicts_with("at-most") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(AT_MOST_FLAG) - .long(AT_MOST_FLAG) - .value_name("AT_MOST_VALIDATORS") - .help( - "Observe the number of validators in --validator-dir, only creating enough to \ - reach the given count. Never deletes an existing validator.", - ) - .conflicts_with("count") - .action(ArgAction::Set) - .display_order(0) - ) -} +pub const STORE_WITHDRAW_FLAG: &str = "store-withdrawal-keystore"; +pub const DEPOSIT_GWEI_FLAG: &str = "deposit-gwei"; pub fn cli_run( + create_config: &Create, matches: &ArgMatches, env: Environment, validator_dir: PathBuf, ) -> Result<(), String> { let spec = env.core_context().eth2_config.spec; - let name: Option = clap_utils::parse_optional(matches, WALLET_NAME_FLAG)?; + let name: Option = create_config.wallet_name.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); let wallet_base_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_WALLET_DIR) } else { - parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? + create_config + .wallets_dir + .clone() + .unwrap_or(PathBuf::from(DEFAULT_WALLET_DIR)) }; let secrets_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_SECRET_DIR) } else { - parse_path_or_default_with_flag(matches, SECRETS_DIR_FLAG, DEFAULT_SECRET_DIR)? + create_config + .secrets_dir + .clone() + .unwrap_or(PathBuf::from(DEFAULT_SECRET_DIR)) }; - let deposit_gwei = clap_utils::parse_optional(matches, DEPOSIT_GWEI_FLAG)? + let deposit_gwei = create_config + .deposit_gwei .unwrap_or(spec.max_effective_balance); - let count: Option = clap_utils::parse_optional(matches, COUNT_FLAG)?; - let at_most: Option = clap_utils::parse_optional(matches, AT_MOST_FLAG)?; + let count = create_config.count; + let at_most = create_config.at_most; // The command will always fail if the wallet dir does not exist. if !wallet_base_dir.exists() { @@ -186,8 +105,7 @@ pub fn cli_run( return Ok(()); } - let wallet_password_path: Option = - clap_utils::parse_optional(matches, WALLET_PASSWORD_FLAG)?; + let wallet_password_path = create_config.wallet_password.clone(); let wallet_name = read_wallet_name_from_cli(name, stdin_inputs)?; let wallet_password = read_wallet_password_from_cli(wallet_password_path, stdin_inputs)?; @@ -251,7 +169,7 @@ pub fn cli_run( .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) .create_eth1_tx_data(deposit_gwei, &spec) - .store_withdrawal_keystore(matches.get_flag(STORE_WITHDRAW_FLAG)) + .store_withdrawal_keystore(create_config.store_withdrawal_keystore) .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; diff --git a/account_manager/src/validator/exit.rs b/account_manager/src/validator/exit.rs index 1393d0f1526..d6e4ecdbc6e 100644 --- a/account_manager/src/validator/exit.rs +++ b/account_manager/src/validator/exit.rs @@ -1,7 +1,7 @@ +use crate::validator::cli::Exit; use account_utils::STDIN_INPUTS_FLAG; use bls::{Keypair, PublicKey}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; +use clap::ArgMatches; use environment::Environment; use eth2::{ types::{GenesisData, StateId, ValidatorData, ValidatorId, ValidatorStatus}, @@ -18,87 +18,26 @@ use std::time::Duration; use tokio::time::sleep; use types::{ChainSpec, Epoch, EthSpec, VoluntaryExit}; -pub const CMD: &str = "exit"; -pub const KEYSTORE_FLAG: &str = "keystore"; -pub const PASSWORD_FILE_FLAG: &str = "password-file"; -pub const BEACON_SERVER_FLAG: &str = "beacon-node"; -pub const NO_WAIT: &str = "no-wait"; -pub const NO_CONFIRMATION: &str = "no-confirmation"; pub const PASSWORD_PROMPT: &str = "Enter the keystore password"; -pub const PRESIGN: &str = "presign"; pub const DEFAULT_BEACON_NODE: &str = "http://localhost:5052/"; pub const CONFIRMATION_PHRASE: &str = "Exit my validator"; pub const WEBSITE_URL: &str = "https://lighthouse-book.sigmaprime.io/validator_voluntary_exit.html"; -pub fn cli_app() -> Command { - Command::new("exit") - .about("Submits a VoluntaryExit to the beacon chain for a given validator keystore.") - .arg( - Arg::new(KEYSTORE_FLAG) - .long(KEYSTORE_FLAG) - .value_name("KEYSTORE_PATH") - .help("The path to the EIP-2335 voting keystore for the validator") - .action(ArgAction::Set) - .required(true) - .display_order(0) - ) - .arg( - Arg::new(PASSWORD_FILE_FLAG) - .long(PASSWORD_FILE_FLAG) - .value_name("PASSWORD_FILE_PATH") - .help("The path to the password file which unlocks the validator voting keystore") - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(BEACON_SERVER_FLAG) - .long(BEACON_SERVER_FLAG) - .value_name("NETWORK_ADDRESS") - .help("Address to a beacon node HTTP API") - .default_value(DEFAULT_BEACON_NODE) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(NO_WAIT) - .long(NO_WAIT) - .help("Exits after publishing the voluntary exit without waiting for confirmation that the exit was included in the beacon chain") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) - .arg( - Arg::new(NO_CONFIRMATION) - .long(NO_CONFIRMATION) - .help("Exits without prompting for confirmation that you understand the implications of a voluntary exit. This should be used with caution") - .display_order(0) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - ) - .arg( - Arg::new(PRESIGN) - .long(PRESIGN) - .help("Only presign the voluntary exit message without publishing it") - .default_value("false") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) -} - -pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { - let keystore_path: PathBuf = clap_utils::parse_required(matches, KEYSTORE_FLAG)?; - let password_file_path: Option = - clap_utils::parse_optional(matches, PASSWORD_FILE_FLAG)?; - +pub fn cli_run( + exit_config: &Exit, + matches: &ArgMatches, + env: Environment, +) -> Result<(), String> { + let keystore_path: PathBuf = exit_config.keystore.clone(); + let password_file_path: Option = exit_config.password_file.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); - let no_wait = matches.get_flag(NO_WAIT); - let no_confirmation = matches.get_flag(NO_CONFIRMATION); - let presign = matches.get_flag(PRESIGN); + let no_wait = exit_config.no_wait; + let no_confirmation = exit_config.no_confirmation; let spec = env.eth2_config().spec.clone(); - let server_url: String = clap_utils::parse_required(matches, BEACON_SERVER_FLAG)?; + let server_url = exit_config.beacon_node.clone(); + let presign = exit_config.presign; let client = BeaconNodeHttpClient::new( SensitiveUrl::parse(&server_url) .map_err(|e| format!("Failed to parse beacon http server: {:?}", e))?, diff --git a/account_manager/src/validator/import.rs b/account_manager/src/validator/import.rs index 4d2353b5534..499c0ae14a5 100644 --- a/account_manager/src/validator/import.rs +++ b/account_manager/src/validator/import.rs @@ -1,4 +1,3 @@ -use crate::wallet::create::PASSWORD_FLAG; use account_utils::validator_definitions::SigningDefinition; use account_utils::{ eth2_keystore::Keystore, @@ -9,8 +8,7 @@ use account_utils::{ }, STDIN_INPUTS_FLAG, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; +use clap::ArgMatches; use slashing_protection::{SlashingDatabase, SLASHING_PROTECTION_FILENAME}; use std::fs; use std::path::PathBuf; @@ -18,79 +16,26 @@ use std::thread::sleep; use std::time::Duration; use zeroize::Zeroizing; -pub const CMD: &str = "import"; -pub const KEYSTORE_FLAG: &str = "keystore"; -pub const DIR_FLAG: &str = "directory"; -pub const REUSE_PASSWORD_FLAG: &str = "reuse-password"; +use super::cli::Import; pub const PASSWORD_PROMPT: &str = "Enter the keystore password, or press enter to omit it:"; pub const KEYSTORE_REUSE_WARNING: &str = "DO NOT USE THE ORIGINAL KEYSTORES TO VALIDATE WITH \ ANOTHER CLIENT, OR YOU WILL GET SLASHED."; +pub const CMD: &str = "import"; +pub const KEYSTORE_FLAG: &str = "keystore"; +pub const DIR_FLAG: &str = "directory"; +pub const REUSE_PASSWORD_FLAG: &str = "reuse-password"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about( - "Imports one or more EIP-2335 passwords into a Lighthouse VC directory, \ - requesting passwords interactively. The directory flag provides a convenient \ - method for importing a directory of keys generated by the eth2-deposit-cli \ - Python utility.", - ) - .arg( - Arg::new(KEYSTORE_FLAG) - .long(KEYSTORE_FLAG) - .value_name("KEYSTORE_PATH") - .help("Path to a single keystore to be imported.") - .conflicts_with(DIR_FLAG) - .required_unless_present(DIR_FLAG) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(DIR_FLAG) - .long(DIR_FLAG) - .value_name("KEYSTORES_DIRECTORY") - .help( - "Path to a directory which contains zero or more keystores \ - for import. This directory and all sub-directories will be \ - searched and any file name which contains 'keystore' and \ - has the '.json' extension will be attempted to be imported.", - ) - .conflicts_with(KEYSTORE_FLAG) - .required_unless_present(KEYSTORE_FLAG) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(REUSE_PASSWORD_FLAG) - .long(REUSE_PASSWORD_FLAG) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .help("If present, the same password will be used for all imported keystores.") - .display_order(0), - ) - .arg( - Arg::new(PASSWORD_FLAG) - .long(PASSWORD_FLAG) - .value_name("KEYSTORE_PASSWORD_PATH") - .requires(REUSE_PASSWORD_FLAG) - .help( - "The path to the file containing the password which will unlock all \ - keystores being imported. This flag must be used with `--reuse-password`. \ - The password will be copied to the `validator_definitions.yml` file, so after \ - import we strongly recommend you delete the file at KEYSTORE_PASSWORD_PATH.", - ) - .action(ArgAction::Set) - .display_order(0), - ) -} - -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { - let keystore: Option = clap_utils::parse_optional(matches, KEYSTORE_FLAG)?; - let keystores_dir: Option = clap_utils::parse_optional(matches, DIR_FLAG)?; +pub fn cli_run( + import_config: &Import, + matches: &ArgMatches, + validator_dir: PathBuf, +) -> Result<(), String> { + let keystore = import_config.keystore.clone(); + let keystores_dir = import_config.directory.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); - let reuse_password = matches.get_flag(REUSE_PASSWORD_FLAG); - let keystore_password_path: Option = - clap_utils::parse_optional(matches, PASSWORD_FLAG)?; + let reuse_password = import_config.reuse_password; + let keystore_password_path = import_config.password_file.clone(); let mut defs = ValidatorDefinitions::open_or_create(&validator_dir) .map_err(|e| format!("Unable to open {}: {:?}", CONFIG_FILENAME, e))?; diff --git a/account_manager/src/validator/list.rs b/account_manager/src/validator/list.rs index d082a49590b..52c108138c0 100644 --- a/account_manager/src/validator/list.rs +++ b/account_manager/src/validator/list.rs @@ -1,13 +1,6 @@ use account_utils::validator_definitions::ValidatorDefinitions; -use clap::Command; use std::path::PathBuf; -pub const CMD: &str = "list"; - -pub fn cli_app() -> Command { - Command::new(CMD).about("Lists the public keys of all validators.") -} - pub fn cli_run(validator_dir: PathBuf) -> Result<(), String> { let validator_definitions = ValidatorDefinitions::open(&validator_dir).map_err(|e| { format!( diff --git a/account_manager/src/validator/mod.rs b/account_manager/src/validator/mod.rs index b699301cde3..f1055e8e5e4 100644 --- a/account_manager/src/validator/mod.rs +++ b/account_manager/src/validator/mod.rs @@ -1,3 +1,4 @@ +pub mod cli; pub mod create; pub mod exit; pub mod import; @@ -6,63 +7,49 @@ pub mod modify; pub mod recover; pub mod slashing_protection; -use crate::{VALIDATOR_DIR_FLAG, VALIDATOR_DIR_FLAG_ALIAS}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use directory::{parse_path_or_default_with_flag, DEFAULT_VALIDATOR_DIR}; +use clap::ArgMatches; +use cli::Validator; +use directory::{parse_path_or_default_with_flag_v2, DEFAULT_VALIDATOR_DIR}; use environment::Environment; use std::path::PathBuf; use types::EthSpec; pub const CMD: &str = "validator"; -pub fn cli_app() -> Command { - Command::new(CMD) - .display_order(0) - .about("Provides commands for managing Eth2 validators.") - .arg( - Arg::new(VALIDATOR_DIR_FLAG) - .long(VALIDATOR_DIR_FLAG) - .alias(VALIDATOR_DIR_FLAG_ALIAS) - .value_name("VALIDATOR_DIRECTORY") - .help( - "The path to search for validator directories. \ - Defaults to ~/.lighthouse/{network}/validators", - ) - .action(ArgAction::Set) - .conflicts_with("datadir"), - ) - .subcommand(create::cli_app()) - .subcommand(modify::cli_app()) - .subcommand(import::cli_app()) - .subcommand(list::cli_app()) - .subcommand(recover::cli_app()) - .subcommand(slashing_protection::cli_app()) - .subcommand(exit::cli_app()) -} - -pub fn cli_run(matches: &ArgMatches, env: Environment) -> Result<(), String> { +pub fn cli_run( + validator_config: &Validator, + matches: &ArgMatches, + env: Environment, +) -> Result<(), String> { let validator_base_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_VALIDATOR_DIR) } else { - parse_path_or_default_with_flag(matches, VALIDATOR_DIR_FLAG, DEFAULT_VALIDATOR_DIR)? + parse_path_or_default_with_flag_v2( + matches, + validator_config.validator_dir.clone(), + DEFAULT_VALIDATOR_DIR, + )? }; eprintln!("validator-dir path: {:?}", validator_base_dir); - match matches.subcommand() { - Some((create::CMD, matches)) => create::cli_run::(matches, env, validator_base_dir), - Some((modify::CMD, matches)) => modify::cli_run(matches, validator_base_dir), - Some((import::CMD, matches)) => import::cli_run(matches, validator_base_dir), - Some((list::CMD, _)) => list::cli_run(validator_base_dir), - Some((recover::CMD, matches)) => recover::cli_run(matches, validator_base_dir), - Some((slashing_protection::CMD, matches)) => { - slashing_protection::cli_run(matches, env, validator_base_dir) + match &validator_config.subcommand { + cli::ValidatorSubcommand::Create(create_config) => { + create::cli_run::(create_config, matches, env, validator_base_dir) + } + cli::ValidatorSubcommand::Exit(exit_config) => exit::cli_run(exit_config, matches, env), + cli::ValidatorSubcommand::Import(import_config) => { + import::cli_run(import_config, matches, validator_base_dir) + } + cli::ValidatorSubcommand::List(_) => list::cli_run(validator_base_dir), + cli::ValidatorSubcommand::Recover(recover_config) => { + recover::cli_run(recover_config, matches, validator_base_dir) + } + cli::ValidatorSubcommand::Modify(modify_config) => { + modify::cli_run(modify_config, validator_base_dir) + } + cli::ValidatorSubcommand::SlashingProtection(slashing_protection_config) => { + slashing_protection::cli_run(slashing_protection_config, env, validator_base_dir) } - Some((exit::CMD, matches)) => exit::cli_run(matches, env), - Some((unknown, _)) => Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )), - _ => Err(format!("No command provided for {}. See --help", CMD)), } } diff --git a/account_manager/src/validator/modify.rs b/account_manager/src/validator/modify.rs index 571cd28bf5e..fad86c7e697 100644 --- a/account_manager/src/validator/modify.rs +++ b/account_manager/src/validator/modify.rs @@ -1,9 +1,9 @@ use account_utils::validator_definitions::ValidatorDefinitions; use bls::PublicKey; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; use std::{collections::HashSet, path::PathBuf}; +use super::cli::Modify; + pub const CMD: &str = "modify"; pub const ENABLE: &str = "enable"; pub const DISABLE: &str = "disable"; @@ -11,81 +11,28 @@ pub const DISABLE: &str = "disable"; pub const PUBKEY_FLAG: &str = "pubkey"; pub const ALL: &str = "all"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Modify validator status in validator_definitions.yml.") - .display_order(0) - .subcommand( - Command::new(ENABLE) - .about("Enable validator(s) in validator_definitions.yml.") - .arg( - Arg::new(PUBKEY_FLAG) - .long(PUBKEY_FLAG) - .value_name("PUBKEY") - .help("Validator pubkey to enable") - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(ALL) - .long(ALL) - .help("Enable all validators in the validator directory") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .conflicts_with(PUBKEY_FLAG) - .display_order(0), - ), - ) - .subcommand( - Command::new(DISABLE) - .about("Disable validator(s) in validator_definitions.yml.") - .arg( - Arg::new(PUBKEY_FLAG) - .long(PUBKEY_FLAG) - .value_name("PUBKEY") - .help("Validator pubkey to disable") - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(ALL) - .long(ALL) - .help("Disable all validators in the validator directory") - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .conflicts_with(PUBKEY_FLAG) - .display_order(0), - ), - ) -} - -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { +pub fn cli_run(modify_config: &Modify, validator_dir: PathBuf) -> Result<(), String> { // `true` implies we are setting `validator_definition.enabled = true` and // vice versa. - let (enabled, sub_matches) = match matches.subcommand() { - Some((ENABLE, sub_matches)) => (true, sub_matches), - Some((DISABLE, sub_matches)) => (false, sub_matches), - Some((unknown, _)) => { - return Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )) - } - _ => return Err(format!("No command provided for {}. See --help", CMD)), + let (enabled, pubkey_opt, is_all) = match modify_config { + Modify::Enable(enable) => (true, enable.pubkey.clone(), enable.all), + Modify::Disable(disable) => (false, disable.pubkey.clone(), disable.all), }; + let mut defs = ValidatorDefinitions::open(&validator_dir).map_err(|e| { format!( "No validator definitions found in {:?}: {:?}", validator_dir, e ) })?; - let pubkeys_to_modify = if sub_matches.get_flag(ALL) { + + let pubkeys_to_modify = if is_all { defs.as_slice() .iter() .map(|def| def.voting_public_key.clone()) .collect::>() } else { - let public_key: PublicKey = clap_utils::parse_required(sub_matches, PUBKEY_FLAG)?; + let public_key = pubkey_opt.ok_or_else(|| "Pubkey flag must be provided.".to_string())?; std::iter::once(public_key).collect::>() }; diff --git a/account_manager/src/validator/recover.rs b/account_manager/src/validator/recover.rs index 19d161a468f..e2e65655a7a 100644 --- a/account_manager/src/validator/recover.rs +++ b/account_manager/src/validator/recover.rs @@ -1,92 +1,33 @@ -use super::create::STORE_WITHDRAW_FLAG; -use crate::validator::create::COUNT_FLAG; -use crate::SECRETS_DIR_FLAG; use account_utils::eth2_keystore::{keypair_from_secret, Keystore, KeystoreBuilder}; use account_utils::{random_password, read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use clap_utils::FLAG_HEADER; -use directory::{parse_path_or_default_with_flag, DEFAULT_SECRET_DIR}; +use clap::ArgMatches; +use directory::{parse_path_or_default_with_flag_v2, DEFAULT_SECRET_DIR}; use eth2_wallet::bip39::Seed; use eth2_wallet::{recover_validator_secret_from_mnemonic, KeyType, ValidatorKeystores}; use std::fs::create_dir_all; use std::path::PathBuf; use validator_dir::Builder as ValidatorDirBuilder; -pub const CMD: &str = "recover"; -pub const FIRST_INDEX_FLAG: &str = "first-index"; -pub const MNEMONIC_FLAG: &str = "mnemonic-path"; - -pub fn cli_app() -> Command { - Command::new(CMD) - .about( - "Recovers validator private keys given a BIP-39 mnemonic phrase. \ - If you did not specify a `--first-index` or count `--count`, by default this will \ - only recover the keys associated with the validator at index 0 for an HD wallet \ - in accordance with the EIP-2333 spec.") - .arg( - Arg::new(FIRST_INDEX_FLAG) - .long(FIRST_INDEX_FLAG) - .value_name("FIRST_INDEX") - .help("The first of consecutive key indexes you wish to recover.") - .action(ArgAction::Set) - .required(false) - .default_value("0") - .display_order(0) - ) - .arg( - Arg::new(COUNT_FLAG) - .long(COUNT_FLAG) - .value_name("COUNT") - .help("The number of validator keys you wish to recover. Counted consecutively from the provided `--first_index`.") - .action(ArgAction::Set) - .required(false) - .default_value("1") - .display_order(0) - ) - .arg( - Arg::new(MNEMONIC_FLAG) - .long(MNEMONIC_FLAG) - .value_name("MNEMONIC_PATH") - .help( - "If present, the mnemonic will be read in from this file.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(SECRETS_DIR_FLAG) - .long(SECRETS_DIR_FLAG) - .value_name("SECRETS_DIR") - .help( - "The path where the validator keystore passwords will be stored. \ - Defaults to ~/.lighthouse/{network}/secrets", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(STORE_WITHDRAW_FLAG) - .long(STORE_WITHDRAW_FLAG) - .help( - "If present, the withdrawal keystore will be stored alongside the voting \ - keypair. It is generally recommended to *not* store the withdrawal key and \ - instead generate them from the wallet seed when required.", - ) - .action(ArgAction::SetTrue) - .help_heading(FLAG_HEADER) - .display_order(0) - ) -} -pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), String> { +use super::cli::Recover; + +pub fn cli_run( + recover_config: &Recover, + matches: &ArgMatches, + validator_dir: PathBuf, +) -> Result<(), String> { let secrets_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_SECRET_DIR) } else { - parse_path_or_default_with_flag(matches, SECRETS_DIR_FLAG, DEFAULT_SECRET_DIR)? + parse_path_or_default_with_flag_v2( + matches, + recover_config.secrets_dir.clone(), + DEFAULT_SECRET_DIR, + )? }; - let first_index: u32 = clap_utils::parse_required(matches, FIRST_INDEX_FLAG)?; - let count: u32 = clap_utils::parse_required(matches, COUNT_FLAG)?; - let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; + let first_index = recover_config.first_index; + let count = recover_config.count; + let mnemonic_path = recover_config.mnemonic_path.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); eprintln!("secrets-dir path: {:?}", secrets_dir); @@ -133,7 +74,7 @@ pub fn cli_run(matches: &ArgMatches, validator_dir: PathBuf) -> Result<(), Strin .password_dir(secrets_dir.clone()) .voting_keystore(keystores.voting, voting_password.as_bytes()) .withdrawal_keystore(keystores.withdrawal, withdrawal_password.as_bytes()) - .store_withdrawal_keystore(matches.get_flag(STORE_WITHDRAW_FLAG)) + .store_withdrawal_keystore(recover_config.store_withdrawal_keystore) .build() .map_err(|e| format!("Unable to build validator directory: {:?}", e))?; diff --git a/account_manager/src/validator/slashing_protection.rs b/account_manager/src/validator/slashing_protection.rs index bcd860a4847..108900a9bc8 100644 --- a/account_manager/src/validator/slashing_protection.rs +++ b/account_manager/src/validator/slashing_protection.rs @@ -1,4 +1,3 @@ -use clap::{Arg, ArgAction, ArgMatches, Command}; use environment::Environment; use slashing_protection::{ interchange::Interchange, InterchangeError, InterchangeImportOutcome, SlashingDatabase, @@ -9,56 +8,14 @@ use std::path::PathBuf; use std::str::FromStr; use types::{Epoch, EthSpec, PublicKeyBytes, Slot}; -pub const CMD: &str = "slashing-protection"; +use super::cli::SlashingProtection; + pub const IMPORT_CMD: &str = "import"; pub const EXPORT_CMD: &str = "export"; - -pub const IMPORT_FILE_ARG: &str = "IMPORT-FILE"; -pub const EXPORT_FILE_ARG: &str = "EXPORT-FILE"; - pub const PUBKEYS_FLAG: &str = "pubkeys"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Import or export slashing protection data to or from another client") - .display_order(0) - .subcommand( - Command::new(IMPORT_CMD) - .about("Import an interchange file") - .arg( - Arg::new(IMPORT_FILE_ARG) - .action(ArgAction::Set) - .value_name("FILE") - .display_order(0) - .help("The slashing protection interchange file to import (.json)"), - ) - ) - .subcommand( - Command::new(EXPORT_CMD) - .about("Export an interchange file") - .arg( - Arg::new(EXPORT_FILE_ARG) - .action(ArgAction::Set) - .value_name("FILE") - .help("The filename to export the interchange file to") - .display_order(0) - ) - .arg( - Arg::new(PUBKEYS_FLAG) - .long(PUBKEYS_FLAG) - .action(ArgAction::Set) - .value_name("PUBKEYS") - .help( - "List of public keys to export history for. Keys should be 0x-prefixed, \ - comma-separated. All known keys will be exported if omitted", - ) - .display_order(0) - ) - ) -} - pub fn cli_run( - matches: &ArgMatches, + slashing_protection_config: &SlashingProtection, env: Environment, validator_base_dir: PathBuf, ) -> Result<(), String> { @@ -71,9 +28,9 @@ pub fn cli_run( .genesis_validators_root::()? .ok_or_else(|| "Unable to get genesis state, has genesis occurred?".to_string())?; - match matches.subcommand() { - Some((IMPORT_CMD, matches)) => { - let import_filename: PathBuf = clap_utils::parse_required(matches, IMPORT_FILE_ARG)?; + match slashing_protection_config { + SlashingProtection::Import(import_config) => { + let import_filename = import_config.import_file.clone(); let import_file = File::open(&import_filename).map_err(|e| { format!( "Unable to open import file at {}: {:?}", @@ -172,15 +129,13 @@ pub fn cli_run( Ok(()) } - Some((EXPORT_CMD, matches)) => { - let export_filename: PathBuf = clap_utils::parse_required(matches, EXPORT_FILE_ARG)?; + SlashingProtection::Export(export_config) => { + let export_filename = export_config.export_file.clone(); - let selected_pubkeys = if let Some(pubkeys) = - clap_utils::parse_optional::(matches, PUBKEYS_FLAG)? - { + let selected_pubkeys = if let Some(pubkeys) = export_config.pubkeys.clone() { let pubkeys = pubkeys - .split(',') - .map(PublicKeyBytes::from_str) + .iter() + .map(|s| PublicKeyBytes::from_str(s)) .collect::, _>>() .map_err(|e| format!("Invalid --{} value: {:?}", PUBKEYS_FLAG, e))?; Some(pubkeys) @@ -219,7 +174,5 @@ pub fn cli_run( Ok(()) } - Some((command, _)) => Err(format!("No such subcommand `{}`", command)), - _ => Err("No subcommand provided, see --help for options".to_string()), } } diff --git a/account_manager/src/wallet/cli.rs b/account_manager/src/wallet/cli.rs new file mode 100644 index 00000000000..e4c2f81958b --- /dev/null +++ b/account_manager/src/wallet/cli.rs @@ -0,0 +1,204 @@ +use crate::common::read_wallet_name_from_cli; +use crate::wallet::create::read_new_wallet_password_from_cli; +use crate::WALLETS_DIR_FLAG; +use account_utils::{random_password, PlainText, STDIN_INPUTS_FLAG}; +use clap::ArgMatches; +pub use clap::Parser; +use eth2_wallet::bip39::Mnemonic; +use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType}; +use filesystem::create_with_600_perms; +use serde::{Deserialize, Serialize}; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use super::create::validate_mnemonic_length; + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Manage wallets, from which validator keys can be derived.")] +pub struct Wallet { + #[clap( + long, + value_name = "WALLETS_DIRECTORY", + conflicts_with = "datadir", + help = "A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets" + )] + pub wallets_dir: Option, + #[clap(subcommand)] + pub subcommand: WalletSubcommand, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +pub enum WalletSubcommand { + Create(Create), + List(List), + Recover(Recover), +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Creates a new HD (hierarchical-deterministic) EIP-2386 wallet.")] +pub struct Create { + #[clap( + long, + value_name = "WALLET_NAME", + help = "The wallet will be created with this name. It is not allowed to \ + create two wallets with the same name for the same --base-dir." + )] + pub name: Option, + + #[clap( + long, + value_name = "WALLET_PASSWORD_PATH", + help = "A path to a file containing the password which will unlock the wallet. \ + If the file does not exist, a random password will be generated and \ + saved at that path. To avoid confusion, if the file does not already \ + exist it must include a '.pass' suffix." + )] + pub password_file: Option, + + #[clap( + long = "type", + value_name = "WALLET_TYPE", + value_enum, + default_value_t = WalletType::Hd, + help = "The type of wallet to create. Only HD (hierarchical-deterministic) \ + wallets are supported presently..", + )] + pub r#type: WalletType, + + #[clap( + long, + value_name = "MNEMONIC_PATH", + help = "If present, the mnemonic will be saved to this file. DO NOT SHARE THE MNEMONIC." + )] + pub mnemonic_output_path: Option, + + #[clap( + long, + value_parser = validate_mnemonic_length, + default_value_t = 24, + value_name = "MNEMONIC_LENGTH", + help = "The number of words to use for the mnemonic phrase.", + )] + pub mnemonic_length: usize, +} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Lists the names of all wallets.")] +pub struct List {} + +#[derive(Parser, Clone, Deserialize, Serialize, Debug)] +#[clap(about = "Recovers an EIP-2386 wallet from a given a BIP-39 mnemonic phrase.")] +pub struct Recover { + #[clap( + long, + value_name = "WALLET_NAME", + help = "The wallet will be created with this name. It is not allowed to \ + create two wallets with the same name for the same --base-dir." + )] + pub name: Option, + + #[clap( + long, + value_name = "PASSWORD_FILE_PATH", + help = "This will be the new password for your recovered wallet. \ + A path to a file containing the password which will unlock the wallet. \ + If the file does not exist, a random password will be generated and \ + saved at that path. To avoid confusion, if the file does not already \ + exist it must include a '.pass' suffix." + )] + pub password_file: Option, + + #[clap( + long, + value_name = "MNEMONIC_PATH", + help = "If present, the mnemonic will be read in from this file." + )] + pub mnemonic_path: Option, + + #[clap( + long = "type", + value_name = "WALLET_TYPE", + value_enum, + default_value_t = WalletType::Hd, + help = "The type of wallet to create. Only HD (hierarchical-deterministic) \ + wallets are supported presently..", + )] + pub r#type: WalletType, +} + +pub trait NewWallet { + fn get_name(&self) -> Option; + fn get_password(&self) -> Option; + fn get_type(&self) -> WalletType; + fn create_wallet_from_mnemonic( + &self, + wallet_base_dir: &Path, + matches: &ArgMatches, + mnemonic: &Mnemonic, + ) -> Result { + let name: Option = self.get_name(); + let wallet_password_path: Option = self.get_password(); + let wallet_type: WalletType = self.get_type(); + let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); + + let mgr = WalletManager::open(wallet_base_dir) + .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; + + let wallet_password: PlainText = match wallet_password_path { + Some(path) => { + // Create a random password if the file does not exist. + if !path.exists() { + // To prevent users from accidentally supplying their password to the PASSWORD_FLAG and + // create a file with that name, we require that the password has a .pass suffix. + if path.extension() != Some(OsStr::new("pass")) { + return Err(format!( + "Only creates a password file if that file ends in .pass: {:?}", + path + )); + } + + create_with_600_perms(&path, random_password().as_bytes()) + .map_err(|e| format!("Unable to write to {:?}: {:?}", path, e))?; + } + read_new_wallet_password_from_cli(Some(path), stdin_inputs)? + } + None => read_new_wallet_password_from_cli(None, stdin_inputs)?, + }; + + let wallet_name = read_wallet_name_from_cli(name, stdin_inputs)?; + + let wallet = mgr + .create_wallet( + wallet_name, + wallet_type, + mnemonic, + wallet_password.as_bytes(), + ) + .map_err(|e| format!("Unable to create wallet: {:?}", e))?; + Ok(wallet) + } +} + +impl NewWallet for Create { + fn get_name(&self) -> Option { + self.name.clone() + } + fn get_password(&self) -> Option { + self.password_file.clone() + } + fn get_type(&self) -> WalletType { + self.r#type + } +} + +impl NewWallet for Recover { + fn get_name(&self) -> Option { + self.name.clone() + } + fn get_password(&self) -> Option { + self.password_file.clone() + } + fn get_type(&self) -> WalletType { + self.r#type + } +} diff --git a/account_manager/src/wallet/create.rs b/account_manager/src/wallet/create.rs index 6369646929a..80ffa3c29ba 100644 --- a/account_manager/src/wallet/create.rs +++ b/account_manager/src/wallet/create.rs @@ -1,19 +1,18 @@ -use crate::common::read_wallet_name_from_cli; -use crate::WALLETS_DIR_FLAG; use account_utils::{ - is_password_sufficiently_complex, random_password, read_password_from_user, strip_off_newlines, - STDIN_INPUTS_FLAG, + is_password_sufficiently_complex, read_password_from_user, strip_off_newlines, }; -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; use eth2_wallet::{ bip39::{Language, Mnemonic, MnemonicType}, PlainText, }; -use eth2_wallet_manager::{LockedWallet, WalletManager, WalletType}; use filesystem::create_with_600_perms; -use std::ffi::OsStr; use std::fs; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; + +use crate::wallet::cli::NewWallet; + +use super::cli::Create; pub const CMD: &str = "create"; pub const HD_TYPE: &str = "hd"; @@ -22,6 +21,10 @@ pub const PASSWORD_FLAG: &str = "password-file"; pub const TYPE_FLAG: &str = "type"; pub const MNEMONIC_FLAG: &str = "mnemonic-output-path"; pub const MNEMONIC_LENGTH_FLAG: &str = "mnemonic-length"; +pub const NEW_WALLET_PASSWORD_PROMPT: &str = + "Enter a password for your new wallet that is at least 12 characters long:"; +pub const RETYPE_PASSWORD_PROMPT: &str = "Please re-enter your wallet's new password:"; + pub const MNEMONIC_TYPES: &[MnemonicType] = &[ MnemonicType::Words12, MnemonicType::Words15, @@ -29,101 +32,25 @@ pub const MNEMONIC_TYPES: &[MnemonicType] = &[ MnemonicType::Words21, MnemonicType::Words24, ]; -pub const NEW_WALLET_PASSWORD_PROMPT: &str = - "Enter a password for your new wallet that is at least 12 characters long:"; -pub const RETYPE_PASSWORD_PROMPT: &str = "Please re-enter your wallet's new password:"; - -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Creates a new HD (hierarchical-deterministic) EIP-2386 wallet.") - .arg( - Arg::new(NAME_FLAG) - .long(NAME_FLAG) - .value_name("WALLET_NAME") - .help( - "The wallet will be created with this name. It is not allowed to \ - create two wallets with the same name for the same --base-dir.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(PASSWORD_FLAG) - .long(PASSWORD_FLAG) - .value_name("WALLET_PASSWORD_PATH") - .help( - "A path to a file containing the password which will unlock the wallet. \ - If the file does not exist, a random password will be generated and \ - saved at that path. To avoid confusion, if the file does not already \ - exist it must include a '.pass' suffix.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(TYPE_FLAG) - .long(TYPE_FLAG) - .value_name("WALLET_TYPE") - .help( - "The type of wallet to create. Only HD (hierarchical-deterministic) \ - wallets are supported presently..", - ) - .action(ArgAction::Set) - .value_parser([HD_TYPE]) - .default_value(HD_TYPE) - .display_order(0) - ) - .arg( - Arg::new(MNEMONIC_FLAG) - .long(MNEMONIC_FLAG) - .value_name("MNEMONIC_PATH") - .help( - "If present, the mnemonic will be saved to this file. DO NOT SHARE THE MNEMONIC.", - ) - .action(ArgAction::Set) - .display_order(0) - ) - .arg( - Arg::new(MNEMONIC_LENGTH_FLAG) - .long(MNEMONIC_LENGTH_FLAG) - .value_name("MNEMONIC_LENGTH") - .help("The number of words to use for the mnemonic phrase.") - .action(ArgAction::Set) - .value_parser(|len: &str| { - match len - .parse::() - .ok() - .and_then(|words| MnemonicType::for_word_count(words).ok()) - { - Some(_) => Ok(len.to_string()), - None => Err(format!( - "Mnemonic length must be one of {}", - MNEMONIC_TYPES - .iter() - .map(|t| t.word_count().to_string()) - .collect::>() - .join(", ") - )), - } - }) - .default_value("24") - .display_order(0) - ) -} -pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { - let mnemonic_output_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; +pub fn cli_run( + create_config: &Create, + matches: &ArgMatches, + wallet_base_dir: PathBuf, +) -> Result<(), String> { + let mnemonic_output_path = create_config.mnemonic_output_path.clone(); // Create a new random mnemonic. // // The `tiny-bip39` crate uses `thread_rng()` for this entropy. - let mnemonic_length = clap_utils::parse_required(matches, MNEMONIC_LENGTH_FLAG)?; + let mnemonic_length = create_config.mnemonic_length; let mnemonic = Mnemonic::new( MnemonicType::for_word_count(mnemonic_length).expect("Mnemonic length already validated"), Language::English, ); - let wallet = create_wallet_from_mnemonic(matches, wallet_base_dir.as_path(), &mnemonic)?; + let wallet = + create_config.create_wallet_from_mnemonic(wallet_base_dir.as_path(), matches, &mnemonic)?; if let Some(path) = mnemonic_output_path { create_with_600_perms(&path, mnemonic.phrase().as_bytes()) @@ -154,57 +81,6 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str Ok(()) } -pub fn create_wallet_from_mnemonic( - matches: &ArgMatches, - wallet_base_dir: &Path, - mnemonic: &Mnemonic, -) -> Result { - let name: Option = clap_utils::parse_optional(matches, NAME_FLAG)?; - let wallet_password_path: Option = clap_utils::parse_optional(matches, PASSWORD_FLAG)?; - let type_field: String = clap_utils::parse_required(matches, TYPE_FLAG)?; - let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); - let wallet_type = match type_field.as_ref() { - HD_TYPE => WalletType::Hd, - unknown => return Err(format!("--{} {} is not supported", TYPE_FLAG, unknown)), - }; - - let mgr = WalletManager::open(wallet_base_dir) - .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; - - let wallet_password: PlainText = match wallet_password_path { - Some(path) => { - // Create a random password if the file does not exist. - if !path.exists() { - // To prevent users from accidentally supplying their password to the PASSWORD_FLAG and - // create a file with that name, we require that the password has a .pass suffix. - if path.extension() != Some(OsStr::new("pass")) { - return Err(format!( - "Only creates a password file if that file ends in .pass: {:?}", - path - )); - } - - create_with_600_perms(&path, random_password().as_bytes()) - .map_err(|e| format!("Unable to write to {:?}: {:?}", path, e))?; - } - read_new_wallet_password_from_cli(Some(path), stdin_inputs)? - } - None => read_new_wallet_password_from_cli(None, stdin_inputs)?, - }; - - let wallet_name = read_wallet_name_from_cli(name, stdin_inputs)?; - - let wallet = mgr - .create_wallet( - wallet_name, - wallet_type, - mnemonic, - wallet_password.as_bytes(), - ) - .map_err(|e| format!("Unable to create wallet: {:?}", e))?; - Ok(wallet) -} - /// Used when a user is creating a new wallet. Read in a wallet password from a file if the password file /// path is provided. Otherwise, read from an interactive prompt using tty unless the `--stdin-inputs` /// flag is provided. This verifies the password complexity and verifies the password is correctly re-entered. @@ -245,3 +121,23 @@ pub fn read_new_wallet_password_from_cli( }, } } + +pub fn validate_mnemonic_length(len: &str) -> Result { + match len.parse::().ok().and_then(|words| { + if MnemonicType::for_word_count(words).is_ok() { + Some(words) + } else { + None + } + }) { + Some(words) => Ok(words), + None => Err(format!( + "Mnemonic length must be one of {}", + MNEMONIC_TYPES + .iter() + .map(|t| t.word_count().to_string()) + .collect::>() + .join(", ") + )), + } +} diff --git a/account_manager/src/wallet/list.rs b/account_manager/src/wallet/list.rs index a551ffae128..4cb29231cd8 100644 --- a/account_manager/src/wallet/list.rs +++ b/account_manager/src/wallet/list.rs @@ -1,14 +1,9 @@ use crate::WALLETS_DIR_FLAG; -use clap::Command; use eth2_wallet_manager::WalletManager; use std::path::PathBuf; pub const CMD: &str = "list"; -pub fn cli_app() -> Command { - Command::new(CMD).about("Lists the names of all wallets.") -} - pub fn cli_run(wallet_base_dir: PathBuf) -> Result<(), String> { let mgr = WalletManager::open(wallet_base_dir) .map_err(|e| format!("Unable to open --{}: {:?}", WALLETS_DIR_FLAG, e))?; diff --git a/account_manager/src/wallet/mod.rs b/account_manager/src/wallet/mod.rs index f6f3bb0419a..976f5f639e3 100644 --- a/account_manager/src/wallet/mod.rs +++ b/account_manager/src/wallet/mod.rs @@ -1,51 +1,38 @@ +pub mod cli; pub mod create; pub mod list; pub mod recover; -use crate::WALLETS_DIR_FLAG; -use clap::{Arg, ArgAction, ArgMatches, Command}; -use directory::{parse_path_or_default_with_flag, DEFAULT_WALLET_DIR}; +use clap::ArgMatches; +use cli::Wallet; +use directory::{parse_path_or_default_with_flag_v2, DEFAULT_WALLET_DIR}; use std::fs::create_dir_all; use std::path::PathBuf; pub const CMD: &str = "wallet"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Manage wallets, from which validator keys can be derived.") - .display_order(0) - .arg( - Arg::new(WALLETS_DIR_FLAG) - .long(WALLETS_DIR_FLAG) - .value_name("WALLETS_DIRECTORY") - .help("A path containing Eth2 EIP-2386 wallets. Defaults to ~/.lighthouse/{network}/wallets") - .action(ArgAction::Set) - .conflicts_with("datadir"), - ) - .subcommand(create::cli_app()) - .subcommand(list::cli_app()) - .subcommand(recover::cli_app()) -} - -pub fn cli_run(matches: &ArgMatches) -> Result<(), String> { +pub fn cli_run(wallet_config: &Wallet, matches: &ArgMatches) -> Result<(), String> { let wallet_base_dir = if matches.get_one::("datadir").is_some() { let path: PathBuf = clap_utils::parse_required(matches, "datadir")?; path.join(DEFAULT_WALLET_DIR) } else { - parse_path_or_default_with_flag(matches, WALLETS_DIR_FLAG, DEFAULT_WALLET_DIR)? + parse_path_or_default_with_flag_v2( + matches, + wallet_config.wallets_dir.clone(), + DEFAULT_WALLET_DIR, + )? }; create_dir_all(&wallet_base_dir).map_err(|_| "Could not create wallet base dir")?; eprintln!("wallet-dir path: {:?}", wallet_base_dir); - match matches.subcommand() { - Some((create::CMD, matches)) => create::cli_run(matches, wallet_base_dir), - Some((list::CMD, _)) => list::cli_run(wallet_base_dir), - Some((recover::CMD, matches)) => recover::cli_run(matches, wallet_base_dir), - Some((unknown, _)) => Err(format!( - "{} does not have a {} command. See --help", - CMD, unknown - )), - _ => Err("No subcommand provided, see --help for options".to_string()), + match &wallet_config.subcommand { + cli::WalletSubcommand::Create(create_config) => { + create::cli_run(create_config, matches, wallet_base_dir) + } + cli::WalletSubcommand::List(_) => list::cli_run(wallet_base_dir), + cli::WalletSubcommand::Recover(recover_config) => { + recover::cli_run(recover_config, matches, wallet_base_dir) + } } } diff --git a/account_manager/src/wallet/recover.rs b/account_manager/src/wallet/recover.rs index 766d5dbe0cb..38873729f9f 100644 --- a/account_manager/src/wallet/recover.rs +++ b/account_manager/src/wallet/recover.rs @@ -1,65 +1,19 @@ -use crate::wallet::create::create_wallet_from_mnemonic; -use crate::wallet::create::{HD_TYPE, NAME_FLAG, PASSWORD_FLAG, TYPE_FLAG}; +use crate::wallet::cli::NewWallet; use account_utils::{read_mnemonic_from_cli, STDIN_INPUTS_FLAG}; -use clap::{Arg, ArgAction, ArgMatches, Command}; +use clap::ArgMatches; use std::path::PathBuf; +use super::cli::Recover; + pub const CMD: &str = "recover"; pub const MNEMONIC_FLAG: &str = "mnemonic-path"; -pub fn cli_app() -> Command { - Command::new(CMD) - .about("Recovers an EIP-2386 wallet from a given a BIP-39 mnemonic phrase.") - .arg( - Arg::new(NAME_FLAG) - .long(NAME_FLAG) - .value_name("WALLET_NAME") - .help( - "The wallet will be created with this name. It is not allowed to \ - create two wallets with the same name for the same --base-dir.", - ) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(PASSWORD_FLAG) - .long(PASSWORD_FLAG) - .value_name("PASSWORD_FILE_PATH") - .help( - "This will be the new password for your recovered wallet. \ - A path to a file containing the password which will unlock the wallet. \ - If the file does not exist, a random password will be generated and \ - saved at that path. To avoid confusion, if the file does not already \ - exist it must include a '.pass' suffix.", - ) - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(MNEMONIC_FLAG) - .long(MNEMONIC_FLAG) - .value_name("MNEMONIC_PATH") - .help("If present, the mnemonic will be read in from this file.") - .action(ArgAction::Set) - .display_order(0), - ) - .arg( - Arg::new(TYPE_FLAG) - .long(TYPE_FLAG) - .value_name("WALLET_TYPE") - .help( - "The type of wallet to create. Only HD (hierarchical-deterministic) \ - wallets are supported presently..", - ) - .action(ArgAction::Set) - .value_parser([HD_TYPE]) - .default_value(HD_TYPE) - .display_order(0), - ) -} - -pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), String> { - let mnemonic_path: Option = clap_utils::parse_optional(matches, MNEMONIC_FLAG)?; +pub fn cli_run( + recover_config: &Recover, + matches: &ArgMatches, + wallet_base_dir: PathBuf, +) -> Result<(), String> { + let mnemonic_path = recover_config.mnemonic_path.clone(); let stdin_inputs = cfg!(windows) || matches.get_flag(STDIN_INPUTS_FLAG); eprintln!(); @@ -68,7 +22,8 @@ pub fn cli_run(matches: &ArgMatches, wallet_base_dir: PathBuf) -> Result<(), Str let mnemonic = read_mnemonic_from_cli(mnemonic_path, stdin_inputs)?; - let wallet = create_wallet_from_mnemonic(matches, wallet_base_dir.as_path(), &mnemonic) + let wallet = recover_config + .create_wallet_from_mnemonic(wallet_base_dir.as_path(), matches, &mnemonic) .map_err(|e| format!("Unable to create wallet: {:?}", e))?; println!("Your wallet has been successfully recovered."); diff --git a/common/clap_utils/src/lib.rs b/common/clap_utils/src/lib.rs index a4b5f4dc1c4..d5f1a913819 100644 --- a/common/clap_utils/src/lib.rs +++ b/common/clap_utils/src/lib.rs @@ -57,6 +57,20 @@ pub fn parse_hardcoded_network( Eth2NetworkConfig::constant(network_name.as_str()) } +/// If `path` is `Some`, return it, else return the default path +pub fn parse_path_with_default_in_home_dir_v2( + path: Option, + default: PathBuf, +) -> Result { + if let Some(p) = path { + Ok(p) + } else { + dirs::home_dir() + .map(|home| home.join(default)) + .ok_or_else(|| "Unable to locate home directory.".to_string()) + } +} + /// If `name` is in `matches`, parses the value as a path. Otherwise, attempts to find the user's /// home directory and appends `default` to it. pub fn parse_path_with_default_in_home_dir( diff --git a/common/directory/src/lib.rs b/common/directory/src/lib.rs index d042f8dfadc..b1a10f4c6f2 100644 --- a/common/directory/src/lib.rs +++ b/common/directory/src/lib.rs @@ -62,6 +62,24 @@ pub fn parse_path_or_default_with_flag( ) } +/// If `dir` is `Some`, return it. +/// +/// Otherwise, construct the default directory for the `testnet` from the `matches` +/// and append `flag` to it. +pub fn parse_path_or_default_with_flag_v2( + matches: &ArgMatches, + dir: Option, + flag: &str, +) -> Result { + clap_utils::parse_path_with_default_in_home_dir_v2( + dir, + PathBuf::new() + .join(DEFAULT_ROOT_DIR) + .join(get_network_dir(matches)) + .join(flag), + ) +} + /// Get the approximate size of a directory and its contents. /// /// Will skip unreadable files, and files. Not 100% accurate if files are being created and deleted diff --git a/common/eth2_wallet_manager/Cargo.toml b/common/eth2_wallet_manager/Cargo.toml index a6eb24c78c2..78fab5515d3 100644 --- a/common/eth2_wallet_manager/Cargo.toml +++ b/common/eth2_wallet_manager/Cargo.toml @@ -6,8 +6,10 @@ edition = { workspace = true } # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clap = { workspace = true, features = ["derive"] } eth2_wallet = { workspace = true } lockfile = { workspace = true } +serde = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/common/eth2_wallet_manager/src/wallet_manager.rs b/common/eth2_wallet_manager/src/wallet_manager.rs index c988ca4135e..4d70665f261 100644 --- a/common/eth2_wallet_manager/src/wallet_manager.rs +++ b/common/eth2_wallet_manager/src/wallet_manager.rs @@ -2,8 +2,10 @@ use crate::{ filesystem::{create, Error as FilesystemError}, LockedWallet, }; +use clap::ValueEnum; use eth2_wallet::{bip39::Mnemonic, Error as WalletError, Uuid, Wallet, WalletBuilder}; use lockfile::LockfileError; +use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::ffi::OsString; use std::fs::{create_dir_all, read_dir, File}; @@ -54,6 +56,7 @@ impl From for Error { /// Defines the type of an EIP-2386 wallet. /// /// Presently only `Hd` wallets are supported. +#[derive(Copy, Clone, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize, Debug, ValueEnum)] pub enum WalletType { /// Hierarchical-deterministic. Hd, diff --git a/lighthouse/src/cli.rs b/lighthouse/src/cli.rs index ed665d2a479..589b497fa36 100644 --- a/lighthouse/src/cli.rs +++ b/lighthouse/src/cli.rs @@ -1,3 +1,4 @@ +use account_manager::AccountManager; use clap::Parser; use database_manager::cli::DatabaseManager; use serde::{Deserialize, Serialize}; @@ -5,8 +6,10 @@ use validator_client::cli::ValidatorClient; #[derive(Parser, Clone, Deserialize, Serialize, Debug)] pub enum LighthouseSubcommands { - #[clap(name = "database_manager")] - DatabaseManager(Box), + #[clap(name = "database_manager", display_order = 0)] + DatabaseManager(DatabaseManager), + #[clap(name = "account_manager", display_order = 0)] + AccountManager(AccountManager), #[clap(name = "validator_client")] ValidatorClient(Box), } diff --git a/lighthouse/src/main.rs b/lighthouse/src/main.rs index bbd8f764e7c..134a38c71b2 100644 --- a/lighthouse/src/main.rs +++ b/lighthouse/src/main.rs @@ -429,7 +429,6 @@ fn main() { ) .subcommand(beacon_node::cli_app()) .subcommand(boot_node::cli_app()) - .subcommand(account_manager::cli_app()) .subcommand(validator_manager::cli_app()); let cli = LighthouseSubcommands::augment_subcommands(cli); @@ -742,18 +741,8 @@ fn run( (Some(_), Some(_)) => panic!("CLI prevents both --network and --testnet-dir"), }; - if let Some(sub_matches) = matches.subcommand_matches(account_manager::CMD) { - eprintln!("Running account manager for {} network", network_name); - // Pass the entire `environment` to the account manager so it can run blocking operations. - account_manager::run(sub_matches, environment)?; - - // Exit as soon as account manager returns control. - return Ok(()); - } - if let Some(sub_matches) = matches.subcommand_matches(validator_manager::CMD) { eprintln!("Running validator manager for {} network", network_name); - // Pass the entire `environment` to the account manager so it can run blocking operations. validator_manager::run::(sub_matches, environment)?; @@ -762,6 +751,14 @@ fn run( } match LighthouseSubcommands::from_arg_matches(matches) { + Ok(LighthouseSubcommands::AccountManager(account_manager_config)) => { + eprintln!("Running account manager for {} network", network_name); + // Pass the entire `environment` to the account manager so it can run blocking operations. + account_manager::run(matches, &account_manager_config, environment)?; + + // Exit as soon as account manager returns control. + return Ok(()); + } Ok(LighthouseSubcommands::DatabaseManager(db_manager_config)) => { info!("Running database manager for {} network", network_name); database_manager::run(matches, &db_manager_config, environment)?;