From 3004eaa9bd527e0ca4731db485ce4dbde368618a Mon Sep 17 00:00:00 2001 From: samkim-crypto Date: Thu, 25 Jan 2024 06:27:02 +0900 Subject: [PATCH] [clap-v3-utils] Add functions to parse directly from `SignerSource` (#34678) * add `_from_source` function variants for signer, keypair, and pubkey * make `parse_signer_source` an associated function of `SignerSource` * refactor `SignerSource` into `input_parsers::signer` * make `_from_source` functions public * remove unnecessary import --- clap-v3-utils/src/input_parsers/signer.rs | 273 ++++++++++++++- clap-v3-utils/src/input_validators.rs | 9 +- clap-v3-utils/src/keypair.rs | 405 ++++++---------------- 3 files changed, 386 insertions(+), 301 deletions(-) diff --git a/clap-v3-utils/src/input_parsers/signer.rs b/clap-v3-utils/src/input_parsers/signer.rs index 468e2dfef95238..d71a37b888646a 100644 --- a/clap-v3-utils/src/input_parsers/signer.rs +++ b/clap-v3-utils/src/input_parsers/signer.rs @@ -1,20 +1,162 @@ use { crate::{ input_parsers::{keypair_of, keypairs_of, pubkey_of, pubkeys_of}, - keypair::{ - parse_signer_source, pubkey_from_path, resolve_signer_from_path, signer_from_path, - SignerSource, SignerSourceError, SignerSourceKind, - }, + keypair::{pubkey_from_path, resolve_signer_from_path, signer_from_path, ASK_KEYWORD}, }, clap::{builder::ValueParser, ArgMatches}, - solana_remote_wallet::remote_wallet::RemoteWalletManager, + solana_remote_wallet::{ + locator::{Locator as RemoteWalletLocator, LocatorError as RemoteWalletLocatorError}, + remote_wallet::RemoteWalletManager, + }, solana_sdk::{ + derivation_path::{DerivationPath, DerivationPathError}, pubkey::Pubkey, signature::{Keypair, Signature, Signer}, }, std::{error, rc::Rc, str::FromStr}, + thiserror::Error, }; +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, +} + +#[derive(Clone)] +pub 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, Clone)] +pub 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, + } + } + + 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()), + }, + } + } + } + } + } +} + // Sentinel value used to indicate to write to screen instead of file pub const STDOUT_OUTFILE_TOKEN: &str = "-"; @@ -72,7 +214,7 @@ impl SignerSourceParserBuilder { pub fn build(self) -> ValueParser { ValueParser::from( move |arg: &str| -> Result { - let signer_source = parse_signer_source(arg)?; + let signer_source = SignerSource::parse(arg)?; if !self.allow_legacy && signer_source.legacy { return Err(SignerSourceError::UnsupportedSource); } @@ -240,11 +382,130 @@ mod tests { super::*, assert_matches::assert_matches, clap::{Arg, Command}, + solana_remote_wallet::locator::Manufacturer, solana_sdk::signature::write_keypair_file, std::fs, tempfile::NamedTempFile, }; + #[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) + ); + } + fn app<'ab>() -> Command<'ab> { Command::new("test") .arg( diff --git a/clap-v3-utils/src/input_validators.rs b/clap-v3-utils/src/input_validators.rs index 7938dec360bdc8..0b3f75e1a6f334 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}, @@ -119,7 +122,7 @@ 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 { @@ -154,7 +157,7 @@ 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 { diff --git a/clap-v3-utils/src/keypair.rs b/clap-v3-utils/src/keypair.rs index 8adbfff3631f8b..7e41b3c82fbbb3 100644 --- a/clap-v3-utils/src/keypair.rs +++ b/clap-v3-utils/src/keypair.rs @@ -11,7 +11,7 @@ use { crate::{ - input_parsers::{signer::try_pubkeys_sigs_of, STDOUT_OUTFILE_TOKEN}, + input_parsers::signer::{try_pubkeys_sigs_of, 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,148 +367,6 @@ impl DefaultSigner { } } -#[derive(Debug, Clone)] -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"; - -#[derive(Clone)] -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), - #[error("unsupported source")] - UnsupportedSource, -} - -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)], @@ -697,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 @@ -760,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)?; @@ -773,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}"), @@ -796,8 +671,8 @@ 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, @@ -809,11 +684,11 @@ pub fn signer_from_path_with_config( SignerSourceKind::Pubkey(pubkey) => { let presigner = try_pubkeys_sigs_of(matches, SIGNER_ARG.name)? .as_ref() - .and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners)); + .and_then(|presigners| presigner_from_pubkey_sigs(pubkey, presigners)); 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, @@ -868,10 +743,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()), } } @@ -880,12 +764,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)?; @@ -895,12 +789,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!( @@ -924,8 +818,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, @@ -936,7 +830,7 @@ pub fn resolve_signer_from_path( Err(RemoteWalletError::NoDeviceFound.into()) } } - _ => Ok(Some(path.to_string())), + SignerSourceKind::Pubkey(pubkey) => Ok(Some(pubkey.to_string())), } } @@ -1015,6 +909,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 @@ -1063,6 +971,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): "); @@ -1114,24 +1036,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!( @@ -1270,11 +1210,10 @@ mod tests { use { super::*, crate::offline::OfflineArgs, - assert_matches::assert_matches, clap::{Arg, Command}, - solana_remote_wallet::{locator::Manufacturer, remote_wallet::initialize_wallet_manager}, + solana_remote_wallet::remote_wallet::initialize_wallet_manager, solana_sdk::{signer::keypair::write_keypair_file, system_instruction}, - tempfile::{NamedTempFile, TempDir}, + tempfile::TempDir, }; #[test] @@ -1317,124 +1256,6 @@ mod tests { assert_eq!(signer_pubkeys, expect); } - #[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> { let dir = TempDir::new()?;