2021-04-29 00:42:21 -07:00
|
|
|
use {
|
|
|
|
crate::{
|
|
|
|
input_parsers::pubkeys_sigs_of,
|
|
|
|
offline::{SIGNER_ARG, SIGN_ONLY_ARG},
|
|
|
|
ArgConstant,
|
2020-02-21 13:55:53 -08:00
|
|
|
},
|
2021-04-29 00:42:21 -07:00
|
|
|
bip39::{Language, Mnemonic, Seed},
|
|
|
|
clap::ArgMatches,
|
|
|
|
rpassword::prompt_password_stderr,
|
|
|
|
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},
|
|
|
|
hash::Hash,
|
|
|
|
message::Message,
|
|
|
|
pubkey::Pubkey,
|
|
|
|
signature::{
|
2021-05-10 18:28:47 -07:00
|
|
|
generate_seed_from_seed_phrase_and_passphrase, keypair_from_seed,
|
|
|
|
keypair_from_seed_and_derivation_path, keypair_from_seed_phrase_and_passphrase,
|
2021-05-03 18:58:56 -07:00
|
|
|
read_keypair, read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer,
|
2021-04-29 00:42:21 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
std::{
|
|
|
|
convert::TryFrom,
|
|
|
|
error,
|
|
|
|
io::{stdin, stdout, Write},
|
|
|
|
process::exit,
|
|
|
|
str::FromStr,
|
|
|
|
sync::Arc,
|
|
|
|
},
|
|
|
|
thiserror::Error,
|
2019-11-22 07:20:40 -08:00
|
|
|
};
|
|
|
|
|
2020-09-22 19:29:32 -07:00
|
|
|
pub struct SignOnly {
|
|
|
|
pub blockhash: Hash,
|
2021-03-12 18:37:39 -08:00
|
|
|
pub message: Option<String>,
|
2020-09-22 19:29:32 -07:00
|
|
|
pub present_signers: Vec<(Pubkey, Signature)>,
|
|
|
|
pub absent_signers: Vec<Pubkey>,
|
|
|
|
pub bad_signers: Vec<Pubkey>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl SignOnly {
|
|
|
|
pub fn has_all_signers(&self) -> bool {
|
|
|
|
self.absent_signers.is_empty() && self.bad_signers.is_empty()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn presigner_of(&self, pubkey: &Pubkey) -> Option<Presigner> {
|
|
|
|
presigner_from_pubkey_sigs(pubkey, &self.present_signers)
|
|
|
|
}
|
|
|
|
}
|
2020-09-22 14:25:10 -07:00
|
|
|
pub type CliSigners = Vec<Box<dyn Signer>>;
|
|
|
|
pub type SignerIndex = usize;
|
|
|
|
pub struct CliSignerInfo {
|
|
|
|
pub signers: CliSigners,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl CliSignerInfo {
|
|
|
|
pub fn index_of(&self, pubkey: Option<Pubkey>) -> Option<usize> {
|
|
|
|
if let Some(pubkey) = pubkey {
|
|
|
|
self.signers
|
|
|
|
.iter()
|
|
|
|
.position(|signer| signer.pubkey() == pubkey)
|
|
|
|
} else {
|
|
|
|
Some(0)
|
|
|
|
}
|
|
|
|
}
|
2020-12-21 13:02:53 -08:00
|
|
|
pub fn index_of_or_none(&self, pubkey: Option<Pubkey>) -> Option<usize> {
|
|
|
|
if let Some(pubkey) = pubkey {
|
|
|
|
self.signers
|
|
|
|
.iter()
|
|
|
|
.position(|signer| signer.pubkey() == pubkey)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
2021-03-15 19:43:07 -07:00
|
|
|
pub fn signers_for_message(&self, message: &Message) -> Vec<&dyn Signer> {
|
|
|
|
self.signers
|
|
|
|
.iter()
|
|
|
|
.filter_map(|k| {
|
|
|
|
if message.signer_keys().contains(&&k.pubkey()) {
|
|
|
|
Some(k.as_ref())
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
}
|
2020-09-22 14:25:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
pub struct DefaultSigner {
|
|
|
|
pub arg_name: String,
|
|
|
|
pub path: String,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DefaultSigner {
|
|
|
|
pub fn generate_unique_signers(
|
|
|
|
&self,
|
|
|
|
bulk_signers: Vec<Option<Box<dyn Signer>>>,
|
|
|
|
matches: &ArgMatches<'_>,
|
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
|
|
) -> Result<CliSignerInfo, Box<dyn error::Error>> {
|
|
|
|
let mut unique_signers = vec![];
|
|
|
|
|
|
|
|
// Determine if the default signer is needed
|
|
|
|
if bulk_signers.iter().any(|signer| signer.is_none()) {
|
|
|
|
let default_signer = self.signer_from_path(matches, wallet_manager)?;
|
|
|
|
unique_signers.push(default_signer);
|
|
|
|
}
|
|
|
|
|
2021-04-18 10:27:36 -07:00
|
|
|
for signer in bulk_signers.into_iter().flatten() {
|
|
|
|
if !unique_signers.iter().any(|s| s == &signer) {
|
|
|
|
unique_signers.push(signer);
|
2020-09-22 14:25:10 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(CliSignerInfo {
|
|
|
|
signers: unique_signers,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn signer_from_path(
|
|
|
|
&self,
|
|
|
|
matches: &ArgMatches,
|
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
|
|
) -> Result<Box<dyn Signer>, Box<dyn std::error::Error>> {
|
|
|
|
signer_from_path(matches, &self.path, &self.arg_name, wallet_manager)
|
|
|
|
}
|
2021-03-24 17:32:30 -07:00
|
|
|
|
|
|
|
pub fn signer_from_path_with_config(
|
|
|
|
&self,
|
|
|
|
matches: &ArgMatches,
|
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
|
|
config: &SignerFromPathConfig,
|
|
|
|
) -> Result<Box<dyn Signer>, Box<dyn std::error::Error>> {
|
|
|
|
signer_from_path_with_config(matches, &self.path, &self.arg_name, wallet_manager, config)
|
|
|
|
}
|
2020-09-22 14:25:10 -07:00
|
|
|
}
|
|
|
|
|
2021-04-29 00:42:21 -07:00
|
|
|
pub(crate) struct SignerSource {
|
|
|
|
pub kind: SignerSourceKind,
|
|
|
|
pub derivation_path: Option<DerivationPath>,
|
2021-05-10 18:28:47 -07:00
|
|
|
pub legacy: bool,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SignerSource {
|
|
|
|
fn new(kind: SignerSourceKind) -> Self {
|
|
|
|
Self {
|
|
|
|
kind,
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn new_legacy(kind: SignerSourceKind) -> Self {
|
|
|
|
Self {
|
|
|
|
kind,
|
|
|
|
derivation_path: None,
|
|
|
|
legacy: true,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub(crate) enum SignerSourceKind {
|
2021-05-10 18:28:47 -07:00
|
|
|
Prompt,
|
2020-02-13 13:08:35 -08:00
|
|
|
Filepath(String),
|
2021-04-29 00:42:21 -07:00
|
|
|
Usb(RemoteWalletLocator),
|
2020-02-13 13:08:35 -08:00
|
|
|
Stdin,
|
2020-02-21 13:55:53 -08:00
|
|
|
Pubkey(Pubkey),
|
2020-02-13 13:08:35 -08:00
|
|
|
}
|
|
|
|
|
2021-04-29 00:42:21 -07:00
|
|
|
#[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<S: AsRef<str>>(
|
|
|
|
source: S,
|
|
|
|
) -> Result<SignerSource, SignerSourceError> {
|
2021-04-15 00:01:58 -07:00
|
|
|
let source = source.as_ref();
|
|
|
|
match uriparse::URIReference::try_from(source) {
|
2021-04-29 00:42:21 -07:00
|
|
|
Err(_) => Err(SignerSourceError::UnrecognizedSource),
|
2021-04-15 00:01:58 -07:00
|
|
|
Ok(uri) => {
|
|
|
|
if let Some(scheme) = uri.scheme() {
|
|
|
|
let scheme = scheme.as_str().to_ascii_lowercase();
|
|
|
|
match scheme.as_str() {
|
2021-05-10 18:28:47 -07:00
|
|
|
"prompt" => Ok(SignerSource {
|
|
|
|
kind: SignerSourceKind::Prompt,
|
2021-05-03 18:58:56 -07:00
|
|
|
derivation_path: DerivationPath::from_uri_any_query(&uri)?,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-05-03 18:58:56 -07:00
|
|
|
}),
|
2021-04-29 00:42:21 -07:00
|
|
|
"file" => Ok(SignerSource::new(SignerSourceKind::Filepath(
|
|
|
|
uri.path().to_string(),
|
|
|
|
))),
|
|
|
|
"stdin" => Ok(SignerSource::new(SignerSourceKind::Stdin)),
|
|
|
|
"usb" => Ok(SignerSource {
|
|
|
|
kind: SignerSourceKind::Usb(RemoteWalletLocator::new_from_uri(&uri)?),
|
2021-05-03 18:58:56 -07:00
|
|
|
derivation_path: DerivationPath::from_uri_key_query(&uri)?,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
}),
|
|
|
|
_ => Err(SignerSourceError::UnrecognizedSource),
|
2021-04-15 00:01:58 -07:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
match source {
|
2021-04-29 00:42:21 -07:00
|
|
|
"-" => Ok(SignerSource::new(SignerSourceKind::Stdin)),
|
2021-05-10 18:28:47 -07:00
|
|
|
ASK_KEYWORD => Ok(SignerSource::new_legacy(SignerSourceKind::Prompt)),
|
2021-04-15 00:01:58 -07:00
|
|
|
_ => match Pubkey::from_str(source) {
|
2021-04-29 00:42:21 -07:00
|
|
|
Ok(pubkey) => Ok(SignerSource::new(SignerSourceKind::Pubkey(pubkey))),
|
|
|
|
Err(_) => std::fs::metadata(source)
|
|
|
|
.map(|_| {
|
|
|
|
SignerSource::new(SignerSourceKind::Filepath(source.to_string()))
|
|
|
|
})
|
|
|
|
.map_err(|err| err.into()),
|
2021-04-15 00:01:58 -07:00
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-13 13:08:35 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-21 13:55:53 -08:00
|
|
|
pub fn presigner_from_pubkey_sigs(
|
|
|
|
pubkey: &Pubkey,
|
|
|
|
signers: &[(Pubkey, Signature)],
|
|
|
|
) -> Option<Presigner> {
|
|
|
|
signers.iter().find_map(|(signer, sig)| {
|
|
|
|
if *signer == *pubkey {
|
|
|
|
Some(Presigner::new(signer, sig))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-03-24 17:32:30 -07:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct SignerFromPathConfig {
|
|
|
|
pub allow_null_signer: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for SignerFromPathConfig {
|
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
|
|
|
allow_null_signer: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-24 16:03:30 -08:00
|
|
|
pub fn signer_from_path(
|
2020-02-21 13:55:53 -08:00
|
|
|
matches: &ArgMatches,
|
|
|
|
path: &str,
|
|
|
|
keypair_name: &str,
|
2020-04-18 11:54:21 -07:00
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
2021-03-24 17:32:30 -07:00
|
|
|
) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
|
|
|
|
let config = SignerFromPathConfig::default();
|
|
|
|
signer_from_path_with_config(matches, path, keypair_name, wallet_manager, &config)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn signer_from_path_with_config(
|
|
|
|
matches: &ArgMatches,
|
|
|
|
path: &str,
|
|
|
|
keypair_name: &str,
|
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
|
|
|
config: &SignerFromPathConfig,
|
2020-02-21 13:55:53 -08:00
|
|
|
) -> Result<Box<dyn Signer>, Box<dyn error::Error>> {
|
2021-04-29 00:42:21 -07:00
|
|
|
let SignerSource {
|
|
|
|
kind,
|
|
|
|
derivation_path,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy,
|
2021-04-29 00:42:21 -07:00
|
|
|
} = parse_signer_source(path)?;
|
|
|
|
match kind {
|
2021-05-10 18:28:47 -07:00
|
|
|
SignerSourceKind::Prompt => {
|
2020-02-21 13:55:53 -08:00
|
|
|
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
|
|
|
Ok(Box::new(keypair_from_seed_phrase(
|
|
|
|
keypair_name,
|
|
|
|
skip_validation,
|
|
|
|
false,
|
2021-05-03 18:58:56 -07:00
|
|
|
derivation_path,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy,
|
2020-02-21 13:55:53 -08:00
|
|
|
)?))
|
|
|
|
}
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
|
2020-03-12 23:20:49 -07:00
|
|
|
Err(e) => Err(std::io::Error::new(
|
|
|
|
std::io::ErrorKind::Other,
|
2020-09-18 15:18:16 -07:00
|
|
|
format!("could not read keypair file \"{}\". Run \"solana-keygen new\" to create a keypair file: {}", path, e),
|
2020-03-08 19:19:34 -07:00
|
|
|
)
|
|
|
|
.into()),
|
|
|
|
Ok(file) => Ok(Box::new(file)),
|
|
|
|
},
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Stdin => {
|
2020-02-21 13:55:53 -08:00
|
|
|
let mut stdin = std::io::stdin();
|
|
|
|
Ok(Box::new(read_keypair(&mut stdin)?))
|
|
|
|
}
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Usb(locator) => {
|
2020-04-18 11:54:21 -07:00
|
|
|
if wallet_manager.is_none() {
|
|
|
|
*wallet_manager = maybe_wallet_manager()?;
|
|
|
|
}
|
2020-02-24 16:03:30 -08:00
|
|
|
if let Some(wallet_manager) = wallet_manager {
|
|
|
|
Ok(Box::new(generate_remote_keypair(
|
2021-04-29 00:42:21 -07:00
|
|
|
locator,
|
|
|
|
derivation_path.unwrap_or_default(),
|
2020-02-24 16:03:30 -08:00
|
|
|
wallet_manager,
|
2020-02-26 14:24:44 -08:00
|
|
|
matches.is_present("confirm_key"),
|
2020-03-06 15:03:23 -08:00
|
|
|
keypair_name,
|
2020-02-24 16:03:30 -08:00
|
|
|
)?))
|
|
|
|
} else {
|
|
|
|
Err(RemoteWalletError::NoDeviceFound.into())
|
|
|
|
}
|
|
|
|
}
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Pubkey(pubkey) => {
|
2020-02-21 13:55:53 -08:00
|
|
|
let presigner = pubkeys_sigs_of(matches, SIGNER_ARG.name)
|
|
|
|
.as_ref()
|
|
|
|
.and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners));
|
|
|
|
if let Some(presigner) = presigner {
|
|
|
|
Ok(Box::new(presigner))
|
2021-03-24 17:32:30 -07:00
|
|
|
} else if config.allow_null_signer || matches.is_present(SIGN_ONLY_ARG.name) {
|
2020-03-18 20:49:38 -07:00
|
|
|
Ok(Box::new(NullSigner::new(&pubkey)))
|
2020-02-21 13:55:53 -08:00
|
|
|
} else {
|
2020-03-12 23:20:49 -07:00
|
|
|
Err(std::io::Error::new(
|
|
|
|
std::io::ErrorKind::Other,
|
2020-03-13 14:54:12 -07:00
|
|
|
format!("missing signature for supplied pubkey: {}", pubkey),
|
2020-02-21 13:55:53 -08:00
|
|
|
)
|
|
|
|
.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-08 23:02:24 -07:00
|
|
|
pub fn pubkey_from_path(
|
|
|
|
matches: &ArgMatches,
|
|
|
|
path: &str,
|
|
|
|
keypair_name: &str,
|
2020-04-18 11:54:21 -07:00
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
2020-03-08 23:02:24 -07:00
|
|
|
) -> Result<Pubkey, Box<dyn error::Error>> {
|
2021-04-29 00:42:21 -07:00
|
|
|
let SignerSource { kind, .. } = parse_signer_source(path)?;
|
|
|
|
match kind {
|
|
|
|
SignerSourceKind::Pubkey(pubkey) => Ok(pubkey),
|
2020-03-08 23:02:24 -07:00
|
|
|
_ => Ok(signer_from_path(matches, path, keypair_name, wallet_manager)?.pubkey()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-14 20:48:41 -07:00
|
|
|
pub fn resolve_signer_from_path(
|
|
|
|
matches: &ArgMatches,
|
|
|
|
path: &str,
|
|
|
|
keypair_name: &str,
|
2020-04-18 11:54:21 -07:00
|
|
|
wallet_manager: &mut Option<Arc<RemoteWalletManager>>,
|
2020-03-14 20:48:41 -07:00
|
|
|
) -> Result<Option<String>, Box<dyn error::Error>> {
|
2021-04-29 00:42:21 -07:00
|
|
|
let SignerSource {
|
|
|
|
kind,
|
|
|
|
derivation_path,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy,
|
2021-04-29 00:42:21 -07:00
|
|
|
} = parse_signer_source(path)?;
|
|
|
|
match kind {
|
2021-05-10 18:28:47 -07:00
|
|
|
SignerSourceKind::Prompt => {
|
2020-03-14 20:48:41 -07:00
|
|
|
let skip_validation = matches.is_present(SKIP_SEED_PHRASE_VALIDATION_ARG.name);
|
|
|
|
// This method validates the seed phrase, but returns `None` because there is no path
|
|
|
|
// on disk or to a device
|
2021-05-10 18:28:47 -07:00
|
|
|
keypair_from_seed_phrase(
|
|
|
|
keypair_name,
|
|
|
|
skip_validation,
|
|
|
|
false,
|
|
|
|
derivation_path,
|
|
|
|
legacy,
|
|
|
|
)
|
|
|
|
.map(|_| None)
|
2020-03-14 20:48:41 -07:00
|
|
|
}
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Filepath(path) => match read_keypair_file(&path) {
|
2020-03-14 20:48:41 -07:00
|
|
|
Err(e) => Err(std::io::Error::new(
|
|
|
|
std::io::ErrorKind::Other,
|
2021-05-10 18:28:47 -07:00
|
|
|
format!(
|
|
|
|
"could not read keypair file \"{}\". \
|
|
|
|
Run \"solana-keygen new\" to create a keypair file: {}",
|
|
|
|
path, e
|
|
|
|
),
|
2020-03-14 20:48:41 -07:00
|
|
|
)
|
|
|
|
.into()),
|
|
|
|
Ok(_) => Ok(Some(path.to_string())),
|
|
|
|
},
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Stdin => {
|
2020-03-14 20:48:41 -07:00
|
|
|
let mut stdin = std::io::stdin();
|
|
|
|
// This method validates the keypair from stdin, but returns `None` because there is no
|
|
|
|
// path on disk or to a device
|
|
|
|
read_keypair(&mut stdin).map(|_| None)
|
|
|
|
}
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSourceKind::Usb(locator) => {
|
2020-04-18 11:54:21 -07:00
|
|
|
if wallet_manager.is_none() {
|
|
|
|
*wallet_manager = maybe_wallet_manager()?;
|
|
|
|
}
|
2020-03-14 20:48:41 -07:00
|
|
|
if let Some(wallet_manager) = wallet_manager {
|
|
|
|
let path = generate_remote_keypair(
|
2021-04-29 00:42:21 -07:00
|
|
|
locator,
|
|
|
|
derivation_path.unwrap_or_default(),
|
2020-03-14 20:48:41 -07:00
|
|
|
wallet_manager,
|
|
|
|
matches.is_present("confirm_key"),
|
|
|
|
keypair_name,
|
|
|
|
)
|
|
|
|
.map(|keypair| keypair.path)?;
|
|
|
|
Ok(Some(path))
|
|
|
|
} else {
|
|
|
|
Err(RemoteWalletError::NoDeviceFound.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_ => Ok(Some(path.to_string())),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-10 18:28:47 -07:00
|
|
|
// Keyword used to indicate that the user should be prompted for a keypair seed phrase
|
2019-11-23 08:55:43 -08:00
|
|
|
pub const ASK_KEYWORD: &str = "ASK";
|
|
|
|
|
|
|
|
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",
|
|
|
|
};
|
|
|
|
|
2019-12-02 19:42:42 -08:00
|
|
|
/// Prompts user for a passphrase and then asks for confirmirmation to check for mistakes
|
|
|
|
pub fn prompt_passphrase(prompt: &str) -> Result<String, Box<dyn error::Error>> {
|
|
|
|
let passphrase = prompt_password_stderr(&prompt)?;
|
|
|
|
if !passphrase.is_empty() {
|
|
|
|
let confirmed = rpassword::prompt_password_stderr("Enter same passphrase again: ")?;
|
|
|
|
if confirmed != passphrase {
|
|
|
|
return Err("Passphrases did not match".into());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(passphrase)
|
|
|
|
}
|
|
|
|
|
2019-11-22 07:20:40 -08:00
|
|
|
/// Reads user input from stdin to retrieve a seed phrase and passphrase for keypair derivation
|
2019-12-06 06:55:00 -08:00
|
|
|
/// Optionally skips validation of seed phrase
|
|
|
|
/// Optionally confirms recovered public key
|
2019-11-25 20:33:15 -08:00
|
|
|
pub fn keypair_from_seed_phrase(
|
2019-11-22 07:20:40 -08:00
|
|
|
keypair_name: &str,
|
|
|
|
skip_validation: bool,
|
2019-12-06 06:55:00 -08:00
|
|
|
confirm_pubkey: bool,
|
2021-05-03 18:58:56 -07:00
|
|
|
derivation_path: Option<DerivationPath>,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: bool,
|
2019-11-22 07:20:40 -08:00
|
|
|
) -> Result<Keypair, Box<dyn error::Error>> {
|
|
|
|
let seed_phrase = prompt_password_stderr(&format!("[{}] seed phrase: ", keypair_name))?;
|
|
|
|
let seed_phrase = seed_phrase.trim();
|
|
|
|
let passphrase_prompt = format!(
|
|
|
|
"[{}] If this seed phrase has an associated passphrase, enter it now. Otherwise, press ENTER to continue: ",
|
|
|
|
keypair_name,
|
|
|
|
);
|
|
|
|
|
2019-12-06 06:55:00 -08:00
|
|
|
let keypair = if skip_validation {
|
2019-12-02 19:42:42 -08:00
|
|
|
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
2021-05-10 18:28:47 -07:00
|
|
|
if legacy {
|
|
|
|
keypair_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase)?
|
|
|
|
} else {
|
|
|
|
let seed = generate_seed_from_seed_phrase_and_passphrase(&seed_phrase, &passphrase);
|
|
|
|
keypair_from_seed_and_derivation_path(&seed, derivation_path)?
|
|
|
|
}
|
2019-11-22 07:20:40 -08:00
|
|
|
} else {
|
2019-12-04 11:40:32 -08:00
|
|
|
let sanitized = sanitize_seed_phrase(seed_phrase);
|
2020-10-16 19:51:53 -07:00
|
|
|
let parse_language_fn = || {
|
|
|
|
for language in &[
|
|
|
|
Language::English,
|
|
|
|
Language::ChineseSimplified,
|
|
|
|
Language::ChineseTraditional,
|
|
|
|
Language::Japanese,
|
|
|
|
Language::Spanish,
|
|
|
|
Language::Korean,
|
|
|
|
Language::French,
|
|
|
|
Language::Italian,
|
|
|
|
] {
|
|
|
|
if let Ok(mnemonic) = Mnemonic::from_phrase(&sanitized, *language) {
|
|
|
|
return Ok(mnemonic);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Err("Can't get mnemonic from seed phrases")
|
|
|
|
};
|
|
|
|
let mnemonic = parse_language_fn()?;
|
2019-12-02 19:42:42 -08:00
|
|
|
let passphrase = prompt_passphrase(&passphrase_prompt)?;
|
2019-11-22 07:20:40 -08:00
|
|
|
let seed = Seed::new(&mnemonic, &passphrase);
|
2021-05-10 18:28:47 -07:00
|
|
|
if legacy {
|
|
|
|
keypair_from_seed(seed.as_bytes())?
|
|
|
|
} else {
|
|
|
|
keypair_from_seed_and_derivation_path(&seed.as_bytes(), derivation_path)?
|
|
|
|
}
|
2019-12-06 06:55:00 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
if confirm_pubkey {
|
2020-02-12 13:15:12 -08:00
|
|
|
let pubkey = keypair.pubkey();
|
2019-12-06 06:55:00 -08:00
|
|
|
print!("Recovered pubkey `{:?}`. Continue? (y/n): ", pubkey);
|
|
|
|
let _ignored = stdout().flush();
|
|
|
|
let mut input = String::new();
|
|
|
|
stdin().read_line(&mut input).expect("Unexpected input");
|
|
|
|
if input.to_lowercase().trim() != "y" {
|
|
|
|
println!("Exiting");
|
|
|
|
exit(1);
|
|
|
|
}
|
2019-11-22 07:20:40 -08:00
|
|
|
}
|
2019-12-06 06:55:00 -08:00
|
|
|
|
|
|
|
Ok(keypair)
|
2019-11-22 07:20:40 -08:00
|
|
|
}
|
|
|
|
|
2019-12-04 11:40:32 -08:00
|
|
|
fn sanitize_seed_phrase(seed_phrase: &str) -> String {
|
|
|
|
seed_phrase
|
|
|
|
.split_whitespace()
|
|
|
|
.collect::<Vec<&str>>()
|
|
|
|
.join(" ")
|
|
|
|
}
|
|
|
|
|
2019-11-22 07:20:40 -08:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2021-04-29 00:42:21 -07:00
|
|
|
use solana_remote_wallet::locator::Manufacturer;
|
2021-03-15 19:43:07 -07:00
|
|
|
use solana_sdk::system_instruction;
|
2021-04-29 00:42:21 -07:00
|
|
|
use tempfile::NamedTempFile;
|
2019-12-04 11:40:32 -08:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_sanitize_seed_phrase() {
|
|
|
|
let seed_phrase = " Mary had\ta\u{2009}little \n\t lamb";
|
|
|
|
assert_eq!(
|
|
|
|
"Mary had a little lamb".to_owned(),
|
|
|
|
sanitize_seed_phrase(seed_phrase)
|
|
|
|
);
|
|
|
|
}
|
2021-03-15 19:43:07 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_signer_info_signers_for_message() {
|
|
|
|
let source = Keypair::new();
|
|
|
|
let fee_payer = Keypair::new();
|
|
|
|
let nonsigner1 = Keypair::new();
|
|
|
|
let nonsigner2 = Keypair::new();
|
|
|
|
let recipient = Pubkey::new_unique();
|
|
|
|
let message = Message::new(
|
|
|
|
&[system_instruction::transfer(
|
|
|
|
&source.pubkey(),
|
|
|
|
&recipient,
|
|
|
|
42,
|
|
|
|
)],
|
|
|
|
Some(&fee_payer.pubkey()),
|
|
|
|
);
|
|
|
|
let signers = vec![
|
|
|
|
Box::new(fee_payer) as Box<dyn Signer>,
|
|
|
|
Box::new(source) as Box<dyn Signer>,
|
|
|
|
Box::new(nonsigner1) as Box<dyn Signer>,
|
|
|
|
Box::new(nonsigner2) as Box<dyn Signer>,
|
|
|
|
];
|
|
|
|
let signer_info = CliSignerInfo { signers };
|
|
|
|
let msg_signers = signer_info.signers_for_message(&message);
|
|
|
|
let signer_pubkeys = msg_signers.iter().map(|s| s.pubkey()).collect::<Vec<_>>();
|
|
|
|
let expect = vec![
|
|
|
|
signer_info.signers[0].pubkey(),
|
|
|
|
signer_info.signers[1].pubkey(),
|
|
|
|
];
|
|
|
|
assert_eq!(signer_pubkeys, expect);
|
|
|
|
}
|
2021-04-15 00:01:58 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_parse_signer_source() {
|
|
|
|
assert!(matches!(
|
2021-04-29 00:42:21 -07:00
|
|
|
parse_signer_source("-").unwrap(),
|
|
|
|
SignerSource {
|
|
|
|
kind: SignerSourceKind::Stdin,
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
|
|
|
));
|
2021-05-10 18:28:47 -07:00
|
|
|
let stdin = "stdin:".to_string();
|
2021-04-29 00:42:21 -07:00
|
|
|
assert!(matches!(
|
2021-05-10 18:28:47 -07:00
|
|
|
parse_signer_source(&stdin).unwrap(),
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSource {
|
|
|
|
kind: SignerSourceKind::Stdin,
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
|
|
|
));
|
|
|
|
assert!(matches!(
|
|
|
|
parse_signer_source(ASK_KEYWORD).unwrap(),
|
|
|
|
SignerSource {
|
2021-05-10 18:28:47 -07:00
|
|
|
kind: SignerSourceKind::Prompt,
|
2021-04-29 00:42:21 -07:00
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: true,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
2021-04-15 00:01:58 -07:00
|
|
|
));
|
|
|
|
let pubkey = Pubkey::new_unique();
|
|
|
|
assert!(
|
2021-04-29 00:42:21 -07:00
|
|
|
matches!(parse_signer_source(&pubkey.to_string()).unwrap(), SignerSource {
|
|
|
|
kind: SignerSourceKind::Pubkey(p),
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
|
|
|
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,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
} if p == absolute_path_str)
|
2021-04-15 00:01:58 -07:00
|
|
|
);
|
2021-04-29 00:42:21 -07:00
|
|
|
assert!(
|
|
|
|
matches!(parse_signer_source(&relative_path_str).unwrap(), SignerSource {
|
|
|
|
kind: SignerSourceKind::Filepath(p),
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
} if p == relative_path_str)
|
|
|
|
);
|
|
|
|
|
2021-04-15 00:01:58 -07:00
|
|
|
let usb = "usb://ledger".to_string();
|
2021-04-29 00:42:21 -07:00
|
|
|
let expected_locator = RemoteWalletLocator {
|
|
|
|
manufacturer: Manufacturer::Ledger,
|
|
|
|
pubkey: None,
|
|
|
|
};
|
|
|
|
assert!(matches!(parse_signer_source(&usb).unwrap(), SignerSource {
|
|
|
|
kind: SignerSourceKind::Usb(u),
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
} 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,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
} if u == expected_locator && d == expected_derivation_path));
|
|
|
|
// Catchall into SignerSource::Filepath fails
|
|
|
|
let junk = "sometextthatisnotapubkeyorfile".to_string();
|
2021-04-15 00:01:58 -07:00
|
|
|
assert!(Pubkey::from_str(&junk).is_err());
|
2021-04-29 00:42:21 -07:00
|
|
|
assert!(matches!(
|
|
|
|
parse_signer_source(&junk),
|
|
|
|
Err(SignerSourceError::IoError(_))
|
|
|
|
));
|
2021-04-15 00:01:58 -07:00
|
|
|
|
2021-05-10 18:28:47 -07:00
|
|
|
let prompt = "prompt:".to_string();
|
2021-04-29 00:42:21 -07:00
|
|
|
assert!(matches!(
|
2021-05-10 18:28:47 -07:00
|
|
|
parse_signer_source(&prompt).unwrap(),
|
2021-04-29 00:42:21 -07:00
|
|
|
SignerSource {
|
2021-05-10 18:28:47 -07:00
|
|
|
kind: SignerSourceKind::Prompt,
|
2021-04-29 00:42:21 -07:00
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
}
|
|
|
|
));
|
2021-04-15 23:08:44 -07:00
|
|
|
assert!(
|
2021-04-29 00:42:21 -07:00
|
|
|
matches!(parse_signer_source(&format!("file:{}", absolute_path_str)).unwrap(), SignerSource {
|
|
|
|
kind: SignerSourceKind::Filepath(p),
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
} if p == absolute_path_str)
|
2021-04-15 23:08:44 -07:00
|
|
|
);
|
|
|
|
assert!(
|
2021-04-29 00:42:21 -07:00
|
|
|
matches!(parse_signer_source(&format!("file:{}", relative_path_str)).unwrap(), SignerSource {
|
|
|
|
kind: SignerSourceKind::Filepath(p),
|
|
|
|
derivation_path: None,
|
2021-05-10 18:28:47 -07:00
|
|
|
legacy: false,
|
2021-04-29 00:42:21 -07:00
|
|
|
} if p == relative_path_str)
|
2021-04-15 23:08:44 -07:00
|
|
|
);
|
2021-04-15 00:01:58 -07:00
|
|
|
}
|
2019-11-22 07:20:40 -08:00
|
|
|
}
|