From 618d8cf2a6eaf638615b63ab598b784cea8352cb Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Sat, 13 May 2023 14:30:03 +0900 Subject: [PATCH] [clap-v3-utils, keygen] Refactor mnemonics, derivation path, and argument parsing clap logic in keygen (#30977) * move mnemonic clap logic from keygen to clap-v3-utils * move derivation path clap logic from keygen to clap-v3-utils * move keygen clap logic from keygen to clap-v3-utils * resolve error from rebase * resolve conflict from rebase * reorganize derivation_path and mnemonic as modules inside keygen --- clap-v3-utils/src/keygen/derivation_path.rs | 34 ++++ clap-v3-utils/src/keygen/mnemonic.rs | 101 ++++++++++++ clap-v3-utils/src/keygen/mod.rs | 47 ++++++ clap-v3-utils/src/lib.rs | 1 + keygen/src/keygen.rs | 173 ++------------------ 5 files changed, 195 insertions(+), 161 deletions(-) create mode 100644 clap-v3-utils/src/keygen/derivation_path.rs create mode 100644 clap-v3-utils/src/keygen/mnemonic.rs create mode 100644 clap-v3-utils/src/keygen/mod.rs diff --git a/clap-v3-utils/src/keygen/derivation_path.rs b/clap-v3-utils/src/keygen/derivation_path.rs new file mode 100644 index 0000000000..9c57323d0d --- /dev/null +++ b/clap-v3-utils/src/keygen/derivation_path.rs @@ -0,0 +1,34 @@ +use { + clap::{Arg, ArgMatches}, + solana_sdk::derivation_path::DerivationPath, + std::error, +}; + +pub const DEFAULT_DERIVATION_PATH: &str = "m/44'/501'/0'/0'"; + +pub fn derivation_path_arg<'a>() -> Arg<'a> { + Arg::new("derivation_path") + .long("derivation-path") + .value_name("DERIVATION_PATH") + .takes_value(true) + .min_values(0) + .max_values(1) + .help("Derivation path. All indexes will be promoted to hardened. \ + If arg is not presented then derivation path will not be used. \ + If arg is presented with empty DERIVATION_PATH value then m/44'/501'/0'/0' will be used." + ) +} + +pub fn acquire_derivation_path( + matches: &ArgMatches, +) -> Result, Box> { + if matches.is_present("derivation_path") { + Ok(Some(DerivationPath::from_absolute_path_str( + matches + .value_of("derivation_path") + .unwrap_or(DEFAULT_DERIVATION_PATH), + )?)) + } else { + Ok(None) + } +} diff --git a/clap-v3-utils/src/keygen/mnemonic.rs b/clap-v3-utils/src/keygen/mnemonic.rs new file mode 100644 index 0000000000..c7c1d4d085 --- /dev/null +++ b/clap-v3-utils/src/keygen/mnemonic.rs @@ -0,0 +1,101 @@ +use { + crate::{keypair::prompt_passphrase, ArgConstant}, + bip39::Language, + clap::{Arg, ArgMatches}, + std::error, +}; + +pub const NO_PASSPHRASE: &str = ""; + +pub const WORD_COUNT_ARG: ArgConstant<'static> = ArgConstant { + long: "word-count", + name: "word_count", + help: "Specify the number of words that will be present in the generated seed phrase", +}; + +pub const LANGUAGE_ARG: ArgConstant<'static> = ArgConstant { + long: "language", + name: "language", + help: "Specify the mnemonic language that will be present in the generated seed phrase", +}; + +pub const NO_PASSPHRASE_ARG: ArgConstant<'static> = ArgConstant { + long: "no-bip39-passphrase", + name: "no_passphrase", + help: "Do not prompt for a BIP39 passphrase", +}; + +pub fn word_count_arg<'a>() -> Arg<'a> { + Arg::new(WORD_COUNT_ARG.name) + .long(WORD_COUNT_ARG.long) + .possible_values(["12", "15", "18", "21", "24"]) + .default_value("12") + .value_name("NUMBER") + .takes_value(true) + .help(WORD_COUNT_ARG.help) +} + +pub fn language_arg<'a>() -> Arg<'a> { + Arg::new(LANGUAGE_ARG.name) + .long(LANGUAGE_ARG.long) + .possible_values([ + "english", + "chinese-simplified", + "chinese-traditional", + "japanese", + "spanish", + "korean", + "french", + "italian", + ]) + .default_value("english") + .value_name("LANGUAGE") + .takes_value(true) + .help(LANGUAGE_ARG.help) +} + +pub fn no_passphrase_arg<'a>() -> Arg<'a> { + Arg::new(NO_PASSPHRASE_ARG.name) + .long(NO_PASSPHRASE_ARG.long) + .alias("no-passphrase") + .help(NO_PASSPHRASE_ARG.help) +} + +pub fn acquire_language(matches: &ArgMatches) -> Language { + match matches.value_of(LANGUAGE_ARG.name).unwrap() { + "english" => Language::English, + "chinese-simplified" => Language::ChineseSimplified, + "chinese-traditional" => Language::ChineseTraditional, + "japanese" => Language::Japanese, + "spanish" => Language::Spanish, + "korean" => Language::Korean, + "french" => Language::French, + "italian" => Language::Italian, + _ => unreachable!(), + } +} + +pub fn no_passphrase_and_message() -> (String, String) { + (NO_PASSPHRASE.to_string(), "".to_string()) +} + +pub fn acquire_passphrase_and_message( + matches: &ArgMatches, +) -> Result<(String, String), Box> { + if matches.is_present(NO_PASSPHRASE_ARG.name) { + Ok(no_passphrase_and_message()) + } else { + match prompt_passphrase( + "\nFor added security, enter a BIP39 passphrase\n\ + \nNOTE! This passphrase improves security of the recovery seed phrase NOT the\n\ + keypair file itself, which is stored as insecure plain text\n\ + \nBIP39 Passphrase (empty for none): ", + ) { + Ok(passphrase) => { + println!(); + Ok((passphrase, " and your BIP39 passphrase".to_string())) + } + Err(e) => Err(e), + } + } +} diff --git a/clap-v3-utils/src/keygen/mod.rs b/clap-v3-utils/src/keygen/mod.rs new file mode 100644 index 0000000000..ec74df3823 --- /dev/null +++ b/clap-v3-utils/src/keygen/mod.rs @@ -0,0 +1,47 @@ +use { + crate::{ + keygen::mnemonic::{language_arg, no_passphrase_arg, word_count_arg}, + ArgConstant, + }, + clap::{Arg, ArgMatches, Command}, + std::{error, path::Path}, +}; + +pub mod derivation_path; +pub mod mnemonic; + +pub const NO_OUTFILE_ARG: ArgConstant<'static> = ArgConstant { + long: "no-outfile", + name: "no_outfile", + help: "Only print a seed phrase and pubkey. Do not output a keypair file", +}; + +pub fn no_outfile_arg<'a>() -> Arg<'a> { + Arg::new(NO_OUTFILE_ARG.name) + .long(NO_OUTFILE_ARG.long) + .help(NO_OUTFILE_ARG.help) +} + +pub trait KeyGenerationCommonArgs { + fn key_generation_common_args(self) -> Self; +} + +impl KeyGenerationCommonArgs for Command<'_> { + fn key_generation_common_args(self) -> Self { + self.arg(word_count_arg()) + .arg(language_arg()) + .arg(no_passphrase_arg()) + } +} + +pub fn check_for_overwrite( + outfile: &str, + matches: &ArgMatches, +) -> Result<(), Box> { + let force = matches.is_present("force"); + if !force && Path::new(outfile).exists() { + let err_msg = format!("Refusing to overwrite {outfile} without --force flag"); + return Err(err_msg.into()); + } + Ok(()) +} diff --git a/clap-v3-utils/src/lib.rs b/clap-v3-utils/src/lib.rs index c6f7e8e2d9..5000f7e1d0 100644 --- a/clap-v3-utils/src/lib.rs +++ b/clap-v3-utils/src/lib.rs @@ -26,6 +26,7 @@ impl std::fmt::Debug for DisplayError { pub mod fee_payer; pub mod input_parsers; pub mod input_validators; +pub mod keygen; pub mod keypair; pub mod memo; pub mod nonce; diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index ad681139d5..cad12bfafc 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -1,20 +1,28 @@ #![allow(clippy::integer_arithmetic)] use { - bip39::{Language, Mnemonic, MnemonicType, Seed}, + bip39::{Mnemonic, MnemonicType, Seed}, clap::{crate_description, crate_name, Arg, ArgMatches, Command}, solana_clap_v3_utils::{ input_parsers::STDOUT_OUTFILE_TOKEN, input_validators::{is_parsable, is_prompt_signer_source}, + keygen::{ + check_for_overwrite, + derivation_path::{acquire_derivation_path, derivation_path_arg}, + mnemonic::{ + acquire_language, acquire_passphrase_and_message, no_passphrase_and_message, + WORD_COUNT_ARG, + }, + no_outfile_arg, KeyGenerationCommonArgs, NO_OUTFILE_ARG, + }, keypair::{ - keypair_from_path, keypair_from_seed_phrase, prompt_passphrase, signer_from_path, + keypair_from_path, keypair_from_seed_phrase, signer_from_path, SKIP_SEED_PHRASE_VALIDATION_ARG, }, - ArgConstant, DisplayError, + DisplayError, }, solana_cli_config::{Config, CONFIG_FILE}, solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_sdk::{ - derivation_path::DerivationPath, instruction::{AccountMeta, Instruction}, message::Message, pubkey::{write_pubkey_file, Pubkey}, @@ -26,7 +34,6 @@ use { std::{ collections::HashSet, error, - path::Path, sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, Arc, @@ -48,102 +55,12 @@ mod smallest_length_44_public_key { } } -const NO_PASSPHRASE: &str = ""; -const DEFAULT_DERIVATION_PATH: &str = "m/44'/501'/0'/0'"; - struct GrindMatch { starts: String, ends: String, count: AtomicU64, } -const WORD_COUNT_ARG: ArgConstant<'static> = ArgConstant { - long: "word-count", - name: "word_count", - help: "Specify the number of words that will be present in the generated seed phrase", -}; - -const LANGUAGE_ARG: ArgConstant<'static> = ArgConstant { - long: "language", - name: "language", - help: "Specify the mnemonic language that will be present in the generated seed phrase", -}; - -const NO_PASSPHRASE_ARG: ArgConstant<'static> = ArgConstant { - long: "no-bip39-passphrase", - name: "no_passphrase", - help: "Do not prompt for a BIP39 passphrase", -}; - -const NO_OUTFILE_ARG: ArgConstant<'static> = ArgConstant { - long: "no-outfile", - name: "no_outfile", - help: "Only print a seed phrase and pubkey. Do not output a keypair file", -}; - -fn word_count_arg<'a>() -> Arg<'a> { - Arg::new(WORD_COUNT_ARG.name) - .long(WORD_COUNT_ARG.long) - .possible_values(["12", "15", "18", "21", "24"]) - .default_value("12") - .value_name("NUMBER") - .takes_value(true) - .help(WORD_COUNT_ARG.help) -} - -fn language_arg<'a>() -> Arg<'a> { - Arg::new(LANGUAGE_ARG.name) - .long(LANGUAGE_ARG.long) - .possible_values([ - "english", - "chinese-simplified", - "chinese-traditional", - "japanese", - "spanish", - "korean", - "french", - "italian", - ]) - .default_value("english") - .value_name("LANGUAGE") - .takes_value(true) - .help(LANGUAGE_ARG.help) -} - -fn no_passphrase_arg<'a>() -> Arg<'a> { - Arg::new(NO_PASSPHRASE_ARG.name) - .long(NO_PASSPHRASE_ARG.long) - .alias("no-passphrase") - .help(NO_PASSPHRASE_ARG.help) -} - -fn no_outfile_arg<'a>() -> Arg<'a> { - Arg::new(NO_OUTFILE_ARG.name) - .long(NO_OUTFILE_ARG.long) - .help(NO_OUTFILE_ARG.help) -} - -trait KeyGenerationCommonArgs { - fn key_generation_common_args(self) -> Self; -} - -impl KeyGenerationCommonArgs for Command<'_> { - fn key_generation_common_args(self) -> Self { - self.arg(word_count_arg()) - .arg(language_arg()) - .arg(no_passphrase_arg()) - } -} - -fn check_for_overwrite(outfile: &str, matches: &ArgMatches) -> Result<(), Box> { - let force = matches.is_present("force"); - if !force && Path::new(outfile).exists() { - let err_msg = format!("Refusing to overwrite {outfile} without --force flag"); - return Err(err_msg.into()); - } - Ok(()) -} - fn get_keypair_from_matches( matches: &ArgMatches, config: Config, @@ -226,45 +143,6 @@ fn grind_validator_starts_and_ends_with(v: &str) -> Result<(), String> { Ok(()) } -fn acquire_language(matches: &ArgMatches) -> Language { - match matches.value_of(LANGUAGE_ARG.name).unwrap() { - "english" => Language::English, - "chinese-simplified" => Language::ChineseSimplified, - "chinese-traditional" => Language::ChineseTraditional, - "japanese" => Language::Japanese, - "spanish" => Language::Spanish, - "korean" => Language::Korean, - "french" => Language::French, - "italian" => Language::Italian, - _ => unreachable!(), - } -} - -fn no_passphrase_and_message() -> (String, String) { - (NO_PASSPHRASE.to_string(), "".to_string()) -} - -fn acquire_passphrase_and_message( - matches: &ArgMatches, -) -> Result<(String, String), Box> { - if matches.is_present(NO_PASSPHRASE_ARG.name) { - Ok(no_passphrase_and_message()) - } else { - match prompt_passphrase( - "\nFor added security, enter a BIP39 passphrase\n\ - \nNOTE! This passphrase improves security of the recovery seed phrase NOT the\n\ - keypair file itself, which is stored as insecure plain text\n\ - \nBIP39 Passphrase (empty for none): ", - ) { - Ok(passphrase) => { - println!(); - Ok((passphrase, " and your BIP39 passphrase".to_string())) - } - Err(e) => Err(e), - } - } -} - fn grind_print_info(grind_matches: &[GrindMatch], num_threads: usize) { println!("Searching with {num_threads} threads for:"); for gm in grind_matches { @@ -342,33 +220,6 @@ fn grind_parse_args( grind_matches } -fn derivation_path_arg<'a>() -> Arg<'a> { - Arg::new("derivation_path") - .long("derivation-path") - .value_name("DERIVATION_PATH") - .takes_value(true) - .min_values(0) - .max_values(1) - .help("Derivation path. All indexes will be promoted to hardened. \ - If arg is not presented then derivation path will not be used. \ - If arg is presented with empty DERIVATION_PATH value then m/44'/501'/0'/0' will be used." - ) -} - -fn acquire_derivation_path( - matches: &ArgMatches, -) -> Result, Box> { - if matches.is_present("derivation_path") { - Ok(Some(DerivationPath::from_absolute_path_str( - matches - .value_of("derivation_path") - .unwrap_or(DEFAULT_DERIVATION_PATH), - )?)) - } else { - Ok(None) - } -} - fn app<'a>(num_threads: &'a str, crate_version: &'a str) -> Command<'a> { Command::new(crate_name!()) .about(crate_description!())