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:
@ -1,6 +1,6 @@
|
|||||||
use crate::keypair::{
|
use crate::keypair::{
|
||||||
keypair_from_seed_phrase, pubkey_from_path, signer_from_path, ASK_KEYWORD,
|
keypair_from_seed_phrase, pubkey_from_path, resolve_signer_from_path, signer_from_path,
|
||||||
SKIP_SEED_PHRASE_VALIDATION_ARG,
|
ASK_KEYWORD, SKIP_SEED_PHRASE_VALIDATION_ARG,
|
||||||
};
|
};
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use clap::ArgMatches;
|
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> {
|
pub fn lamports_of_sol(matches: &ArgMatches<'_>, name: &str) -> Option<u64> {
|
||||||
value_of(matches, name).map(sol_to_lamports)
|
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
|
// Keyword used to indicate that the user should be asked for a keypair seed phrase
|
||||||
pub const ASK_KEYWORD: &str = "ASK";
|
pub const ASK_KEYWORD: &str = "ASK";
|
||||||
|
|
||||||
|
@ -394,6 +394,7 @@ pub enum CliCommand {
|
|||||||
Cancel(Pubkey),
|
Cancel(Pubkey),
|
||||||
Confirm(Signature),
|
Confirm(Signature),
|
||||||
Pay(PayCommand),
|
Pay(PayCommand),
|
||||||
|
ResolveSigner(Option<String>),
|
||||||
ShowAccount {
|
ShowAccount {
|
||||||
pubkey: Pubkey,
|
pubkey: Pubkey,
|
||||||
output_file: Option<String>,
|
output_file: Option<String>,
|
||||||
@ -863,6 +864,13 @@ pub fn parse_command(
|
|||||||
signers: vec![],
|
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)) => {
|
("send-signature", Some(matches)) => {
|
||||||
let to = value_of(matches, "to").unwrap();
|
let to = value_of(matches, "to").unwrap();
|
||||||
let process_id = value_of(matches, "process_id").unwrap();
|
let process_id = value_of(matches, "process_id").unwrap();
|
||||||
@ -2027,6 +2035,13 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
|||||||
*nonce_account,
|
*nonce_account,
|
||||||
*nonce_authority,
|
*nonce_authority,
|
||||||
),
|
),
|
||||||
|
CliCommand::ResolveSigner(path) => {
|
||||||
|
if let Some(path) = path {
|
||||||
|
Ok(path.to_string())
|
||||||
|
} else {
|
||||||
|
Ok("Signer is valid".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
CliCommand::ShowAccount {
|
CliCommand::ShowAccount {
|
||||||
pubkey,
|
pubkey,
|
||||||
output_file,
|
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_arg())
|
||||||
.arg(nonce_authority_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(
|
||||||
SubCommand::with_name("send-signature")
|
SubCommand::with_name("send-signature")
|
||||||
.about("Send a signature to authorize a transfer")
|
.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
|
// Test Simple Pay Subcommand
|
||||||
let test_pay =
|
let test_pay =
|
||||||
test_commands
|
test_commands
|
||||||
|
@ -55,6 +55,7 @@ mod commands {
|
|||||||
/// Ledger Wallet device
|
/// Ledger Wallet device
|
||||||
pub struct LedgerWallet {
|
pub struct LedgerWallet {
|
||||||
pub device: hidapi::HidDevice,
|
pub device: hidapi::HidDevice,
|
||||||
|
pub pretty_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for LedgerWallet {
|
impl fmt::Debug for LedgerWallet {
|
||||||
@ -65,7 +66,10 @@ impl fmt::Debug for LedgerWallet {
|
|||||||
|
|
||||||
impl LedgerWallet {
|
impl LedgerWallet {
|
||||||
pub fn new(device: hidapi::HidDevice) -> Self {
|
pub fn new(device: hidapi::HidDevice) -> Self {
|
||||||
Self { device }
|
Self {
|
||||||
|
device,
|
||||||
|
pretty_path: String::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transport Protocol:
|
// Transport Protocol:
|
||||||
@ -231,7 +235,10 @@ impl LedgerWallet {
|
|||||||
) -> Result<Vec<u8>, RemoteWalletError> {
|
) -> Result<Vec<u8>, RemoteWalletError> {
|
||||||
self.write(command, p1, p2, data)?;
|
self.write(command, p1, p2, data)?;
|
||||||
if p1 == P1_CONFIRM && is_last_part(p2) {
|
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()?;
|
let result = self.read()?;
|
||||||
println!("{}Approved", CHECK_MARK);
|
println!("{}Approved", CHECK_MARK);
|
||||||
Ok(result)
|
Ok(result)
|
||||||
|
@ -14,6 +14,7 @@ pub struct RemoteKeypair {
|
|||||||
pub wallet_type: RemoteWalletType,
|
pub wallet_type: RemoteWalletType,
|
||||||
pub derivation_path: DerivationPath,
|
pub derivation_path: DerivationPath,
|
||||||
pub pubkey: Pubkey,
|
pub pubkey: Pubkey,
|
||||||
|
pub path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteKeypair {
|
impl RemoteKeypair {
|
||||||
@ -21,6 +22,7 @@ impl RemoteKeypair {
|
|||||||
wallet_type: RemoteWalletType,
|
wallet_type: RemoteWalletType,
|
||||||
derivation_path: DerivationPath,
|
derivation_path: DerivationPath,
|
||||||
confirm_key: bool,
|
confirm_key: bool,
|
||||||
|
path: String,
|
||||||
) -> Result<Self, RemoteWalletError> {
|
) -> Result<Self, RemoteWalletError> {
|
||||||
let pubkey = match &wallet_type {
|
let pubkey = match &wallet_type {
|
||||||
RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?,
|
RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?,
|
||||||
@ -30,6 +32,7 @@ impl RemoteKeypair {
|
|||||||
wallet_type,
|
wallet_type,
|
||||||
derivation_path,
|
derivation_path,
|
||||||
pubkey,
|
pubkey,
|
||||||
|
path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -57,10 +60,12 @@ pub fn generate_remote_keypair(
|
|||||||
let (remote_wallet_info, derivation_path) = RemoteWalletInfo::parse_path(path)?;
|
let (remote_wallet_info, derivation_path) = RemoteWalletInfo::parse_path(path)?;
|
||||||
if remote_wallet_info.manufacturer == "ledger" {
|
if remote_wallet_info.manufacturer == "ledger" {
|
||||||
let ledger = get_ledger_from_info(remote_wallet_info, keypair_name, wallet_manager)?;
|
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(
|
Ok(RemoteKeypair::new(
|
||||||
RemoteWalletType::Ledger(ledger),
|
RemoteWalletType::Ledger(ledger),
|
||||||
derivation_path,
|
derivation_path,
|
||||||
confirm_key,
|
confirm_key,
|
||||||
|
path,
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
Err(RemoteWalletError::DeviceTypeMismatch)
|
Err(RemoteWalletError::DeviceTypeMismatch)
|
||||||
|
@ -105,8 +105,9 @@ impl RemoteWalletManager {
|
|||||||
if is_valid_ledger(device_info.vendor_id(), device_info.product_id()) {
|
if is_valid_ledger(device_info.vendor_id(), device_info.product_id()) {
|
||||||
match usb.open_path(&device_info.path()) {
|
match usb.open_path(&device_info.path()) {
|
||||||
Ok(device) => {
|
Ok(device) => {
|
||||||
let ledger = LedgerWallet::new(device);
|
let mut ledger = LedgerWallet::new(device);
|
||||||
if let Ok(info) = ledger.read_device(&device_info) {
|
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();
|
let path = device_info.path().to_str().unwrap().to_string();
|
||||||
trace!("Found device: {:?}", info);
|
trace!("Found device: {:?}", info);
|
||||||
v.push(Device {
|
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
|
/// Helper to determine if a device is a valid HID
|
||||||
pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
|
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
|
usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32
|
||||||
@ -519,4 +534,40 @@ mod tests {
|
|||||||
test_info.pubkey = pubkey;
|
test_info.pubkey = pubkey;
|
||||||
assert!(info.matches(&test_info));
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user