[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
This commit is contained in:
samkim-crypto 2023-05-13 14:30:03 +09:00 committed by GitHub
parent 125a18550d
commit 618d8cf2a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 161 deletions

View File

@ -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<Option<DerivationPath>, Box<dyn error::Error>> {
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)
}
}

View File

@ -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<dyn error::Error>> {
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),
}
}
}

View File

@ -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<dyn error::Error>> {
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(())
}

View File

@ -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;

View File

@ -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<dyn error::Error>> {
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<dyn error::Error>> {
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<Option<DerivationPath>, Box<dyn error::Error>> {
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!())