Cli: Add resolve-signer subcommand (#8859)

* Expose remote-wallet device pretty path

* Add resolve-signer helpers

* Add cli resolve-signer subcommand

* Print pretty-path in waiting msg
This commit is contained in:
Tyera Eulberg 2020-03-14 20:48:41 -07:00 committed by GitHub
parent c3c4c9326b
commit 3c2aff2b5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 179 additions and 5 deletions

View File

@ -1,6 +1,6 @@
use crate::keypair::{
keypair_from_seed_phrase, pubkey_from_path, signer_from_path, ASK_KEYWORD,
SKIP_SEED_PHRASE_VALIDATION_ARG,
keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path,
ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
};
use chrono::DateTime;
use clap::ArgMatches;
@ -129,6 +129,19 @@ pub fn pubkey_of_signer(
}
}
pub fn resolve_signer(
matches: &ArgMatches<'_>,
name: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
Ok(resolve_signer_from_path(
matches,
matches.value_of(name).unwrap(),
name,
wallet_manager,
)?)
}
pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> {
value_of(matches, name).map(sol_to_lamports)
}

View File

@ -124,6 +124,51 @@ pub fn pubkey_from_path(
}
}
pub fn resolve_signer_from_path(
matches: &ArgMatches,
path: &str,
keypair_name: &str,
wallet_manager: Option<&Arc<RemoteWalletManager>>,
) -> Result<Option<String>, Box<dyn error::Error>> {
match parse_keypair_path(path) {
KeypairUrl::Ask => {
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
keypair_from_seed_phrase(keypair_name, skip_validation, false).map(|_| None)
}
KeypairUrl::Filepath(path) => match read_keypair_file(&path) {
Err(e) => Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("could not find keypair file: {} error: {}", path, e),
)
.into()),
Ok(_) => Ok(Some(path.to_string())),
},
KeypairUrl::Stdin => {
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)
}
KeypairUrl::Usb(path) => {
if let Some(wallet_manager) = wallet_manager {
let path = generate_remote_keypair(
path,
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())),
}
}
// Keyword used to indicate that the user should be asked for a keypair seed phrase
pub const ASK_KEYWORD: &str = "ASK";

View File

@ -394,6 +394,7 @@ pub enum CliCommand {
Cancel(Pubkey),
Confirm(Signature),
Pay(PayCommand),
ResolveSigner(Option<String>),
ShowAccount {
pubkey: Pubkey,
output_file: Option<String>,
@ -863,6 +864,13 @@ pub fn parse_command(
signers: vec![],
})
}
("resolve-signer", Some(matches)) => {
let signer_path = resolve_signer(matches, "signer", wallet_manager)?;
Ok(CliCommandInfo {
command: CliCommand::ResolveSigner(signer_path),
signers: vec![],
})
}
("send-signature", Some(matches)) => {
let to = value_of(matches, "to").unwrap();
let process_id = value_of(matches, "process_id").unwrap();
@ -2027,6 +2035,13 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
*nonce_account,
*nonce_authority,
),
CliCommand::ResolveSigner(path) => {
if let Some(path) = path {
Ok(path.to_string())
} else {
Ok("Signer is valid".to_string())
}
}
CliCommand::ShowAccount {
pubkey,
output_file,
@ -2381,6 +2396,19 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, '
.arg(nonce_arg())
.arg(nonce_authority_arg()),
)
.subcommand(
SubCommand::with_name("resolve-signer")
.about("Checks that a signer is valid, and returns its specific path; useful for signers that may be specified generally, eg. usb://ledger")
.arg(
Arg::with_name("signer")
.index(1)
.value_name("KEYPAIR or PUBKEY or REMOTE WALLET PATH")
.takes_value(true)
.required(true)
.validator(is_valid_signer)
.help("The signer path to resolve")
)
)
.subcommand(
SubCommand::with_name("send-signature")
.about("Send a signature to authorize a transfer")
@ -2759,6 +2787,31 @@ mod tests {
}
);
// Test ResolveSigner Subcommand, KeypairUrl::Filepath
let test_resolve_signer =
test_commands
.clone()
.get_matches_from(vec!["test", "resolve-signer", &keypair_file]);
assert_eq!(
parse_command(&test_resolve_signer, "", None).unwrap(),
CliCommandInfo {
command: CliCommand::ResolveSigner(Some(keypair_file.clone())),
signers: vec![],
}
);
// Test ResolveSigner Subcommand, KeypairUrl::Pubkey (Presigner)
let test_resolve_signer =
test_commands
.clone()
.get_matches_from(vec!["test", "resolve-signer", &pubkey_string]);
assert_eq!(
parse_command(&test_resolve_signer, "", None).unwrap(),
CliCommandInfo {
command: CliCommand::ResolveSigner(Some(pubkey.to_string())),
signers: vec![],
}
);
// Test Simple Pay Subcommand
let test_pay =
test_commands

