diff --git a/clap-utils/src/input_parsers.rs b/clap-utils/src/input_parsers.rs index 421f9588bb..1e9b1e6972 100644 --- a/clap-utils/src/input_parsers.rs +++ b/clap-utils/src/input_parsers.rs @@ -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>, +) -> Result, Box> { + Ok(resolve_signer_from_path( + matches, + matches.value_of(name).unwrap(), + name, + wallet_manager, + )?) +} + pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option { value_of(matches, name).map(sol_to_lamports) } diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index c5f40f43ac..9d1895ef41 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -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>, +) -> Result, Box> { + 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"; diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 5490d59ebf..fbeaab417f 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -394,6 +394,7 @@ pub enum CliCommand { Cancel(Pubkey), Confirm(Signature), Pay(PayCommand), + ResolveSigner(Option), ShowAccount { pubkey: Pubkey, output_file: Option, @@ -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 diff --git a/remote-wallet/src/ledger.rs b/remote-wallet/src/ledger.rs index ea81ac3439..f076bd7c33 100644 --- a/remote-wallet/src/ledger.rs +++ b/remote-wallet/src/ledger.rs @@ -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, 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) diff --git a/remote-wallet/src/remote_keypair.rs b/remote-wallet/src/remote_keypair.rs index 648a696a6a..55baa6ae80 100644 --- a/remote-wallet/src/remote_keypair.rs +++ b/remote-wallet/src/remote_keypair.rs @@ -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 { 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) diff --git a/remote-wallet/src/remote_wallet.rs b/remote-wallet/src/remote_wallet.rs index 005f5b29bd..11b18622d7 100644 --- a/remote-wallet/src/remote_wallet.rs +++ b/remote-wallet/src/remote_wallet.rs @@ -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()); + } }