Allow secure keypair input for `solana-archiver` and `solana` cli tools (#7106)

* Add seed phrase keypair recover to archiver

* Add seed phrase keypair to cli with ASK keyword

* cli main tweaks
This commit is contained in:
Justin Starry 2019-11-23 11:55:43 -05:00 committed by GitHub
parent 7f87ac4b65
commit b8cd0a1bc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 218 additions and 108 deletions

View File

@ -1,15 +1,18 @@
use clap::{crate_description, crate_name, App, Arg}; use clap::{crate_description, crate_name, App, Arg};
use console::style; use console::style;
use solana_clap_utils::input_validators::is_keypair; use solana_clap_utils::{
input_validators::is_keypair,
keypair::{
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
},
};
use solana_core::{ use solana_core::{
archiver::Archiver, archiver::Archiver,
cluster_info::{Node, VALIDATOR_PORT_RANGE}, cluster_info::{Node, VALIDATOR_PORT_RANGE},
contact_info::ContactInfo, contact_info::ContactInfo,
}; };
use solana_sdk::{ use solana_sdk::{commitment_config::CommitmentConfig, signature::KeypairUtil};
commitment_config::CommitmentConfig,
signature::{read_keypair_file, Keypair, KeypairUtil},
};
use std::{net::SocketAddr, path::PathBuf, process::exit, sync::Arc}; use std::{net::SocketAddr, path::PathBuf, process::exit, sync::Arc};
fn main() { fn main() {
@ -19,9 +22,9 @@ fn main() {
.about(crate_description!()) .about(crate_description!())
.version(solana_clap_utils::version!()) .version(solana_clap_utils::version!())
.arg( .arg(
Arg::with_name("identity") Arg::with_name("identity_keypair")
.short("i") .short("i")
.long("identity") .long("identity-keypair")
.value_name("PATH") .value_name("PATH")
.takes_value(true) .takes_value(true)
.validator(is_keypair) .validator(is_keypair)
@ -52,30 +55,48 @@ fn main() {
.long("storage-keypair") .long("storage-keypair")
.value_name("PATH") .value_name("PATH")
.takes_value(true) .takes_value(true)
.required(true)
.validator(is_keypair) .validator(is_keypair)
.help("File containing the storage account keypair"), .help("File containing the storage account keypair"),
) )
.arg(
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME")
.multiple(true)
.takes_value(true)
.possible_values(&["identity-keypair", "storage-keypair"])
.help(ASK_SEED_PHRASE_ARG.help),
)
.arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.requires(ASK_SEED_PHRASE_ARG.name)
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
)
.get_matches(); .get_matches();
let ledger_path = PathBuf::from(matches.value_of("ledger").unwrap()); let ledger_path = PathBuf::from(matches.value_of("ledger").unwrap());
let keypair = if let Some(identity) = matches.value_of("identity") { let identity_keypair = keypair_input(&matches, "identity_keypair")
read_keypair_file(identity).unwrap_or_else(|err| { .unwrap_or_else(|err| {
eprintln!("{}: Unable to open keypair file: {}", err, identity); eprintln!("Identity keypair input failed: {}", err);
exit(1); exit(1);
}) })
} else { .keypair;
Keypair::new() let KeypairWithSource {
}; keypair: storage_keypair,
let storage_keypair = if let Some(storage_keypair) = matches.value_of("storage_keypair") { source: storage_keypair_source,
read_keypair_file(storage_keypair).unwrap_or_else(|err| { } = keypair_input(&matches, "storage_keypair").unwrap_or_else(|err| {
eprintln!("{}: Unable to open keypair file: {}", err, storage_keypair); eprintln!("Storage keypair input failed: {}", err);
exit(1); exit(1);
}) });
} else { if storage_keypair_source == keypair::Source::Generated {
Keypair::new() clap::Error::with_description(
}; "The `storage-keypair` argument was not found",
clap::ErrorKind::ArgumentNotFound,
)
.exit();
}
let entrypoint_addr = matches let entrypoint_addr = matches
.value_of("entrypoint") .value_of("entrypoint")
@ -91,8 +112,11 @@ fn main() {
addr.set_ip(solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap()); addr.set_ip(solana_net_utils::get_public_ip_addr(&entrypoint_addr).unwrap());
addr addr
}; };
let node = let node = Node::new_archiver_with_external_ip(
Node::new_archiver_with_external_ip(&keypair.pubkey(), &gossip_addr, VALIDATOR_PORT_RANGE); &identity_keypair.pubkey(),
&gossip_addr,
VALIDATOR_PORT_RANGE,
);
println!( println!(
"{} version {} (branch={}, commit={})", "{} version {} (branch={}, commit={})",
@ -101,10 +125,10 @@ fn main() {
option_env!("CI_BRANCH").unwrap_or("unknown"), option_env!("CI_BRANCH").unwrap_or("unknown"),
option_env!("CI_COMMIT").unwrap_or("unknown") option_env!("CI_COMMIT").unwrap_or("unknown")
); );
solana_metrics::set_host_id(keypair.pubkey().to_string()); solana_metrics::set_host_id(identity_keypair.pubkey().to_string());
println!( println!(
"replicating the data with keypair={:?} gossip_addr={:?}", "replicating the data with identity_keypair={:?} gossip_addr={:?}",
keypair.pubkey(), identity_keypair.pubkey(),
gossip_addr gossip_addr
); );
@ -113,7 +137,7 @@ fn main() {
&ledger_path, &ledger_path,
node, node,
entrypoint_info, entrypoint_info,
Arc::new(keypair), Arc::new(identity_keypair),
Arc::new(storage_keypair), Arc::new(storage_keypair),
CommitmentConfig::recent(), CommitmentConfig::recent(),
) )

View File

@ -138,7 +138,7 @@ Note: Every time the testnet restarts, run the steps to setup the archiver accou
To start the archiver: To start the archiver:
```bash ```bash
solana-archiver --entrypoint testnet.solana.com:8001 --identity archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger solana-archiver --entrypoint testnet.solana.com:8001 --identity-keypair archiver-keypair.json --storage-keypair storage-keypair.json --ledger archiver-ledger
``` ```
## Verify Archiver Setup ## Verify Archiver Setup

View File

@ -1,3 +1,4 @@
use crate::keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG};
use clap::ArgMatches; use clap::ArgMatches;
use solana_sdk::{ use solana_sdk::{
native_token::sol_to_lamports, native_token::sol_to_lamports,
@ -32,7 +33,12 @@ where
// Return the keypair for an argument with filename `name` or None if not present. // Return the keypair for an argument with filename `name` or None if not present.
pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option<Keypair> { pub fn keypair_of(matches: &ArgMatches<'_>, name: &str) -> Option<Keypair> {
if let Some(value) = matches.value_of(name) { if let Some(value) = matches.value_of(name) {
read_keypair_file(value).ok() if value == ASK_KEYWORD {
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
keypair_from_seed_phrase(name, skip_validation).ok()
} else {
read_keypair_file(value).ok()
}
} else { } else {
None None
} }

View File

@ -1,3 +1,4 @@
use crate::keypair::ASK_KEYWORD;
use solana_sdk::pubkey::Pubkey; use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::read_keypair_file; use solana_sdk::signature::read_keypair_file;
@ -16,6 +17,16 @@ pub fn is_keypair(string: String) -> Result<(), String> {
.map_err(|err| format!("{:?}", err)) .map_err(|err| format!("{:?}", err))
} }
// Return an error if a keypair file cannot be parsed
pub fn is_keypair_or_ask_keyword(string: String) -> Result<(), String> {
if string.as_str() == ASK_KEYWORD {
return Ok(());
}
read_keypair_file(&string)
.map(|_| ())
.map_err(|err| format!("{:?}", err))
}
// Return an error if string cannot be parsed as pubkey string or keypair file location // Return an error if string cannot be parsed as pubkey string or keypair file location
pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> { pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> {
is_pubkey(string.clone()).or_else(|_| is_keypair(string)) is_pubkey(string.clone()).or_else(|_| is_keypair(string))

View File

@ -1,3 +1,4 @@
use crate::ArgConstant;
use bip39::{Language, Mnemonic, Seed}; use bip39::{Language, Mnemonic, Seed};
use clap::values_t; use clap::values_t;
use rpassword::prompt_password_stderr; use rpassword::prompt_password_stderr;
@ -7,11 +8,41 @@ use solana_sdk::signature::{
}; };
use std::error; use std::error;
pub const ASK_SEED_PHRASE_ARG: &str = "ask_seed_phrase"; // Keyword used to indicate that the user should be asked for a keypair seed phrase
pub const SKIP_SEED_PHRASE_VALIDATION_ARG: &str = "skip_seed_phrase_validation"; pub const ASK_KEYWORD: &str = "ASK";
pub const ASK_SEED_PHRASE_ARG: ArgConstant<'static> = ArgConstant {
long: "ask-seed-phrase",
name: "ask_seed_phrase",
help: "Securely recover a keypair using a seed phrase and optional passphrase",
};
pub const SKIP_SEED_PHRASE_VALIDATION_ARG: ArgConstant<'static> = ArgConstant {
long: "skip-seed-phrase-validation",
name: "skip_seed_phrase_validation",
help: "Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list",
};
#[derive(Debug, PartialEq)]
pub enum Source {
File,
Generated,
SeedPhrase,
}
pub struct KeypairWithSource {
pub keypair: Keypair,
pub source: Source,
}
impl KeypairWithSource {
fn new(keypair: Keypair, source: Source) -> Self {
Self { keypair, source }
}
}
/// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation /// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation
pub fn keypair_from_seed_phrase( pub(crate) fn keypair_from_seed_phrase(
keypair_name: &str, keypair_name: &str,
skip_validation: bool, skip_validation: bool,
) -> Result<Keypair, Box<dyn error::Error>> { ) -> Result<Keypair, Box<dyn error::Error>> {
@ -33,17 +64,6 @@ pub fn keypair_from_seed_phrase(
} }
} }
pub struct KeypairWithGenerated {
pub keypair: Keypair,
pub generated: bool,
}
impl KeypairWithGenerated {
fn new(keypair: Keypair, generated: bool) -> Self {
Self { keypair, generated }
}
}
/// Checks CLI arguments to determine whether a keypair should be: /// Checks CLI arguments to determine whether a keypair should be:
/// - inputted securely via stdin, /// - inputted securely via stdin,
/// - read in from a file, /// - read in from a file,
@ -51,33 +71,32 @@ impl KeypairWithGenerated {
pub fn keypair_input( pub fn keypair_input(
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
keypair_name: &str, keypair_name: &str,
) -> Result<KeypairWithGenerated, Box<dyn error::Error>> { ) -> Result<KeypairWithSource, Box<dyn error::Error>> {
let ask_seed_phrase_matches = let ask_seed_phrase_matches =
values_t!(matches.values_of(ASK_SEED_PHRASE_ARG), String).unwrap_or_default(); values_t!(matches.values_of(ASK_SEED_PHRASE_ARG.name), String).unwrap_or_default();
let keypair_match_name = keypair_name.replace('-', "_"); let keypair_match_name = keypair_name.replace('-', "_");
if ask_seed_phrase_matches if ask_seed_phrase_matches
.iter() .iter()
.any(|s| s.as_str() == keypair_name) .any(|s| s.as_str() == keypair_name)
{ {
if matches.value_of(keypair_match_name).is_some() { if matches.value_of(keypair_match_name).is_some() {
let ask_seed_phrase_kebab = ASK_SEED_PHRASE_ARG.replace('_', "-");
clap::Error::with_description( clap::Error::with_description(
&format!( &format!(
"`--{} {}` cannot be used with `{} <PATH>`", "`--{} {}` cannot be used with `{} <PATH>`",
ask_seed_phrase_kebab, keypair_name, keypair_name ASK_SEED_PHRASE_ARG.long, keypair_name, keypair_name
), ),
clap::ErrorKind::ArgumentConflict, clap::ErrorKind::ArgumentConflict,
) )
.exit(); .exit();
} }
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG); let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
keypair_from_seed_phrase(keypair_name, skip_validation) keypair_from_seed_phrase(keypair_name, skip_validation)
.map(|keypair| KeypairWithGenerated::new(keypair, false)) .map(|keypair| KeypairWithSource::new(keypair, Source::SeedPhrase))
} else if let Some(keypair_file) = matches.value_of(keypair_match_name) { } else if let Some(keypair_file) = matches.value_of(keypair_match_name) {
read_keypair_file(keypair_file).map(|keypair| KeypairWithGenerated::new(keypair, false)) read_keypair_file(keypair_file).map(|keypair| KeypairWithSource::new(keypair, Source::File))
} else { } else {
Ok(KeypairWithGenerated::new(Keypair::new(), true)) Ok(KeypairWithSource::new(Keypair::new(), Source::Generated))
} }
} }
@ -89,7 +108,7 @@ mod tests {
#[test] #[test]
fn test_keypair_input() { fn test_keypair_input() {
let arg_matches = ArgMatches::default(); let arg_matches = ArgMatches::default();
let KeypairWithGenerated { generated, .. } = keypair_input(&arg_matches, "").unwrap(); let KeypairWithSource { source, .. } = keypair_input(&arg_matches, "").unwrap();
assert!(generated); assert_eq!(source, Source::Generated);
} }
} }

View File

@ -17,6 +17,12 @@ macro_rules! version {
}; };
} }
pub struct ArgConstant<'a> {
pub long: &'a str,
pub name: &'a str,
pub help: &'a str,
}
pub mod input_parsers; pub mod input_parsers;
pub mod input_validators; pub mod input_validators;
pub mod keypair; pub mod keypair;

