Skip to content

Commit

Permalink
Add remote-wallet path apis
Browse files Browse the repository at this point in the history
  • Loading branch information
Tyera Eulberg committed Feb 4, 2020
1 parent 8f422ad commit c3df5a4
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 17 deletions.
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion remote-wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ homepage = "https://solana.com/"

[dependencies]
base32 = "0.4.0"
dialoguer = "0.5.0"
hidapi = "1.1.1"
libusb = "0.3.0"
log = "0.4.8"
parking_lot = "0.7"
rusb = "0.5.5"
semver = "0.9"
solana-sdk = { path = "../sdk", version = "0.24.0" }
thiserror = "1.0"
Expand Down
51 changes: 43 additions & 8 deletions remote-wallet/src/ledger.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use crate::remote_wallet::{DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo};
use crate::remote_wallet::{
initialize_wallet_manager, DerivationPath, RemoteWallet, RemoteWalletError, RemoteWalletInfo,
};
use dialoguer::{theme::ColorfulTheme, Select};
use log::*;
use semver::Version as FirmwareVersion;
use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
use std::{cmp::min, fmt};
use std::{cmp::min, fmt, sync::Arc};

const APDU_TAG: u8 = 0x05;
const APDU_CLA: u8 = 0xe0;
const APDU_PAYLOAD_HEADER_LEN: usize = 7;

const SOL_DERIVATION_PATH_BE: [u8; 8] = [0x80, 0, 0, 44, 0x80, 0, 0x01, 0xF5]; // 44'/501', Solana
// const SOL_DERIVATION_PATH_BE: [u8; 8] = [0x80, 0, 0, 44, 0x80, 0, 0x00, 0x94]; // 44'/148', Stellar

// const SOL_DERIVATION_PATH_BE: [u8; 8] = [0x80, 0, 0, 44, 0x80, 0, 0x00, 0x94]; // 44'/148', Stellar

