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:
parent
c3c4c9326b
commit
3c2aff2b5b
@ -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)
|
||||
}
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user