View File

@ -219,19 +219,28 @@ pub struct CliConfig {
pub rpc_client: Option<RpcClient>, pub rpc_client: Option<RpcClient>,
} }
impl Default for CliConfig { impl CliConfig {
fn default() -> CliConfig { pub fn default_keypair_path() -> String {
let mut keypair_path = dirs::home_dir().expect("home directory"); let mut keypair_path = dirs::home_dir().expect("home directory");
keypair_path.extend(&[".config", "solana", "id.json"]); keypair_path.extend(&[".config", "solana", "id.json"]);
keypair_path.to_str().unwrap().to_string()
}
pub fn default_json_rpc_url() -> String {
"http://127.0.0.1:8899".to_string()
}
}
impl Default for CliConfig {
fn default() -> CliConfig {
CliConfig { CliConfig {
command: CliCommand::Balance { command: CliCommand::Balance {
pubkey: Some(Pubkey::default()), pubkey: Some(Pubkey::default()),
use_lamports_unit: false, use_lamports_unit: false,
}, },
json_rpc_url: "http://127.0.0.1:8899".to_string(), json_rpc_url: Self::default_json_rpc_url(),
keypair: Keypair::new(), keypair: Keypair::new(),
keypair_path: Some(keypair_path.to_str().unwrap().to_string()), keypair_path: Some(Self::default_keypair_path()),
rpc_client: None, rpc_client: None,
} }
} }

View File

@ -16,14 +16,14 @@ lazy_static! {
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] #[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct Config { pub struct Config {
pub url: String, pub url: String,
pub keypair: String, pub keypair_path: String,
} }
impl Config { impl Config {
pub fn new(url: &str, keypair: &str) -> Self { pub fn new(url: &str, keypair_path: &str) -> Self {
Self { Self {
url: url.to_string(), url: url.to_string(),
keypair: keypair.to_string(), keypair_path: keypair_path.to_string(),
} }
} }

View File

@ -1,7 +1,13 @@
use clap::{crate_description, crate_name, Arg, ArgGroup, ArgMatches, SubCommand}; use clap::{crate_description, crate_name, Arg, ArgGroup, ArgMatches, SubCommand};
use console::style; use console::style;
use solana_clap_utils::input_validators::is_url; use solana_clap_utils::{
input_validators::is_url,
keypair::{
self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
},
};
use solana_cli::{ use solana_cli::{
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliError}, cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliError},
config::{self, Config}, config::{self, Config},
@ -15,22 +21,25 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
let parse_args = match matches.subcommand() { let parse_args = match matches.subcommand() {
("get", Some(subcommand_matches)) => { ("get", Some(subcommand_matches)) => {
if let Some(config_file) = matches.value_of("config_file") { if let Some(config_file) = matches.value_of("config_file") {
let default_cli_config = CliConfig::default();
let config = Config::load(config_file).unwrap_or_default(); let config = Config::load(config_file).unwrap_or_default();
if let Some(field) = subcommand_matches.value_of("specific_setting") { if let Some(field) = subcommand_matches.value_of("specific_setting") {
let (value, default_value) = match field { let (value, default_value) = match field {
"url" => (config.url, default_cli_config.json_rpc_url), "url" => (config.url, CliConfig::default_json_rpc_url()),
"keypair" => (config.keypair, default_cli_config.keypair_path.unwrap()), "keypair" => (config.keypair_path, CliConfig::default_keypair_path()),
_ => unreachable!(), _ => unreachable!(),
}; };
println_name_value_or(&format!("* {}:", field), &value, &default_value); println_name_value_or(&format!("* {}:", field), &value, &default_value);
} else { } else {
println_name_value("Wallet Config:", config_file); println_name_value("Wallet Config:", config_file);
println_name_value_or("* url:", &config.url, &default_cli_config.json_rpc_url); println_name_value_or(
"* url:",
&config.url,
&CliConfig::default_json_rpc_url(),
);
println_name_value_or( println_name_value_or(
"* keypair:", "* keypair:",
&config.keypair, &config.keypair_path,
&default_cli_config.keypair_path.unwrap(), &CliConfig::default_keypair_path(),
); );
} }
} else { } else {
@ -48,12 +57,12 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
config.url = url.to_string(); config.url = url.to_string();
} }
if let Some(keypair) = subcommand_matches.value_of("keypair") { if let Some(keypair) = subcommand_matches.value_of("keypair") {
config.keypair = keypair.to_string(); config.keypair_path = keypair.to_string();
} }
config.save(config_file)?; config.save(config_file)?;
println_name_value("Wallet Config Updated:", config_file); println_name_value("Wallet Config Updated:", config_file);
println_name_value("* url:", &config.url); println_name_value("* url:", &config.url);
println_name_value("* keypair:", &config.keypair); println_name_value("* keypair:", &config.keypair_path);
} else { } else {
println!( println!(
"{} Either provide the `--config` arg or ensure home directory exists to use the default config location", "{} Either provide the `--config` arg or ensure home directory exists to use the default config location",
@ -88,28 +97,37 @@ pub fn parse_args(matches: &ArgMatches<'_>) -> Result<CliConfig, Box<dyn error::
} = parse_command(&matches)?; } = parse_command(&matches)?;
let (keypair, keypair_path) = if require_keypair { let (keypair, keypair_path) = if require_keypair {
let keypair_path = if matches.is_present("keypair") { let KeypairWithSource { keypair, source } = keypair_input(&matches, "keypair")?;
matches.value_of("keypair").unwrap().to_string() match source {
} else if config.keypair != "" { keypair::Source::File => (
config.keypair keypair,
} else { Some(matches.value_of("keypair").unwrap().to_string()),
let default = CliConfig::default(); ),
let maybe_keypair_path = default.keypair_path.unwrap(); keypair::Source::SeedPhrase => (keypair, None),
if !std::path::Path::new(&maybe_keypair_path).exists() { keypair::Source::Generated => {
return Err(CliError::KeypairFileNotFound( let keypair_path = if config.keypair_path != "" {
"Generate a new keypair with `solana-keygen new`".to_string(), config.keypair_path
) } else {
.into()); let default_keypair_path = CliConfig::default_keypair_path();
if !std::path::Path::new(&default_keypair_path).exists() {
return Err(CliError::KeypairFileNotFound(
"Generate a new keypair with `solana-keygen new`".to_string(),
)
.into());
}
default_keypair_path
};
let keypair = read_keypair_file(&keypair_path).or_else(|err| {
Err(CliError::BadParameter(format!(
"{}: Unable to open keypair file: {}",
err, keypair_path
)))
})?;
(keypair, Some(keypair_path))
} }
maybe_keypair_path }
};
let keypair = read_keypair_file(&keypair_path).or_else(|err| {
Err(CliError::BadParameter(format!(
"{}: Unable to open keypair file: {}",
err, keypair_path
)))
})?;
(keypair, Some(keypair_path.to_string()))
} else { } else {
let default = CliConfig::default(); let default = CliConfig::default();
(default.keypair, None) (default.keypair, None)
@ -164,6 +182,21 @@ fn main() -> Result<(), Box<dyn error::Error>> {
.takes_value(true) .takes_value(true)
.help("/path/to/id.json"), .help("/path/to/id.json"),
) )
.arg(
Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME")
.global(true)
.takes_value(true)
.possible_values(&["keypair"])
.help(ASK_SEED_PHRASE_ARG.help),
)
.arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.global(true)
.help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
)
.subcommand( .subcommand(
SubCommand::with_name("get") SubCommand::with_name("get")
.about("Get cli config settings") .about("Get cli config settings")

View File

@ -41,7 +41,7 @@ impl StakeSubCommands for App<'_, '_> {
.value_name("STAKE ACCOUNT") .value_name("STAKE ACCOUNT")
.takes_value(true) .takes_value(true)
.required(true) .required(true)
.validator(is_keypair) .validator(is_keypair_or_ask_keyword)
.help("Keypair of the stake account to fund") .help("Keypair of the stake account to fund")
) )
.arg( .arg(

View File

@ -35,7 +35,7 @@ impl StorageSubCommands for App<'_, '_> {
.value_name("STORAGE ACCOUNT") .value_name("STORAGE ACCOUNT")
.takes_value(true) .takes_value(true)
.required(true) .required(true)
.validator(is_keypair), .validator(is_keypair_or_ask_keyword),
), ),
) )
.subcommand( .subcommand(
@ -55,7 +55,7 @@ impl StorageSubCommands for App<'_, '_> {
.value_name("STORAGE ACCOUNT") .value_name("STORAGE ACCOUNT")
.takes_value(true) .takes_value(true)
.required(true) .required(true)
.validator(is_keypair), .validator(is_keypair_or_ask_keyword),
), ),
) )
.subcommand( .subcommand(

View File

@ -30,7 +30,7 @@ impl VoteSubCommands for App<'_, '_> {
.value_name("VOTE ACCOUNT KEYPAIR") .value_name("VOTE ACCOUNT KEYPAIR")
.takes_value(true) .takes_value(true)
.required(true) .required(true)
.validator(is_keypair) .validator(is_keypair_or_ask_keyword)
.help("Vote account keypair to fund"), .help("Vote account keypair to fund"),
) )
.arg( .arg(

View File

@ -18,7 +18,7 @@ while [[ -n $1 ]]; do
entrypoint=$2 entrypoint=$2
args+=("$1" "$2") args+=("$1" "$2")
shift 2 shift 2
elif [[ $1 = --identity ]]; then elif [[ $1 = --identity-keypair ]]; then
identity_keypair=$2 identity_keypair=$2
[[ -r $identity_keypair ]] || { [[ -r $identity_keypair ]] || {
echo "$identity_keypair does not exist" echo "$identity_keypair does not exist"
@ -74,7 +74,7 @@ if [[ ! -r $storage_keypair ]]; then
fi fi
default_arg --entrypoint "$entrypoint" default_arg --entrypoint "$entrypoint"
default_arg --identity "$identity_keypair" default_arg --identity-keypair "$identity_keypair"
default_arg --storage-keypair "$storage_keypair" default_arg --storage-keypair "$storage_keypair"
default_arg --ledger "$ledger" default_arg --ledger "$ledger"

View File

@ -415,7 +415,7 @@ EOF
) )
if [[ $airdropsEnabled != true ]]; then if [[ $airdropsEnabled != true ]]; then
# If this ever becomes a problem, we need to provide the `--identity` # If this ever becomes a problem, we need to provide the `--identity-keypair`
# argument to an existing system account with lamports in it # argument to an existing system account with lamports in it
echo "Error: archivers not supported without airdrops" echo "Error: archivers not supported without airdrops"
exit 1 exit 1

View File

@ -7,7 +7,8 @@ use solana_clap_utils::{
input_parsers::pubkey_of, input_parsers::pubkey_of,
input_validators::{is_keypair, is_pubkey_or_keypair}, input_validators::{is_keypair, is_pubkey_or_keypair},
keypair::{ keypair::{
keypair_input, KeypairWithGenerated, ASK_SEED_PHRASE_ARG, SKIP_SEED_PHRASE_VALIDATION_ARG, self, keypair_input, KeypairWithSource, ASK_SEED_PHRASE_ARG,
SKIP_SEED_PHRASE_VALIDATION_ARG,
}, },
}; };
use solana_client::rpc_client::RpcClient; use solana_client::rpc_client::RpcClient;
@ -326,19 +327,19 @@ pub fn main() {
.help("Stream entries to this unix domain socket path") .help("Stream entries to this unix domain socket path")
) )
.arg( .arg(
Arg::with_name(ASK_SEED_PHRASE_ARG) Arg::with_name(ASK_SEED_PHRASE_ARG.name)
.long("ask-seed-phrase") .long(ASK_SEED_PHRASE_ARG.long)
.value_name("KEYPAIR NAME") .value_name("KEYPAIR NAME")
.multiple(true) .multiple(true)
.takes_value(true) .takes_value(true)
.possible_values(&["identity-keypair", "storage-keypair", "voting-keypair"]) .possible_values(&["identity-keypair", "storage-keypair", "voting-keypair"])
.help("Securely recover a keypair using a seed phrase and optional passphrase"), .help(ASK_SEED_PHRASE_ARG.help),
) )
.arg( .arg(
Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG) Arg::with_name(SKIP_SEED_PHRASE_VALIDATION_ARG.name)
.long("skip-seed-phrase-validation") .long(SKIP_SEED_PHRASE_VALIDATION_ARG.long)
.requires(ASK_SEED_PHRASE_ARG) .requires(ASK_SEED_PHRASE_ARG.name)
.help("Skip validation of seed phrases. Use this if your phrase does not use the BIP39 official English word list"), .help(SKIP_SEED_PHRASE_VALIDATION_ARG.help),
) )
.arg( .arg(
Arg::with_name("identity_keypair") Arg::with_name("identity_keypair")
@ -546,13 +547,14 @@ pub fn main() {
}) })
.keypair, .keypair,
); );
let KeypairWithGenerated { let KeypairWithSource {
keypair: voting_keypair, keypair: voting_keypair,
generated: ephemeral_voting_keypair, source: voting_keypair_source,
} = keypair_input(&matches, "voting-keypair").unwrap_or_else(|err| { } = keypair_input(&matches, "voting-keypair").unwrap_or_else(|err| {
eprintln!("Voting keypair input failed: {}", err); eprintln!("Voting keypair input failed: {}", err);
exit(1); exit(1);
}); });
let ephemeral_voting_keypair = voting_keypair_source == keypair::Source::Generated;
let storage_keypair = keypair_input(&matches, "storage-keypair") let storage_keypair = keypair_input(&matches, "storage-keypair")
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
eprintln!("Storage keypair input failed: {}", err); eprintln!("Storage keypair input failed: {}", err);