/// Ledger vendor ID
const LEDGER_VID: u16 = 0x2c97;
Expand Down Expand Up @@ -175,8 +179,9 @@ impl LedgerWallet {
let status =
(message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize);
trace!("Read status {:x}", status);
#[allow(clippy::match_overlapping_arm)]
match status {
// TODO: These need to be aligned with solana Ledger app error codes
// TODO: These need to be aligned with solana Ledger app error codes, and clippy allowance removed
0x6700 => Err(RemoteWalletError::Protocol("Incorrect length")),
0x6982 => Err(RemoteWalletError::Protocol(
"Security status not satisfied (Canceled by user)",
Expand Down Expand Up @@ -246,8 +251,9 @@ impl RemoteWallet for LedgerWallet {
.manufacturer_string
.clone()
.unwrap_or_else(|| "Unknown".to_owned())
.to_lowercase();
let name = dev_info
.to_lowercase()
.replace(" ", "-");
let model = dev_info
.product_string
.clone()
.unwrap_or_else(|| "Unknown".to_owned())
Expand All @@ -259,7 +265,7 @@ impl RemoteWallet for LedgerWallet {
.unwrap_or_else(|| "Unknown".to_owned());
self.get_pubkey(DerivationPath::default())
.map(|pubkey| RemoteWalletInfo {
name,
model,
manufacturer,
serial,
pubkey,
Expand Down Expand Up @@ -345,6 +351,35 @@ pub fn is_valid_ledger(vendor_id: u16, _product_id: u16) -> bool {
vendor_id == LEDGER_VID
}

///
pub fn get_ledger_from_info(
info: RemoteWalletInfo,
) -> Result<Arc<LedgerWallet>, RemoteWalletError> {
let wallet_manager = initialize_wallet_manager();
let _device_count = wallet_manager.update_devices()?;
let devices = wallet_manager.list_devices();
let (pubkeys, device_paths): (Vec<Pubkey>, Vec<String>) = devices
.iter()
.filter(|&device_info| device_info == &info)
.map(|device_info| (device_info.pubkey, device_info.get_pretty_path()))
.unzip();
if pubkeys.is_empty() {
return Err(RemoteWalletError::NoDeviceFound);
}
let wallet_base_pubkey = if pubkeys.len() > 1 {
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("Multiple hardware wallets found. Please select a device")
.default(0)
.items(&device_paths[..])
.interact()
.unwrap();
pubkeys[selection]
} else {
pubkeys[0]
};
wallet_manager.get_ledger(&wallet_base_pubkey)
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -364,7 +399,7 @@ mod tests {
let ledger_base_pubkey = wallet_manager
.list_devices()
.iter()
.filter(|d| d.manufacturer == "Ledger".to_string())
.filter(|d| d.manufacturer == "ledger".to_string())
.nth(0)
.map(|d| d.pubkey.clone())
.expect("No ledger device detected");
Expand Down
189 changes: 181 additions & 8 deletions remote-wallet/src/remote_wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use parking_lot::{Mutex, RwLock};
use solana_sdk::{pubkey::Pubkey, signature::Signature, transaction::Transaction};
use std::{
fmt,
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
Expand All @@ -24,11 +25,11 @@ pub enum RemoteWalletError {
#[error("device with non-supported product ID or vendor ID was detected")]
InvalidDevice,

#[error("no device arrived")]
NoDeviceArrived,
#[error("invalid path: {0}")]
InvalidPath(String),

#[error("no device left")]
NoDeviceLeft,
#[error("no device found")]
NoDeviceFound,

#[error("protocol error: {0}")]
Protocol(&'static str),
Expand Down Expand Up @@ -171,10 +172,10 @@ pub enum RemoteWalletType {
}

/// Remote wallet information.
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct RemoteWalletInfo {
/// RemoteWallet device name
pub name: String,
/// RemoteWallet device model
pub model: String,
/// RemoteWallet device manufacturer
pub manufacturer: String,
/// RemoteWallet device serial number
Expand All @@ -183,7 +184,101 @@ pub struct RemoteWalletInfo {
pub pubkey: Pubkey,
}

#[derive(Default, PartialEq)]
impl RemoteWalletInfo {
pub fn parse_path(mut path: String) -> Result<(Self, DerivationPath), RemoteWalletError> {
if is_remote_wallet_path(&path).is_ok() {
let path = path.split_off(6);
let mut parts = path.split('/');
let mut wallet_info = RemoteWalletInfo::default();
let manufacturer = parts.next().unwrap();
wallet_info.manufacturer = manufacturer.to_string();
wallet_info.model = parts.next().unwrap_or("").to_string();
wallet_info.pubkey = parts
.next()
.and_then(|pubkey_str| Pubkey::from_str(pubkey_str).ok())
.unwrap_or_default();
let derivation_path = parts
.next()
.map(|account| {
let account = account.parse::<u16>().unwrap();
let change = parts.next().and_then(|change| change.parse::<u16>().ok());
DerivationPath { account, change }
})
.unwrap_or(DerivationPath {
account: 0,
change: None,
});
Ok((wallet_info, derivation_path))
} else {
Err(RemoteWalletError::InvalidPath(path))
}
}

pub fn get_pretty_path(&self) -> String {
format!(
"usb://{}/{}/{:?}",
self.manufacturer, self.model, self.pubkey,
)
}
}

impl PartialEq for RemoteWalletInfo {
fn eq(&self, other: &Self) -> bool {
self.manufacturer == other.manufacturer
&& (self.model == other.model || self.model == "" || other.model == "")
&& (self.pubkey == other.pubkey
|| self.pubkey == Pubkey::default()
|| other.pubkey == Pubkey::default())
}
}

pub fn is_remote_wallet_path(value: &str) -> Result<(), String> {
if value.starts_with("usb://") {
let (_, path) = value.split_at(6);
let mut parts = path.split('/');
let manufacturer = parts.next().unwrap();
if manufacturer != "" {
if let Some(_model) = parts.next() {
if let Some(pubkey_str) = parts.next() {
let _pubkey = Pubkey::from_str(pubkey_str).map_err(|e| {
format!(
"Unable to parse pubkey in remote wallet path, provided: {}, err: {:?}",
pubkey_str, e
)
})?;
if let Some(account) = parts.next() {
let _account = account
.parse::<u16>()
.map_err(|e| {
format!(
"Unable to parse account in remote wallet path, provided: {}, err: {:?}",
account, e
)
})?;

if let Some(change) = parts.next() {
let _change = change
.parse::<u16>()
.map_err(|e| {
format!(
"Unable to parse change in remote wallet path, provided: {}, err: {:?}",
account, e
)
})?;
}
}
}
}
return Ok(());
}
}
Err(format!(
"Unable to parse input as remote wallet path, provided: {}",
value
))
}

#[derive(Default, PartialEq, Clone)]
pub struct DerivationPath {
pub account: u16,
pub change: Option<u16>,
Expand All @@ -210,3 +305,81 @@ pub fn initialize_wallet_manager() -> Arc<RemoteWalletManager> {
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new().unwrap()));
RemoteWalletManager::new(hidapi)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_path() {
let pubkey = Pubkey::new_rand();
assert_eq!(
RemoteWalletInfo::parse_path(format!("usb://ledger/nano-s/{:?}/1/2", pubkey)).unwrap(),
(
RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: "ledger".to_string(),
serial: "".to_string(),
pubkey,
},
DerivationPath {
account: 1,
change: Some(2),
}
)
)
}

#[test]
fn test_remote_wallet_info_partial_eq() {
let pubkey = Pubkey::new_rand();
let info = RemoteWalletInfo {
manufacturer: "Ledger".to_string(),
model: "Nano S".to_string(),
serial: "0001".to_string(),
pubkey: pubkey.clone(),
};
let mut test_info = RemoteWalletInfo::default();
test_info.manufacturer = "Not Ledger".to_string();
assert_ne!(info, test_info);
test_info.manufacturer = "Ledger".to_string();
assert_eq!(info, test_info);
test_info.model = "Other".to_string();
assert_ne!(info, test_info);
test_info.model = "Nano S".to_string();
assert_eq!(info, test_info);
let another_pubkey = Pubkey::new_rand();
test_info.pubkey = another_pubkey;
assert_ne!(info, test_info);
test_info.pubkey = pubkey;
assert_eq!(info, test_info);
}

#[test]
fn test_is_remote_wallet_path() {
assert!(is_remote_wallet_path("usb://").is_err());
assert_eq!(is_remote_wallet_path("usb://ledger"), Ok(()));
assert_eq!(is_remote_wallet_path("usb://ledger/nano-s"), Ok(()));
assert!(is_remote_wallet_path("usb://ledger/nano-s/notpubkey").is_err());

let pubkey = Pubkey::new_rand();
assert_eq!(
is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}", pubkey)),
Ok(())
);
assert_eq!(
is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}/1", pubkey)),
Ok(())
);
assert!(is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}/a", pubkey)).is_err());
assert!(is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}/65537", pubkey)).is_err());
assert_eq!(
is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}/1/1", pubkey)),
Ok(())
);
assert!(is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}/1/b", pubkey)).is_err());
assert!(
is_remote_wallet_path(&format!("usb://ledger/nano-s/{:?}/1/65537", pubkey)).is_err()
);
}
}

0 comments on commit c3df5a4

Please sign in to comment.