View File

@ -55,6 +55,7 @@ mod commands {
/// Ledger Wallet device
pub struct LedgerWallet {
pub device: hidapi::HidDevice,
pub pretty_path: String,
}
impl fmt::Debug for LedgerWallet {
@ -65,7 +66,10 @@ impl fmt::Debug for LedgerWallet {
impl LedgerWallet {
pub fn new(device: hidapi::HidDevice) -> Self {
Self { device }
Self {
device,
pretty_path: String::default(),
}
}
// Transport Protocol:
@ -231,7 +235,10 @@ impl LedgerWallet {
) -> Result<Vec<u8>, RemoteWalletError> {
self.write(command, p1, p2, data)?;
if p1 == P1_CONFIRM && is_last_part(p2) {
println!("Waiting for remote wallet to approve...");
println!(
"Waiting for approval from remote wallet {}",
self.pretty_path
);
let result = self.read()?;
println!("{}Approved", CHECK_MARK);
Ok(result)

View File

@ -14,6 +14,7 @@ pub struct RemoteKeypair {
pub wallet_type: RemoteWalletType,
pub derivation_path: DerivationPath,
pub pubkey: Pubkey,
pub path: String,
}
impl RemoteKeypair {
@ -21,6 +22,7 @@ impl RemoteKeypair {
wallet_type: RemoteWalletType,
derivation_path: DerivationPath,
confirm_key: bool,
path: String,
) -> Result<Self, RemoteWalletError> {
let pubkey = match &wallet_type {
RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?,
@ -30,6 +32,7 @@ impl RemoteKeypair {
wallet_type,
derivation_path,
pubkey,
path,
})
}
}
@ -57,10 +60,12 @@ pub fn generate_remote_keypair(
let (remote_wallet_info, derivation_path) = RemoteWalletInfo::parse_path(path)?;
if remote_wallet_info.manufacturer == "ledger" {
let ledger = get_ledger_from_info(remote_wallet_info, keypair_name, wallet_manager)?;
let path = format!("{}{}", ledger.pretty_path, derivation_path.get_query());
Ok(RemoteKeypair::new(
RemoteWalletType::Ledger(ledger),
derivation_path,
confirm_key,
path,
)?)
} else {
Err(RemoteWalletError::DeviceTypeMismatch)

View File

@ -105,8 +105,9 @@ impl RemoteWalletManager {
if is_valid_ledger(device_info.vendor_id(), device_info.product_id()) {
match usb.open_path(&device_info.path()) {
Ok(device) => {
let ledger = LedgerWallet::new(device);
let mut ledger = LedgerWallet::new(device);
if let Ok(info) = ledger.read_device(&device_info) {
ledger.pretty_path = info.get_pretty_path();
let path = device_info.path().to_str().unwrap().to_string();
trace!("Found device: {:?}", info);
v.push(Device {
@ -328,6 +329,20 @@ impl fmt::Debug for DerivationPath {
}
}
impl DerivationPath {
pub fn get_query(&self) -> String {
if let Some(account) = self.account {
if let Some(change) = self.change {
format!("?key={}/{}", account, change)
} else {
format!("?key={}", account)
}
} else {
"".to_string()
}
}
}
/// Helper to determine if a device is a valid HID
pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32
@ -519,4 +534,40 @@ mod tests {
test_info.pubkey = pubkey;
assert!(info.matches(&test_info));
}
#[test]
fn test_get_pretty_path() {
let pubkey = Pubkey::new_rand();
let pubkey_str = pubkey.to_string();
let remote_wallet_info = RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
error: None,
};
assert_eq!(
remote_wallet_info.get_pretty_path(),
format!("usb://ledger/nano-s/{}", pubkey_str)
);
}
#[test]
fn test_get_query() {
let derivation_path = DerivationPath {
account: None,
change: None,
};
assert_eq!(derivation_path.get_query(), "".to_string());
let derivation_path = DerivationPath {
account: Some(1),
change: None,
};
assert_eq!(derivation_path.get_query(), "?key=1".to_string());
let derivation_path = DerivationPath {
account: Some(1),
change: Some(2),
};
assert_eq!(derivation_path.get_query(), "?key=1/2".to_string());
}
}