diff --git a/clap-v3-utils/src/fee_payer.rs b/clap-v3-utils/src/fee_payer.rs index c87b3dcd7d6a9e..e8083e4cd25d2e 100644 --- a/clap-v3-utils/src/fee_payer.rs +++ b/clap-v3-utils/src/fee_payer.rs @@ -1,5 +1,5 @@ use { - crate::{input_validators, ArgConstant}, + crate::{input_parsers::signer::SignerSourceParserBuilder, ArgConstant}, clap::Arg, }; @@ -16,6 +16,6 @@ pub fn fee_payer_arg<'a>() -> Arg<'a> { .long(FEE_PAYER_ARG.long) .takes_value(true) .value_name("KEYPAIR") - .validator(|s| input_validators::is_valid_signer(s)) + .value_parser(SignerSourceParserBuilder::default().build()) .help(FEE_PAYER_ARG.help) } diff --git a/clap-v3-utils/src/input_parsers/mod.rs b/clap-v3-utils/src/input_parsers/mod.rs index d96af9516b9e5d..c3da217455d4d7 100644 --- a/clap-v3-utils/src/input_parsers/mod.rs +++ b/clap-v3-utils/src/input_parsers/mod.rs @@ -1,30 +1,29 @@ use { crate::{ input_validators::normalize_to_url_if_moniker, - keypair::{keypair_from_seed_phrase, ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG}, + keypair::{ + keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path, + ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG, + }, }, chrono::DateTime, clap::ArgMatches, + solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_sdk::{ clock::UnixTimestamp, commitment_config::CommitmentConfig, genesis_config::ClusterType, native_token::sol_to_lamports, pubkey::{Pubkey, MAX_SEED_LEN}, - signature::{read_keypair_file, Keypair, Signer}, + signature::{read_keypair_file, Keypair, Signature, Signer}, }, - std::str::FromStr, + std::{rc::Rc, str::FromStr}, }; pub mod signer; -#[deprecated( - since = "1.17.0", - note = "Please use the functions in `solana_clap_v3_utils::input_parsers::signer` directly instead" -)] -pub use signer::{ - pubkey_of_signer, pubkeys_of_multiple_signers, pubkeys_sigs_of, resolve_signer, signer_of, - STDOUT_OUTFILE_TOKEN, -}; + +// Sentinel value used to indicate to write to screen instead of file +pub const STDOUT_OUTFILE_TOKEN: &str = "-"; // Return parsed values from matches at `name` pub fn values_of(matches: &ArgMatches, name: &str) -> Option> @@ -246,6 +245,10 @@ pub fn parse_derived_address_seed(arg: &str) -> Result { } // Return the keypair for an argument with filename `name` or None if not present. +#[deprecated( + since = "1.17.0", + note = "please use `SignerSource::try_get_keypair` instead" +)] pub fn keypair_of(matches: &ArgMatches, name: &str) -> Option { if let Some(value) = matches.value_of(name) { if value == ASK_KEYWORD { @@ -259,6 +262,10 @@ pub fn keypair_of(matches: &ArgMatches, name: &str) -> Option { } } +#[deprecated( + since = "1.17.0", + note = "please use `SignerSource::try_get_keypairs` instead" +)] pub fn keypairs_of(matches: &ArgMatches, name: &str) -> Option> { matches.values_of(name).map(|values| { values @@ -276,10 +283,19 @@ pub fn keypairs_of(matches: &ArgMatches, name: &str) -> Option> { // Return a pubkey for an argument that can itself be parsed into a pubkey, // or is a filename that can be read as a keypair +#[deprecated( + since = "1.17.0", + note = "Please use `SignerSource::try_get_pubkey` instead" +)] +#[allow(deprecated)] pub fn pubkey_of(matches: &ArgMatches, name: &str) -> Option { value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey())) } +#[deprecated( + since = "1.17.0", + note = "Please use `SignerSource::try_get_pubkeys` instead" +)] pub fn pubkeys_of(matches: &ArgMatches, name: &str) -> Option> { matches.values_of(name).map(|values| { values @@ -294,12 +310,109 @@ pub fn pubkeys_of(matches: &ArgMatches, name: &str) -> Option> { }) } +// Return a signer from matches at `name` +#[allow(clippy::type_complexity)] +#[deprecated( + since = "1.17.0", + note = "Please use `SignerSource::try_get_signer` instead" +)] +pub fn signer_of( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, +) -> Result<(Option>, Option), Box> { + if let Some(location) = matches.try_get_one::(name)? { + let signer = signer_from_path(matches, location, name, wallet_manager)?; + let signer_pubkey = signer.pubkey(); + Ok((Some(signer), Some(signer_pubkey))) + } else { + Ok((None, None)) + } +} + +#[deprecated( + since = "1.17.0", + note = "Please use `SignerSource::try_get_pubkey` instead" +)] +pub fn pubkey_of_signer( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, +) -> Result, Box> { + if let Some(location) = matches.try_get_one::(name)? { + Ok(Some(pubkey_from_path( + matches, + location, + name, + wallet_manager, + )?)) + } else { + Ok(None) + } +} + +#[deprecated( + since = "1.17.0", + note = "Please use `SignerSource::try_get_pubkeys` instead" +)] +pub fn pubkeys_of_multiple_signers( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, +) -> Result>, Box> { + if let Some(pubkey_matches) = matches.try_get_many::(name)? { + let mut pubkeys: Vec = vec![]; + for signer in pubkey_matches { + pubkeys.push(pubkey_from_path(matches, signer, name, wallet_manager)?); + } + Ok(Some(pubkeys)) + } else { + Ok(None) + } +} + +// Return pubkey/signature pairs for a string of the form pubkey=signature +#[deprecated( + since = "1.17.0", + note = "Please use `clap::value_parser!(PubkeySignature)` instead" +)] +pub fn pubkeys_sigs_of(matches: &ArgMatches, name: &str) -> Option> { + matches.values_of(name).map(|values| { + values + .map(|pubkey_signer_string| { + let mut signer = pubkey_signer_string.split('='); + let key = Pubkey::from_str(signer.next().unwrap()).unwrap(); + let sig = Signature::from_str(signer.next().unwrap()).unwrap(); + (key, sig) + }) + .collect() + }) +} + +#[deprecated( + since = "1.17.0", + note = "Please use `SignerSource::try_resolve` instead" +)] +pub fn resolve_signer( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, +) -> Result, Box> { + resolve_signer_from_path( + matches, + matches.try_get_one::(name)?.unwrap(), + name, + wallet_manager, + ) +} + #[cfg(test)] mod tests { use { super::*, clap::{Arg, Command}, - solana_sdk::{hash::Hash, pubkey::Pubkey}, + solana_sdk::{hash::Hash, pubkey::Pubkey, signature::write_keypair_file}, + std::fs, }; fn app<'ab>() -> Command<'ab> { @@ -315,6 +428,13 @@ mod tests { .arg(Arg::new("unit").takes_value(true).long("unit")) } + fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String { + use std::env; + let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + + format!("{out_dir}/tmp/{name}-{pubkey}") + } + #[test] fn test_values_of() { let matches = app().get_matches_from(vec!["test", "--multiple", "50", "--multiple", "39"]); @@ -545,4 +665,83 @@ mod tests { } } } + + #[test] + #[allow(deprecated)] + fn test_keypair_of() { + let keypair = Keypair::new(); + let outfile = tmp_file_path("test_keypair_of.json", &keypair.pubkey()); + let _ = write_keypair_file(&keypair, &outfile).unwrap(); + + let matches = app().get_matches_from(vec!["test", "--single", &outfile]); + assert_eq!( + keypair_of(&matches, "single").unwrap().pubkey(), + keypair.pubkey() + ); + assert!(keypair_of(&matches, "multiple").is_none()); + + let matches = app().get_matches_from(vec!["test", "--single", "random_keypair_file.json"]); + assert!(keypair_of(&matches, "single").is_none()); + + fs::remove_file(&outfile).unwrap(); + } + + #[test] + #[allow(deprecated)] + fn test_pubkey_of() { + let keypair = Keypair::new(); + let outfile = tmp_file_path("test_pubkey_of.json", &keypair.pubkey()); + let _ = write_keypair_file(&keypair, &outfile).unwrap(); + + let matches = app().get_matches_from(vec!["test", "--single", &outfile]); + assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey())); + assert_eq!(pubkey_of(&matches, "multiple"), None); + + let matches = + app().get_matches_from(vec!["test", "--single", &keypair.pubkey().to_string()]); + assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey())); + + let matches = app().get_matches_from(vec!["test", "--single", "random_keypair_file.json"]); + assert_eq!(pubkey_of(&matches, "single"), None); + + fs::remove_file(&outfile).unwrap(); + } + + #[test] + #[allow(deprecated)] + fn test_pubkeys_of() { + let keypair = Keypair::new(); + let outfile = tmp_file_path("test_pubkeys_of.json", &keypair.pubkey()); + let _ = write_keypair_file(&keypair, &outfile).unwrap(); + + let matches = app().get_matches_from(vec![ + "test", + "--multiple", + &keypair.pubkey().to_string(), + "--multiple", + &outfile, + ]); + assert_eq!( + pubkeys_of(&matches, "multiple"), + Some(vec![keypair.pubkey(), keypair.pubkey()]) + ); + fs::remove_file(&outfile).unwrap(); + } + + #[test] + #[allow(deprecated)] + fn test_pubkeys_sigs_of() { + let key1 = solana_sdk::pubkey::new_rand(); + let key2 = solana_sdk::pubkey::new_rand(); + let sig1 = Keypair::new().sign_message(&[0u8]); + let sig2 = Keypair::new().sign_message(&[1u8]); + let signer1 = format!("{key1}={sig1}"); + let signer2 = format!("{key2}={sig2}"); + let matches = + app().get_matches_from(vec!["test", "--multiple", &signer1, "--multiple", &signer2]); + assert_eq!( + pubkeys_sigs_of(&matches, "multiple"), + Some(vec![(key1, sig1), (key2, sig2)]) + ); + } } diff --git a/clap-v3-utils/src/input_parsers/signer.rs b/clap-v3-utils/src/input_parsers/signer.rs index 28425a95a05465..3daa0fc1df4c88 100644 --- a/clap-v3-utils/src/input_parsers/signer.rs +++ b/clap-v3-utils/src/input_parsers/signer.rs @@ -1,145 +1,360 @@ use { crate::{ - input_parsers::{keypair_of, keypairs_of, pubkey_of, pubkeys_of}, - keypair::{pubkey_from_path, resolve_signer_from_path, signer_from_path}, + input_parsers::STDOUT_OUTFILE_TOKEN, + keypair::{ + keypair_from_source, pubkey_from_source, resolve_signer_from_source, + signer_from_source, ASK_KEYWORD, + }, + }, + clap::{builder::ValueParser, ArgMatches}, + solana_remote_wallet::{ + locator::{Locator as RemoteWalletLocator, LocatorError as RemoteWalletLocatorError}, + remote_wallet::RemoteWalletManager, }, - clap::ArgMatches, - solana_remote_wallet::remote_wallet::RemoteWalletManager, solana_sdk::{ + derivation_path::{DerivationPath, DerivationPathError}, pubkey::Pubkey, signature::{Keypair, Signature, Signer}, }, std::{error, rc::Rc, str::FromStr}, + thiserror::Error, }; -// Sentinel value used to indicate to write to screen instead of file -pub const STDOUT_OUTFILE_TOKEN: &str = "-"; - -// Return the keypair for an argument with filename `name` or `None` if not present wrapped inside `Result`. -pub fn try_keypair_of( - matches: &ArgMatches, - name: &str, -) -> Result, Box> { - matches.try_contains_id(name)?; - Ok(keypair_of(matches, name)) +const SIGNER_SOURCE_PROMPT: &str = "prompt"; +const SIGNER_SOURCE_FILEPATH: &str = "file"; +const SIGNER_SOURCE_USB: &str = "usb"; +const SIGNER_SOURCE_STDIN: &str = "stdin"; +const SIGNER_SOURCE_PUBKEY: &str = "pubkey"; + +#[derive(Debug, Error)] +pub enum SignerSourceError { + #[error("unrecognized signer source")] + UnrecognizedSource, + #[error(transparent)] + RemoteWalletLocatorError(#[from] RemoteWalletLocatorError), + #[error(transparent)] + DerivationPathError(#[from] DerivationPathError), + #[error(transparent)] + IoError(#[from] std::io::Error), + #[error("unsupported source")] + UnsupportedSource, } -pub fn try_keypairs_of( - matches: &ArgMatches, - name: &str, -) -> Result>, Box> { - matches.try_contains_id(name)?; - Ok(keypairs_of(matches, name)) +#[derive(Clone)] +pub enum SignerSourceKind { + Prompt, + Filepath(String), + Usb(RemoteWalletLocator), + Stdin, + Pubkey(Pubkey), } -// Return a `Result` wrapped pubkey for an argument that can itself be parsed into a pubkey, -// or is a filename that can be read as a keypair -pub fn try_pubkey_of( - matches: &ArgMatches, - name: &str, -) -> Result, Box> { - matches.try_contains_id(name)?; - Ok(pubkey_of(matches, name)) +impl AsRef for SignerSourceKind { + fn as_ref(&self) -> &str { + match self { + Self::Prompt => SIGNER_SOURCE_PROMPT, + Self::Filepath(_) => SIGNER_SOURCE_FILEPATH, + Self::Usb(_) => SIGNER_SOURCE_USB, + Self::Stdin => SIGNER_SOURCE_STDIN, + Self::Pubkey(_) => SIGNER_SOURCE_PUBKEY, + } + } } -pub fn try_pubkeys_of( - matches: &ArgMatches, - name: &str, -) -> Result>, Box> { - matches.try_contains_id(name)?; - Ok(pubkeys_of(matches, name)) +impl std::fmt::Debug for SignerSourceKind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let s: &str = self.as_ref(); + write!(f, "{s}") + } } -// Return pubkey/signature pairs for a string of the form pubkey=signature -pub fn pubkeys_sigs_of(matches: &ArgMatches, name: &str) -> Option> { - matches.values_of(name).map(|values| { - values - .map(|pubkey_signer_string| { - let mut signer = pubkey_signer_string.split('='); - let key = Pubkey::from_str(signer.next().unwrap()).unwrap(); - let sig = Signature::from_str(signer.next().unwrap()).unwrap(); - (key, sig) - }) - .collect() - }) +#[derive(Debug, Clone)] +pub struct SignerSource { + pub kind: SignerSourceKind, + pub derivation_path: Option, + pub legacy: bool, } -// Return pubkey/signature pairs for a string of the form pubkey=signature wrapped inside `Result` -#[allow(clippy::type_complexity)] -pub fn try_pubkeys_sigs_of( - matches: &ArgMatches, - name: &str, -) -> Result>, Box> { - matches.try_contains_id(name)?; - Ok(pubkeys_sigs_of(matches, name)) -} +impl SignerSource { + fn new(kind: SignerSourceKind) -> Self { + Self { + kind, + derivation_path: None, + legacy: false, + } + } -// Return a signer from matches at `name` -#[allow(clippy::type_complexity)] -pub fn signer_of( - matches: &ArgMatches, - name: &str, - wallet_manager: &mut Option>, -) -> Result<(Option>, Option), Box> { - if let Some(location) = matches.try_get_one::(name)? { - let signer = signer_from_path(matches, location, name, wallet_manager)?; - let signer_pubkey = signer.pubkey(); - Ok((Some(signer), Some(signer_pubkey))) - } else { - Ok((None, None)) + fn new_legacy(kind: SignerSourceKind) -> Self { + Self { + kind, + derivation_path: None, + legacy: true, + } } -} -pub fn pubkey_of_signer( - matches: &ArgMatches, - name: &str, - wallet_manager: &mut Option>, -) -> Result, Box> { - if let Some(location) = matches.try_get_one::(name)? { - Ok(Some(pubkey_from_path( - matches, - location, - name, - wallet_manager, - )?)) - } else { - Ok(None) + pub fn try_get_keypair( + matches: &ArgMatches, + name: &str, + ) -> Result, Box> { + let source = matches.try_get_one::(name)?; + if let Some(source) = source { + keypair_from_source(matches, source, name, true).map(Some) + } else { + Ok(None) + } + } + + pub fn try_get_keypairs( + matches: &ArgMatches, + name: &str, + ) -> Result>, Box> { + let sources = matches.try_get_many::(name)?; + if let Some(sources) = sources { + let keypairs = sources + .filter_map(|source| keypair_from_source(matches, source, name, true).ok()) + .collect(); + Ok(Some(keypairs)) + } else { + Ok(None) + } + } + + #[allow(clippy::type_complexity)] + pub fn try_get_signer( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, + ) -> Result, Pubkey)>, Box> { + let source = matches.try_get_one::(name)?; + if let Some(source) = source { + let signer = signer_from_source(matches, source, name, wallet_manager)?; + let signer_pubkey = signer.pubkey(); + Ok(Some((signer, signer_pubkey))) + } else { + Ok(None) + } + } + + #[allow(clippy::type_complexity)] + pub fn try_get_signers( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, + ) -> Result, Pubkey)>>, Box> { + let sources = matches.try_get_many::(name)?; + if let Some(sources) = sources { + let signers = sources + .filter_map(|source| { + let signer = signer_from_source(matches, source, name, wallet_manager).ok()?; + let signer_pubkey = signer.pubkey(); + Some((signer, signer_pubkey)) + }) + .collect(); + Ok(Some(signers)) + } else { + Ok(None) + } + } + + pub fn try_get_pubkey( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, + ) -> Result, Box> { + let source = matches.try_get_one::(name)?; + if let Some(source) = source { + pubkey_from_source(matches, source, name, wallet_manager).map(Some) + } else { + Ok(None) + } + } + + pub fn try_get_pubkeys( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, + ) -> Result>, Box> { + let sources = matches.try_get_many::(name)?; + if let Some(sources) = sources { + let pubkeys = sources + .filter_map(|source| pubkey_from_source(matches, source, name, wallet_manager).ok()) + .collect(); + Ok(Some(pubkeys)) + } else { + Ok(None) + } + } + + pub fn try_resolve( + matches: &ArgMatches, + name: &str, + wallet_manager: &mut Option>, + ) -> Result, Box> { + let source = matches.try_get_one::(name)?; + if let Some(source) = source { + resolve_signer_from_source(matches, source, name, wallet_manager) + } else { + Ok(None) + } + } + + pub(crate) fn parse>(source: S) -> Result { + let source = source.as_ref(); + let source = { + #[cfg(target_family = "windows")] + { + // trim matched single-quotes since cmd.exe won't + let mut source = source; + while let Some(trimmed) = source.strip_prefix('\'') { + source = if let Some(trimmed) = trimmed.strip_suffix('\'') { + trimmed + } else { + break; + } + } + source.replace('\\', "/") + } + #[cfg(not(target_family = "windows"))] + { + source.to_string() + } + }; + match uriparse::URIReference::try_from(source.as_str()) { + Err(_) => Err(SignerSourceError::UnrecognizedSource), + Ok(uri) => { + if let Some(scheme) = uri.scheme() { + let scheme = scheme.as_str().to_ascii_lowercase(); + match scheme.as_str() { + SIGNER_SOURCE_PROMPT => Ok(SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: DerivationPath::from_uri_any_query(&uri)?, + legacy: false, + }), + SIGNER_SOURCE_FILEPATH => Ok(SignerSource::new( + SignerSourceKind::Filepath(uri.path().to_string()), + )), + SIGNER_SOURCE_USB => Ok(SignerSource { + kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?), + derivation_path: DerivationPath::from_uri_key_query(&uri)?, + legacy: false, + }), + SIGNER_SOURCE_STDIN => Ok(SignerSource::new(SignerSourceKind::Stdin)), + _ => { + #[cfg(target_family = "windows")] + // On Windows, an absolute path's drive letter will be parsed as the URI + // scheme. Assume a filepath source in case of a single character shceme. + if scheme.len() == 1 { + return Ok(SignerSource::new(SignerSourceKind::Filepath(source))); + } + Err(SignerSourceError::UnrecognizedSource) + } + } + } else { + match source.as_str() { + STDOUT_OUTFILE_TOKEN => Ok(SignerSource::new(SignerSourceKind::Stdin)), + ASK_KEYWORD => Ok(SignerSource::new_legacy(SignerSourceKind::Prompt)), + _ => match Pubkey::from_str(source.as_str()) { + Ok(pubkey) => Ok(SignerSource::new(SignerSourceKind::Pubkey(pubkey))), + Err(_) => std::fs::metadata(source.as_str()) + .map(|_| SignerSource::new(SignerSourceKind::Filepath(source))) + .map_err(|err| err.into()), + }, + } + } + } + } } } -pub fn pubkeys_of_multiple_signers( - matches: &ArgMatches, - name: &str, - wallet_manager: &mut Option>, -) -> Result>, Box> { - if let Some(pubkey_matches) = matches.try_get_many::(name)? { - let mut pubkeys: Vec = vec![]; - for signer in pubkey_matches { - pubkeys.push(pubkey_from_path(matches, signer, name, wallet_manager)?); +#[derive(Debug)] +pub struct SignerSourceParserBuilder { + prompt: bool, + file_path: bool, + usb: bool, + stdin: bool, + pubkey: bool, + allow_legacy: bool, +} + +impl Default for SignerSourceParserBuilder { + fn default() -> Self { + Self { + prompt: true, + file_path: true, + usb: true, + stdin: true, + pubkey: true, + allow_legacy: true, } - Ok(Some(pubkeys)) - } else { - Ok(None) } } -pub fn resolve_signer( - matches: &ArgMatches, - name: &str, - wallet_manager: &mut Option>, -) -> Result, Box> { - resolve_signer_from_path( - matches, - matches.try_get_one::(name)?.unwrap(), - name, - wallet_manager, - ) +impl SignerSourceParserBuilder { + pub fn new() -> Self { + Self { + prompt: false, + file_path: false, + usb: false, + stdin: false, + pubkey: false, + allow_legacy: true, + } + } + + pub fn allow_prompt(mut self) -> Self { + self.prompt = true; + self + } + + pub fn allow_file_path(mut self) -> Self { + self.file_path = true; + self + } + + pub fn allow_usb(mut self) -> Self { + self.usb = true; + self + } + + pub fn allow_stdin(mut self) -> Self { + self.stdin = true; + self + } + + pub fn allow_pubkey(mut self) -> Self { + self.pubkey = true; + self + } + + pub fn disallow_legacy(mut self) -> Self { + self.allow_legacy = false; + self + } + + pub fn build(self) -> ValueParser { + ValueParser::from( + move |arg: &str| -> Result { + let signer_source = SignerSource::parse(arg)?; + if !self.allow_legacy && signer_source.legacy { + return Err(SignerSourceError::UnsupportedSource); + } + match signer_source.kind { + SignerSourceKind::Prompt if self.prompt => Ok(signer_source), + SignerSourceKind::Filepath(_) if self.file_path => Ok(signer_source), + SignerSourceKind::Usb(_) if self.usb => Ok(signer_source), + SignerSourceKind::Stdin if self.stdin => Ok(signer_source), + SignerSourceKind::Pubkey(_) if self.pubkey => Ok(signer_source), + _ => Err(SignerSourceError::UnsupportedSource), + } + }, + ) + } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub struct PubkeySignature { - pubkey: Pubkey, - signature: Signature, + pub pubkey: Pubkey, + pub signature: Signature, } impl FromStr for PubkeySignature { type Err = String; @@ -164,141 +379,414 @@ impl FromStr for PubkeySignature { mod tests { use { super::*, + assert_matches::assert_matches, clap::{Arg, Command}, - solana_sdk::signature::write_keypair_file, - std::fs, + solana_remote_wallet::locator::Manufacturer, + tempfile::NamedTempFile, }; - fn app<'ab>() -> Command<'ab> { - Command::new("test") - .arg( - Arg::new("multiple") - .long("multiple") - .takes_value(true) - .multiple_occurrences(true) - .multiple_values(true), - ) - .arg(Arg::new("single").takes_value(true).long("single")) - .arg(Arg::new("unit").takes_value(true).long("unit")) - } - - fn tmp_file_path(name: &str, pubkey: &Pubkey) -> String { - use std::env; - let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + #[test] + fn test_parse_pubkey_signature() { + let command = Command::new("test").arg( + Arg::new("pubkeysig") + .long("pubkeysig") + .takes_value(true) + .value_parser(clap::value_parser!(PubkeySignature)), + ); - format!("{out_dir}/tmp/{name}-{pubkey}") - } + // success case + let matches = command + .clone() + .try_get_matches_from(vec![ + "test", + "--pubkeysig", + "11111111111111111111111111111111=4TpFuec1u4BZfxgHg2VQXwvBHANZuNSJHmgrU34GViLAM5uYZ8t7uuhWMHN4k9r41B2p9mwnHjPGwTmTxyvCZw63" + ] + ) + .unwrap(); - #[test] - fn test_keypair_of() { - let keypair = Keypair::new(); - let outfile = tmp_file_path("test_keypair_of.json", &keypair.pubkey()); - let _ = write_keypair_file(&keypair, &outfile).unwrap(); + let expected = PubkeySignature { + pubkey: Pubkey::from_str("11111111111111111111111111111111").unwrap(), + signature: Signature::from_str("4TpFuec1u4BZfxgHg2VQXwvBHANZuNSJHmgrU34GViLAM5uYZ8t7uuhWMHN4k9r41B2p9mwnHjPGwTmTxyvCZw63").unwrap(), + }; - let matches = app().get_matches_from(vec!["test", "--single", &outfile]); assert_eq!( - keypair_of(&matches, "single").unwrap().pubkey(), - keypair.pubkey() + *matches.get_one::("pubkeysig").unwrap(), + expected, ); - assert!(keypair_of(&matches, "multiple").is_none()); - - let matches = app().get_matches_from(vec!["test", "--single", "random_keypair_file.json"]); - assert!(keypair_of(&matches, "single").is_none()); - fs::remove_file(&outfile).unwrap(); + // validation fails + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--pubkeysig", "this_is_an_invalid_arg"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] - fn test_pubkey_of() { - let keypair = Keypair::new(); - let outfile = tmp_file_path("test_pubkey_of.json", &keypair.pubkey()); - let _ = write_keypair_file(&keypair, &outfile).unwrap(); + fn test_parse_keypair_source() { + let command = Command::new("test").arg( + Arg::new("keypair") + .long("keypair") + .takes_value(true) + .value_parser(SignerSourceParserBuilder::new().allow_file_path().build()), + ); + + // success case + let file0 = NamedTempFile::new().unwrap(); + let path = file0.path(); + let path_str = path.to_str().unwrap(); - let matches = app().get_matches_from(vec!["test", "--single", &outfile]); - assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey())); - assert_eq!(pubkey_of(&matches, "multiple"), None); + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", path_str]) + .unwrap(); - let matches = - app().get_matches_from(vec!["test", "--single", &keypair.pubkey().to_string()]); - assert_eq!(pubkey_of(&matches, "single"), Some(keypair.pubkey())); + let signer_source = matches.get_one::("keypair").unwrap(); - let matches = app().get_matches_from(vec!["test", "--single", "random_keypair_file.json"]); - assert_eq!(pubkey_of(&matches, "single"), None); + assert!(matches!(signer_source, SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } + if p == path_str)); - fs::remove_file(&outfile).unwrap(); + // faile cases + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "-"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "usb::/ledger"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] - fn test_pubkeys_of() { - let keypair = Keypair::new(); - let outfile = tmp_file_path("test_pubkeys_of.json", &keypair.pubkey()); - let _ = write_keypair_file(&keypair, &outfile).unwrap(); - - let matches = app().get_matches_from(vec![ - "test", - "--multiple", - &keypair.pubkey().to_string(), - "--multiple", - &outfile, - ]); - assert_eq!( - pubkeys_of(&matches, "multiple"), - Some(vec![keypair.pubkey(), keypair.pubkey()]) + fn test_parse_keypair_or_ask_keyword_source() { + // allow `ASK` keyword + let command = Command::new("test").arg( + Arg::new("keypair") + .long("keypair") + .takes_value(true) + .value_parser( + SignerSourceParserBuilder::new() + .allow_file_path() + .allow_prompt() + .build(), + ), ); - fs::remove_file(&outfile).unwrap(); - } - #[test] - fn test_pubkeys_sigs_of() { - let key1 = solana_sdk::pubkey::new_rand(); - let key2 = solana_sdk::pubkey::new_rand(); - let sig1 = Keypair::new().sign_message(&[0u8]); - let sig2 = Keypair::new().sign_message(&[1u8]); - let signer1 = format!("{key1}={sig1}"); - let signer2 = format!("{key2}={sig2}"); - let matches = - app().get_matches_from(vec!["test", "--multiple", &signer1, "--multiple", &signer2]); - assert_eq!( - pubkeys_sigs_of(&matches, "multiple"), - Some(vec![(key1, sig1), (key2, sig2)]) + // success cases + let file0 = NamedTempFile::new().unwrap(); + let path = file0.path(); + let path_str = path.to_str().unwrap(); + + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", path_str]) + .unwrap(); + let signer_source = matches.get_one::("keypair").unwrap(); + assert!(matches!(signer_source, SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } + if p == path_str)); + + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "ASK"]) + .unwrap(); + let signer_source = matches.get_one::("keypair").unwrap(); + assert_matches!( + signer_source, + SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: None, + legacy: true, + } + ); + + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "prompt:"]) + .unwrap(); + let signer_source = matches.get_one::("keypair").unwrap(); + assert_matches!( + signer_source, + SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: None, + legacy: false, + } + ); + + // faile cases + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "-"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "usb::/ledger"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + + // disallow `ASK` keyword + let command = Command::new("test").arg( + Arg::new("keypair") + .long("keypair") + .takes_value(true) + .value_parser( + SignerSourceParserBuilder::new() + .allow_file_path() + .allow_prompt() + .disallow_legacy() + .build(), + ), ); + + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "ASK"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); } #[test] - fn test_parse_pubkey_signature() { + fn test_parse_prompt_signer_source() { let command = Command::new("test").arg( - Arg::new("pubkeysig") - .long("pubkeysig") + Arg::new("keypair") + .long("keypair") .takes_value(true) - .value_parser(clap::value_parser!(PubkeySignature)), + .value_parser(SignerSourceParserBuilder::new().allow_prompt().build()), ); // success case let matches = command .clone() - .try_get_matches_from(vec![ - "test", - "--pubkeysig", - "11111111111111111111111111111111=4TpFuec1u4BZfxgHg2VQXwvBHANZuNSJHmgrU34GViLAM5uYZ8t7uuhWMHN4k9r41B2p9mwnHjPGwTmTxyvCZw63" - ] - ) + .try_get_matches_from(vec!["test", "--keypair", "ASK"]) .unwrap(); + let signer_source = matches.get_one::("keypair").unwrap(); + assert_matches!( + signer_source, + SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: None, + legacy: true, + } + ); - let expected = PubkeySignature { - pubkey: Pubkey::from_str("11111111111111111111111111111111").unwrap(), - signature: Signature::from_str("4TpFuec1u4BZfxgHg2VQXwvBHANZuNSJHmgrU34GViLAM5uYZ8t7uuhWMHN4k9r41B2p9mwnHjPGwTmTxyvCZw63").unwrap(), - }; + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "prompt:"]) + .unwrap(); + let signer_source = matches.get_one::("keypair").unwrap(); + assert_matches!( + signer_source, + SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: None, + legacy: false, + } + ); - assert_eq!( - *matches.get_one::("pubkeysig").unwrap(), - expected, + // faile cases + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "-"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--keypair", "usb::/ledger"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + } + + #[test] + fn test_parse_pubkey_or_keypair_signer_source() { + let command = Command::new("test").arg( + Arg::new("signer") + .long("signer") + .takes_value(true) + .value_parser( + SignerSourceParserBuilder::new() + .allow_pubkey() + .allow_file_path() + .build(), + ), ); - // validation fails + // success cases + let pubkey = Pubkey::new_unique(); + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--signer", &pubkey.to_string()]) + .unwrap(); + let signer_source = matches.get_one::("signer").unwrap(); + assert!(matches!( + signer_source, + SignerSource { + kind: SignerSourceKind::Pubkey(p), + derivation_path: None, + legacy: false, + } + if *p == pubkey)); + + let file0 = NamedTempFile::new().unwrap(); + let path = file0.path(); + let path_str = path.to_str().unwrap(); + let matches = command + .clone() + .try_get_matches_from(vec!["test", "--signer", path_str]) + .unwrap(); + let signer_source = matches.get_one::("signer").unwrap(); + assert!(matches!( + signer_source, + SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } + if p == path_str)); + + // faile cases let matches_error = command .clone() - .try_get_matches_from(vec!["test", "--pubkeysig", "this_is_an_invalid_arg"]) + .try_get_matches_from(vec!["test", "--signer", "-"]) .unwrap_err(); assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + + let matches_error = command + .clone() + .try_get_matches_from(vec!["test", "--signer", "usb::/ledger"]) + .unwrap_err(); + assert_eq!(matches_error.kind, clap::error::ErrorKind::ValueValidation); + } + + #[test] + fn test_parse_signer_source() { + assert_matches!( + SignerSource::parse(STDOUT_OUTFILE_TOKEN).unwrap(), + SignerSource { + kind: SignerSourceKind::Stdin, + derivation_path: None, + legacy: false, + } + ); + let stdin = "stdin:".to_string(); + assert_matches!( + SignerSource::parse(stdin).unwrap(), + SignerSource { + kind: SignerSourceKind::Stdin, + derivation_path: None, + legacy: false, + } + ); + assert_matches!( + SignerSource::parse(ASK_KEYWORD).unwrap(), + SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: None, + legacy: true, + } + ); + let pubkey = Pubkey::new_unique(); + assert!( + matches!(SignerSource::parse(pubkey.to_string()).unwrap(), SignerSource { + kind: SignerSourceKind::Pubkey(p), + derivation_path: None, + legacy: false, + } + if p == pubkey) + ); + + // Set up absolute and relative path strs + let file0 = NamedTempFile::new().unwrap(); + let path = file0.path(); + assert!(path.is_absolute()); + let absolute_path_str = path.to_str().unwrap(); + + let file1 = NamedTempFile::new_in(std::env::current_dir().unwrap()).unwrap(); + let path = file1.path().file_name().unwrap().to_str().unwrap(); + let path = std::path::Path::new(path); + assert!(path.is_relative()); + let relative_path_str = path.to_str().unwrap(); + + assert!( + matches!(SignerSource::parse(absolute_path_str).unwrap(), SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } if p == absolute_path_str) + ); + assert!( + matches!(SignerSource::parse(relative_path_str).unwrap(), SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } if p == relative_path_str) + ); + + let usb = "usb://ledger".to_string(); + let expected_locator = RemoteWalletLocator { + manufacturer: Manufacturer::Ledger, + pubkey: None, + }; + assert_matches!(SignerSource::parse(usb).unwrap(), SignerSource { + kind: SignerSourceKind::Usb(u), + derivation_path: None, + legacy: false, + } if u == expected_locator); + let usb = "usb://ledger?key=0/0".to_string(); + let expected_locator = RemoteWalletLocator { + manufacturer: Manufacturer::Ledger, + pubkey: None, + }; + let expected_derivation_path = Some(DerivationPath::new_bip44(Some(0), Some(0))); + assert_matches!(SignerSource::parse(usb).unwrap(), SignerSource { + kind: SignerSourceKind::Usb(u), + derivation_path: d, + legacy: false, + } if u == expected_locator && d == expected_derivation_path); + // Catchall into SignerSource::Filepath fails + let junk = "sometextthatisnotapubkeyorfile".to_string(); + assert!(Pubkey::from_str(&junk).is_err()); + assert_matches!( + SignerSource::parse(&junk), + Err(SignerSourceError::IoError(_)) + ); + + let prompt = "prompt:".to_string(); + assert_matches!( + SignerSource::parse(prompt).unwrap(), + SignerSource { + kind: SignerSourceKind::Prompt, + derivation_path: None, + legacy: false, + } + ); + assert!( + matches!(SignerSource::parse(format!("file:{absolute_path_str}")).unwrap(), SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } if p == absolute_path_str) + ); + assert!( + matches!(SignerSource::parse(format!("file:{relative_path_str}")).unwrap(), SignerSource { + kind: SignerSourceKind::Filepath(p), + derivation_path: None, + legacy: false, + } if p == relative_path_str) + ); } } diff --git a/clap-v3-utils/src/input_validators.rs b/clap-v3-utils/src/input_validators.rs index 4bb40b0cd130af..2084747346a441 100644 --- a/clap-v3-utils/src/input_validators.rs +++ b/clap-v3-utils/src/input_validators.rs @@ -1,5 +1,8 @@ use { - crate::keypair::{parse_signer_source, SignerSourceKind, ASK_KEYWORD}, + crate::{ + input_parsers::signer::{SignerSource, SignerSourceKind}, + keypair::ASK_KEYWORD, + }, chrono::DateTime, solana_sdk::{ clock::{Epoch, Slot}, @@ -59,6 +62,10 @@ where } // Return an error if a pubkey cannot be parsed. +#[deprecated( + since = "1.17.0", + note = "please use `clap::value_parser!(Pubkey)` instead" +)] pub fn is_pubkey(string: &str) -> Result<(), String> { is_parsable_generic::(string) } @@ -76,6 +83,10 @@ where } // Return an error if a keypair file cannot be parsed. +#[deprecated( + since = "1.17.0", + note = "please use `SignerSourceParseBuilder::new().allow_file_path().build()` instead" +)] pub fn is_keypair(string: T) -> Result<(), String> where T: AsRef + Display, @@ -86,6 +97,10 @@ where } // Return an error if a keypair file cannot be parsed +#[deprecated( + since = "1.17.0", + note = "please use `SignerSourceParseBuilder::new().allow_file_path().allow_prompt().build()` instead" +)] pub fn is_keypair_or_ask_keyword(string: T) -> Result<(), String> where T: AsRef + Display, @@ -99,11 +114,15 @@ where } // Return an error if a `SignerSourceKind::Prompt` cannot be parsed +#[deprecated( + since = "1.17.0", + note = "please use `SignerSourceParseBuilder::new().allow_prompt().build()` instead" +)] pub fn is_prompt_signer_source(string: &str) -> Result<(), String> { if string == ASK_KEYWORD { return Ok(()); } - match parse_signer_source(string) + match SignerSource::parse(string) .map_err(|err| format!("{err}"))? .kind { @@ -115,6 +134,11 @@ pub fn is_prompt_signer_source(string: &str) -> Result<(), String> { } // Return an error if string cannot be parsed as pubkey string or keypair file location +#[deprecated( + since = "1.17.0", + note = "please use `SignerSourceParseBuilder::new().allow_pubkey().allow_path().build()` instead" +)] +#[allow(deprecated)] pub fn is_pubkey_or_keypair(string: T) -> Result<(), String> where T: AsRef + Display, @@ -124,11 +148,16 @@ where // Return an error if string cannot be parsed as a pubkey string, or a valid Signer that can // produce a pubkey() +#[deprecated( + since = "1.17.0", + note = "please use `SignerSourceParseBuilder::new().allow_pubkey().allow_path().build()` instead" +)] +#[allow(deprecated)] pub fn is_valid_pubkey(string: T) -> Result<(), String> where T: AsRef + Display, { - match parse_signer_source(string.as_ref()) + match SignerSource::parse(string.as_ref()) .map_err(|err| format!("{err}"))? .kind { @@ -145,6 +174,11 @@ where // when paired with an offline `--signer` argument to provide a Presigner (pubkey + signature). // Clap validators can't check multiple fields at once, so the verification that a `--signer` is // also provided and correct happens in parsing, not in validation. +#[deprecated( + since = "1.17.0", + note = "please use `SignerSourceParseBuilder::new().build()` instead" +)] +#[allow(deprecated)] pub fn is_valid_signer(string: T) -> Result<(), String> where T: AsRef + Display, diff --git a/clap-v3-utils/src/keypair.rs b/clap-v3-utils/src/keypair.rs index 886deabfb4ce73..dd05148848be4b 100644 --- a/clap-v3-utils/src/keypair.rs +++ b/clap-v3-utils/src/keypair.rs @@ -11,7 +11,7 @@ use { crate::{ - input_parsers::{pubkeys_sigs_of, STDOUT_OUTFILE_TOKEN}, + input_parsers::signer::{PubkeySignature, SignerSource, SignerSourceKind}, offline::{SIGNER_ARG, SIGN_ONLY_ARG}, ArgConstant, }, @@ -19,12 +19,11 @@ use { clap::ArgMatches, rpassword::prompt_password, solana_remote_wallet::{ - locator::{Locator as RemoteWalletLocator, LocatorError as RemoteWalletLocatorError}, remote_keypair::generate_remote_keypair, remote_wallet::{maybe_wallet_manager, RemoteWalletError, RemoteWalletManager}, }, solana_sdk::{ - derivation_path::{DerivationPath, DerivationPathError}, + derivation_path::DerivationPath, hash::Hash, message::Message, pubkey::Pubkey, @@ -37,15 +36,12 @@ use { solana_zk_token_sdk::encryption::{auth_encryption::AeKey, elgamal::ElGamalKeypair}, std::{ cell::RefCell, - convert::TryFrom, error, io::{stdin, stdout, Write}, ops::Deref, process::exit, rc::Rc, - str::FromStr, }, - thiserror::Error, }; pub struct SignOnly { @@ -166,7 +162,7 @@ impl DefaultSigner { fn path(&self) -> Result<&str, Box> { if !self.is_path_checked.borrow().deref() { - parse_signer_source(&self.path) + SignerSource::parse(&self.path) .and_then(|s| { if let SignerSourceKind::Filepath(path) = &s.kind { std::fs::metadata(path).map(|_| ()).map_err(|e| e.into()) @@ -371,145 +367,6 @@ impl DefaultSigner { } } -#[derive(Debug)] -pub(crate) struct SignerSource { - pub kind: SignerSourceKind, - pub derivation_path: Option, - pub legacy: bool, -} - -impl SignerSource { - fn new(kind: SignerSourceKind) -> Self { - Self { - kind, - derivation_path: None, - legacy: false, - } - } - - fn new_legacy(kind: SignerSourceKind) -> Self { - Self { - kind, - derivation_path: None, - legacy: true, - } - } -} - -const SIGNER_SOURCE_PROMPT: &str = "prompt"; -const SIGNER_SOURCE_FILEPATH: &str = "file"; -const SIGNER_SOURCE_USB: &str = "usb"; -const SIGNER_SOURCE_STDIN: &str = "stdin"; -const SIGNER_SOURCE_PUBKEY: &str = "pubkey"; - -pub(crate) enum SignerSourceKind { - Prompt, - Filepath(String), - Usb(RemoteWalletLocator), - Stdin, - Pubkey(Pubkey), -} - -impl AsRef for SignerSourceKind { - fn as_ref(&self) -> &str { - match self { - Self::Prompt => SIGNER_SOURCE_PROMPT, - Self::Filepath(_) => SIGNER_SOURCE_FILEPATH, - Self::Usb(_) => SIGNER_SOURCE_USB, - Self::Stdin => SIGNER_SOURCE_STDIN, - Self::Pubkey(_) => SIGNER_SOURCE_PUBKEY, - } - } -} - -impl std::fmt::Debug for SignerSourceKind { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let s: &str = self.as_ref(); - write!(f, "{s}") - } -} - -#[derive(Debug, Error)] -pub(crate) enum SignerSourceError { - #[error("unrecognized signer source")] - UnrecognizedSource, - #[error(transparent)] - RemoteWalletLocatorError(#[from] RemoteWalletLocatorError), - #[error(transparent)] - DerivationPathError(#[from] DerivationPathError), - #[error(transparent)] - IoError(#[from] std::io::Error), -} - -pub(crate) fn parse_signer_source>( - source: S, -) -> Result { - let source = source.as_ref(); - let source = { - #[cfg(target_family = "windows")] - { - // trim matched single-quotes since cmd.exe won't - let mut source = source; - while let Some(trimmed) = source.strip_prefix('\'') { - source = if let Some(trimmed) = trimmed.strip_suffix('\'') { - trimmed - } else { - break; - } - } - source.replace('\\', "/") - } - #[cfg(not(target_family = "windows"))] - { - source.to_string() - } - }; - match uriparse::URIReference::try_from(source.as_str()) { - Err(_) => Err(SignerSourceError::UnrecognizedSource), - Ok(uri) => { - if let Some(scheme) = uri.scheme() { - let scheme = scheme.as_str().to_ascii_lowercase(); - match scheme.as_str() { - SIGNER_SOURCE_PROMPT => Ok(SignerSource { - kind: SignerSourceKind::Prompt, - derivation_path: DerivationPath::from_uri_any_query(&uri)?, - legacy: false, - }), - SIGNER_SOURCE_FILEPATH => Ok(SignerSource::new(SignerSourceKind::Filepath( - uri.path().to_string(), - ))), - SIGNER_SOURCE_USB => Ok(SignerSource { - kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?), - derivation_path: DerivationPath::from_uri_key_query(&uri)?, - legacy: false, - }), - SIGNER_SOURCE_STDIN => Ok(SignerSource::new(SignerSourceKind::Stdin)), - _ => { - #[cfg(target_family = "windows")] - // On Windows, an absolute path's drive letter will be parsed as the URI - // scheme. Assume a filepath source in case of a single character shceme. - if scheme.len() == 1 { - return Ok(SignerSource::new(SignerSourceKind::Filepath(source))); - } - Err(SignerSourceError::UnrecognizedSource) - } - } - } else { - match source.as_str() { - STDOUT_OUTFILE_TOKEN => Ok(SignerSource::new(SignerSourceKind::Stdin)), - ASK_KEYWORD => Ok(SignerSource::new_legacy(SignerSourceKind::Prompt)), - _ => match Pubkey::from_str(source.as_str()) { - Ok(pubkey) => Ok(SignerSource::new(SignerSourceKind::Pubkey(pubkey))), - Err(_) => std::fs::metadata(source.as_str()) - .map(|_| SignerSource::new(SignerSourceKind::Filepath(source))) - .map_err(|err| err.into()), - }, - } - } - } - } -} - pub fn presigner_from_pubkey_sigs( pubkey: &Pubkey, signers: &[(Pubkey, Signature)], @@ -694,6 +551,16 @@ pub fn signer_from_path( signer_from_path_with_config(matches, path, keypair_name, wallet_manager, &config) } +pub fn signer_from_source( + matches: &ArgMatches, + source: &SignerSource, + keypair_name: &str, + wallet_manager: &mut Option>, +) -> Result, Box> { + let config = SignerFromPathConfig::default(); + signer_from_source_with_config(matches, source, keypair_name, wallet_manager, &config) +} + /// Loads a [Signer] from one of several possible sources. /// /// The `path` is not strictly a file system path, but is interpreted as various @@ -757,12 +624,23 @@ pub fn signer_from_path_with_config( keypair_name: &str, wallet_manager: &mut Option>, config: &SignerFromPathConfig, +) -> Result, Box> { + let source = SignerSource::parse(path)?; + signer_from_source_with_config(matches, &source, keypair_name, wallet_manager, config) +} + +pub fn signer_from_source_with_config( + matches: &ArgMatches, + source: &SignerSource, + keypair_name: &str, + wallet_manager: &mut Option>, + config: &SignerFromPathConfig, ) -> Result, Box> { let SignerSource { kind, derivation_path, legacy, - } = parse_signer_source(path)?; + } = source; match kind { SignerSourceKind::Prompt => { let skip_validation = matches.try_contains_id(SKIP_SEED_PHRASE_VALIDATION_ARG.name)?; @@ -770,11 +648,11 @@ pub fn signer_from_path_with_config( keypair_name, skip_validation, false, - derivation_path, - legacy, + derivation_path.clone(), + *legacy, )?)) } - SignerSourceKind::Filepath(path) => match read_keypair_file(&path) { + SignerSourceKind::Filepath(path) => match read_keypair_file(path) { Err(e) => Err(std::io::Error::new( std::io::ErrorKind::Other, format!("could not read keypair file \"{path}\". Run \"solana-keygen new\" to create a keypair file: {e}"), @@ -792,9 +670,10 @@ pub fn signer_from_path_with_config( } if let Some(wallet_manager) = wallet_manager { let confirm_key = matches.try_contains_id("confirm_key").unwrap_or(false); + Ok(Box::new(generate_remote_keypair( - locator, - derivation_path.unwrap_or_default(), + locator.clone(), + derivation_path.clone().unwrap_or_default(), wallet_manager, confirm_key, keypair_name, @@ -804,13 +683,20 @@ pub fn signer_from_path_with_config( } } SignerSourceKind::Pubkey(pubkey) => { - let presigner = pubkeys_sigs_of(matches, SIGNER_ARG.name) - .as_ref() - .and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners)); + let pubkey_signatures = matches.try_get_many::(SIGNER_ARG.name)?; + let presigner = if let Some(pubkey_signatures) = pubkey_signatures { + let pubkey_signatures_vec: Vec<_> = pubkey_signatures + .map(|pubkey_signature| (pubkey_signature.pubkey, pubkey_signature.signature)) + .collect(); + presigner_from_pubkey_sigs(pubkey, &pubkey_signatures_vec) + } else { + None + }; + if let Some(presigner) = presigner { Ok(Box::new(presigner)) } else if config.allow_null_signer || matches.try_contains_id(SIGN_ONLY_ARG.name)? { - Ok(Box::new(NullSigner::new(&pubkey))) + Ok(Box::new(NullSigner::new(pubkey))) } else { Err(std::io::Error::new( std::io::ErrorKind::Other, @@ -865,10 +751,19 @@ pub fn pubkey_from_path( keypair_name: &str, wallet_manager: &mut Option>, ) -> Result> { - let SignerSource { kind, .. } = parse_signer_source(path)?; - match kind { + let source = SignerSource::parse(path)?; + pubkey_from_source(matches, &source, keypair_name, wallet_manager) +} + +pub fn pubkey_from_source( + matches: &ArgMatches, + source: &SignerSource, + keypair_name: &str, + wallet_manager: &mut Option>, +) -> Result> { + match source.kind { SignerSourceKind::Pubkey(pubkey) => Ok(pubkey), - _ => Ok(signer_from_path(matches, path, keypair_name, wallet_manager)?.pubkey()), + _ => Ok(signer_from_source(matches, source, keypair_name, wallet_manager)?.pubkey()), } } @@ -877,12 +772,22 @@ pub fn resolve_signer_from_path( path: &str, keypair_name: &str, wallet_manager: &mut Option>, +) -> Result, Box> { + let source = SignerSource::parse(path)?; + resolve_signer_from_source(matches, &source, keypair_name, wallet_manager) +} + +pub fn resolve_signer_from_source( + matches: &ArgMatches, + source: &SignerSource, + keypair_name: &str, + wallet_manager: &mut Option>, ) -> Result, Box> { let SignerSource { kind, derivation_path, legacy, - } = parse_signer_source(path)?; + } = source; match kind { SignerSourceKind::Prompt => { let skip_validation = matches.try_contains_id(SKIP_SEED_PHRASE_VALIDATION_ARG.name)?; @@ -892,12 +797,12 @@ pub fn resolve_signer_from_path( keypair_name, skip_validation, false, - derivation_path, - legacy, + derivation_path.clone(), + *legacy, ) .map(|_| None) } - SignerSourceKind::Filepath(path) => match read_keypair_file(&path) { + SignerSourceKind::Filepath(path) => match read_keypair_file(path) { Err(e) => Err(std::io::Error::new( std::io::ErrorKind::Other, format!( @@ -921,8 +826,8 @@ pub fn resolve_signer_from_path( if let Some(wallet_manager) = wallet_manager { let confirm_key = matches.try_contains_id("confirm_key").unwrap_or(false); let path = generate_remote_keypair( - locator, - derivation_path.unwrap_or_default(), + locator.clone(), + derivation_path.clone().unwrap_or_default(), wallet_manager, confirm_key, keypair_name, @@ -933,7 +838,7 @@ pub fn resolve_signer_from_path( Err(RemoteWalletError::NoDeviceFound.into()) } } - _ => Ok(Some(path.to_string())), + SignerSourceKind::Pubkey(pubkey) => Ok(Some(pubkey.to_string())), } } @@ -1012,6 +917,20 @@ pub fn keypair_from_path( Ok(keypair) } +pub fn keypair_from_source( + matches: &ArgMatches, + source: &SignerSource, + keypair_name: &str, + confirm_pubkey: bool, +) -> Result> { + let skip_validation = matches.try_contains_id(SKIP_SEED_PHRASE_VALIDATION_ARG.name)?; + let keypair = encodable_key_from_source(source, keypair_name, skip_validation)?; + if confirm_pubkey { + confirm_encodable_keypair_pubkey(&keypair, "pubkey"); + } + Ok(keypair) +} + /// Loads an [ElGamalKeypair] from one of several possible sources. /// /// If `confirm_pubkey` is `true` then after deriving the keypair, the user will @@ -1060,6 +979,20 @@ pub fn elgamal_keypair_from_path( Ok(elgamal_keypair) } +pub fn elgamal_keypair_from_source( + matches: &ArgMatches, + source: &SignerSource, + elgamal_keypair_name: &str, + confirm_pubkey: bool, +) -> Result> { + let skip_validation = matches.try_contains_id(SKIP_SEED_PHRASE_VALIDATION_ARG.name)?; + let elgamal_keypair = encodable_key_from_source(source, elgamal_keypair_name, skip_validation)?; + if confirm_pubkey { + confirm_encodable_keypair_pubkey(&elgamal_keypair, "ElGamal pubkey"); + } + Ok(elgamal_keypair) +} + fn confirm_encodable_keypair_pubkey(keypair: &K, pubkey_label: &str) { let pubkey = keypair.encodable_pubkey().to_string(); println!("Recovered {pubkey_label} `{pubkey:?}`. Continue? (y/n): "); @@ -1111,24 +1044,42 @@ pub fn ae_key_from_path( encodable_key_from_path(path, key_name, skip_validation) } +pub fn ae_key_from_source( + matches: &ArgMatches, + source: &SignerSource, + key_name: &str, +) -> Result> { + let skip_validation = matches.try_contains_id(SKIP_SEED_PHRASE_VALIDATION_ARG.name)?; + encodable_key_from_source(source, key_name, skip_validation) +} + fn encodable_key_from_path( path: &str, keypair_name: &str, skip_validation: bool, +) -> Result> { + let source = SignerSource::parse(path)?; + encodable_key_from_source(&source, keypair_name, skip_validation) +} + +fn encodable_key_from_source( + source: &SignerSource, + keypair_name: &str, + skip_validation: bool, ) -> Result> { let SignerSource { kind, derivation_path, legacy, - } = parse_signer_source(path)?; + } = source; match kind { SignerSourceKind::Prompt => Ok(encodable_key_from_seed_phrase( keypair_name, skip_validation, - derivation_path, - legacy, + derivation_path.clone(), + *legacy, )?), - SignerSourceKind::Filepath(path) => match K::read_from_file(&path) { + SignerSourceKind::Filepath(path) => match K::read_from_file(path) { Err(e) => Err(std::io::Error::new( std::io::ErrorKind::Other, format!( @@ -1266,12 +1217,11 @@ fn sanitize_seed_phrase(seed_phrase: &str) -> String { mod tests { use { super::*, - crate::offline::OfflineArgs, - assert_matches::assert_matches, + crate::{input_parsers::signer::SignerSourceParserBuilder, offline::OfflineArgs}, clap::{Arg, Command}, - solana_remote_wallet::{locator::Manufacturer, remote_wallet::initialize_wallet_manager}, - solana_sdk::{signer::keypair::write_keypair_file, system_instruction}, - tempfile::{NamedTempFile, TempDir}, + solana_remote_wallet::remote_wallet::initialize_wallet_manager, + solana_sdk::{signature::write_keypair_file, system_instruction}, + tempfile::TempDir, }; #[test] @@ -1315,125 +1265,7 @@ mod tests { } #[test] - fn test_parse_signer_source() { - assert_matches!( - parse_signer_source(STDOUT_OUTFILE_TOKEN).unwrap(), - SignerSource { - kind: SignerSourceKind::Stdin, - derivation_path: None, - legacy: false, - } - ); - let stdin = "stdin:".to_string(); - assert_matches!( - parse_signer_source(stdin).unwrap(), - SignerSource { - kind: SignerSourceKind::Stdin, - derivation_path: None, - legacy: false, - } - ); - assert_matches!( - parse_signer_source(ASK_KEYWORD).unwrap(), - SignerSource { - kind: SignerSourceKind::Prompt, - derivation_path: None, - legacy: true, - } - ); - let pubkey = Pubkey::new_unique(); - assert!( - matches!(parse_signer_source(pubkey.to_string()).unwrap(), SignerSource { - kind: SignerSourceKind::Pubkey(p), - derivation_path: None, - legacy: false, - } - if p == pubkey) - ); - - // Set up absolute and relative path strs - let file0 = NamedTempFile::new().unwrap(); - let path = file0.path(); - assert!(path.is_absolute()); - let absolute_path_str = path.to_str().unwrap(); - - let file1 = NamedTempFile::new_in(std::env::current_dir().unwrap()).unwrap(); - let path = file1.path().file_name().unwrap().to_str().unwrap(); - let path = std::path::Path::new(path); - assert!(path.is_relative()); - let relative_path_str = path.to_str().unwrap(); - - assert!( - matches!(parse_signer_source(absolute_path_str).unwrap(), SignerSource { - kind: SignerSourceKind::Filepath(p), - derivation_path: None, - legacy: false, - } if p == absolute_path_str) - ); - assert!( - matches!(parse_signer_source(relative_path_str).unwrap(), SignerSource { - kind: SignerSourceKind::Filepath(p), - derivation_path: None, - legacy: false, - } if p == relative_path_str) - ); - - let usb = "usb://ledger".to_string(); - let expected_locator = RemoteWalletLocator { - manufacturer: Manufacturer::Ledger, - pubkey: None, - }; - assert_matches!(parse_signer_source(usb).unwrap(), SignerSource { - kind: SignerSourceKind::Usb(u), - derivation_path: None, - legacy: false, - } if u == expected_locator); - let usb = "usb://ledger?key=0/0".to_string(); - let expected_locator = RemoteWalletLocator { - manufacturer: Manufacturer::Ledger, - pubkey: None, - }; - let expected_derivation_path = Some(DerivationPath::new_bip44(Some(0), Some(0))); - assert_matches!(parse_signer_source(usb).unwrap(), SignerSource { - kind: SignerSourceKind::Usb(u), - derivation_path: d, - legacy: false, - } if u == expected_locator && d == expected_derivation_path); - // Catchall into SignerSource::Filepath fails - let junk = "sometextthatisnotapubkeyorfile".to_string(); - assert!(Pubkey::from_str(&junk).is_err()); - assert_matches!( - parse_signer_source(&junk), - Err(SignerSourceError::IoError(_)) - ); - - let prompt = "prompt:".to_string(); - assert_matches!( - parse_signer_source(prompt).unwrap(), - SignerSource { - kind: SignerSourceKind::Prompt, - derivation_path: None, - legacy: false, - } - ); - assert!( - matches!(parse_signer_source(format!("file:{absolute_path_str}")).unwrap(), SignerSource { - kind: SignerSourceKind::Filepath(p), - derivation_path: None, - legacy: false, - } if p == absolute_path_str) - ); - assert!( - matches!(parse_signer_source(format!("file:{relative_path_str}")).unwrap(), SignerSource { - kind: SignerSourceKind::Filepath(p), - derivation_path: None, - legacy: false, - } if p == relative_path_str) - ); - } - - #[test] - fn signer_from_path_with_file() -> Result<(), Box> { + fn signer_from_source_with_file() -> Result<(), Box> { let dir = TempDir::new()?; let dir = dir.path(); let keypair_path = dir.join("id.json"); @@ -1448,18 +1280,19 @@ mod tests { .arg( Arg::new("keypair") .required(true) + .value_parser(SignerSourceParserBuilder::default().build()) .help("The signing keypair"), ) .offline_args(); let clap_matches = clap_app.get_matches_from(args); - let keypair_str: String = clap_matches.value_of_t_or_exit("keypair"); + let keypair_source = clap_matches.get_one::("keypair").unwrap(); let wallet_manager = initialize_wallet_manager()?; - let signer = signer_from_path( + let signer = signer_from_source( &clap_matches, - &keypair_str, + keypair_source, "signer", &mut Some(wallet_manager), )?; diff --git a/clap-v3-utils/src/nonce.rs b/clap-v3-utils/src/nonce.rs index 7ea6d5b8fc7284..cc01e6fa67e9c5 100644 --- a/clap-v3-utils/src/nonce.rs +++ b/clap-v3-utils/src/nonce.rs @@ -1,5 +1,5 @@ use { - crate::{input_validators::*, ArgConstant}, + crate::{input_parsers::signer::SignerSourceParserBuilder, ArgConstant}, clap::{Arg, Command}, }; @@ -23,7 +23,12 @@ fn nonce_arg<'a>() -> Arg<'a> { .long(NONCE_ARG.long) .takes_value(true) .value_name("PUBKEY") - .validator(|s| is_valid_pubkey(s)) + .value_parser( + SignerSourceParserBuilder::new() + .allow_pubkey() + .allow_file_path() + .build(), + ) .help(NONCE_ARG.help) } @@ -32,7 +37,7 @@ pub fn nonce_authority_arg<'a>() -> Arg<'a> { .long(NONCE_AUTHORITY_ARG.long) .takes_value(true) .value_name("KEYPAIR") - .validator(|s| is_valid_signer(s)) + .value_parser(SignerSourceParserBuilder::default().build()) .help(NONCE_AUTHORITY_ARG.help) } diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index e6b5289c38e7cb..f4b78dbe1f6081 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -1,4 +1,4 @@ -#![allow(clippy::arithmetic_side_effects)] +#![allow(clippy::arithmetic_side_effects, deprecated)] use { bip39::{Mnemonic, MnemonicType, Seed}, clap::{crate_description, crate_name, value_parser, Arg, ArgMatches, Command}, diff --git a/remote-wallet/src/locator.rs b/remote-wallet/src/locator.rs index ca8e7d696cffe2..756452d898146b 100644 --- a/remote-wallet/src/locator.rs +++ b/remote-wallet/src/locator.rs @@ -87,7 +87,7 @@ impl From for LocatorError { } } -#[derive(Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub struct Locator { pub manufacturer: Manufacturer, pub pubkey: Option, diff --git a/zk-keygen/src/main.rs b/zk-keygen/src/main.rs index b72ef7c2856d1e..4d480faff8503c 100644 --- a/zk-keygen/src/main.rs +++ b/zk-keygen/src/main.rs @@ -1,3 +1,5 @@ +#![allow(deprecated)] + use { bip39::{Mnemonic, MnemonicType, Seed}, clap::{crate_description, crate_name, Arg, ArgMatches, Command},