#[cfg(feature = "keystone")]
use crate::keystone::KeystoneWallet;
#[cfg(all(feature = "hidapi", feature = "keystone"))]
use rusb::UsbContext;
#[cfg(feature = "hidapi")]
use {crate::ledger::is_valid_ledger, parking_lot::Mutex, std::sync::Arc};
use {
crate::{
ledger::LedgerWallet,
ledger_error::LedgerError,
locator::{Locator, LocatorError, Manufacturer},
trezor::TrezorWallet,
},
log::*,
parking_lot::RwLock,
solana_derivation_path::{DerivationPath, DerivationPathError},
solana_pubkey::Pubkey,
solana_signature::Signature,
solana_signer::SignerError,
std::{
rc::Rc,
time::{Duration, Instant},
},
thiserror::Error,
trezor_client::{self, error::Error as TrezorClientError},
};
const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00;
const HID_USB_DEVICE_CLASS: u8 = 0;
#[derive(Error, Debug, Clone)]
pub enum RemoteWalletError {
#[error("hidapi error: {0}")]
Hid(String),
#[error("device type mismatch")]
DeviceTypeMismatch,
#[error("device with non-supported product ID or vendor ID was detected")]
InvalidDevice,
#[error(transparent)]
DerivationPathError(#[from] DerivationPathError),
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("invalid path: {0}")]
InvalidPath(String),
#[error(transparent)]
LedgerError(#[from] LedgerError),
#[error("no device found")]
NoDeviceFound,
#[error("protocol error: {0}")]
Protocol(&'static str),
#[error("pubkey not found for given address")]
PubkeyNotFound,
#[error("trezor error: {0}")]
TrezorError(String),
#[cfg(feature = "keystone")]
#[error("keystone error: {0}")]
KeystoneError(String),
#[error("remote wallet operation rejected by the user")]
UserCancel,
#[error(transparent)]
LocatorError(#[from] LocatorError),
}
#[cfg(feature = "hidapi")]
impl From<hidapi::HidError> for RemoteWalletError {
fn from(err: hidapi::HidError) -> RemoteWalletError {
RemoteWalletError::Hid(err.to_string())
}
}
impl From<RemoteWalletError> for SignerError {
fn from(err: RemoteWalletError) -> SignerError {
match err {
RemoteWalletError::Hid(hid_error) => SignerError::Connection(hid_error),
RemoteWalletError::DeviceTypeMismatch => SignerError::Connection(err.to_string()),
RemoteWalletError::InvalidDevice => SignerError::Connection(err.to_string()),
RemoteWalletError::InvalidInput(input) => SignerError::InvalidInput(input),
RemoteWalletError::LedgerError(e) => SignerError::Protocol(e.to_string()),
RemoteWalletError::TrezorError(e) => SignerError::Protocol(e),
#[cfg(feature = "keystone")]
RemoteWalletError::KeystoneError(e) => SignerError::Protocol(e),
RemoteWalletError::NoDeviceFound => SignerError::NoDeviceFound,
RemoteWalletError::Protocol(e) => SignerError::Protocol(e.to_string()),
RemoteWalletError::UserCancel => {
SignerError::UserCancel("remote wallet operation rejected by the user".to_string())
}
_ => SignerError::Custom(err.to_string()),
}
}
}
impl From<TrezorClientError> for RemoteWalletError {
fn from(err: TrezorClientError) -> RemoteWalletError {
RemoteWalletError::TrezorError(err.to_string())
}
}
pub struct RemoteWalletManager {
#[cfg(feature = "hidapi")]
usb: Arc<Mutex<hidapi::HidApi>>,
devices: RwLock<Vec<Device>>,
}
impl RemoteWalletManager {
#[cfg(feature = "hidapi")]
pub fn new(usb: Arc<Mutex<hidapi::HidApi>>) -> Rc<Self> {
Rc::new(Self {
usb,
devices: RwLock::new(Vec::new()),
})
}
#[cfg(feature = "hidapi")]
pub fn update_devices(&self) -> Result<usize, RemoteWalletError> {
let mut usb = self.usb.lock();
usb.refresh_devices()?;
let num_prev_devices = self.devices.read().len();
let mut detected_devices = vec![];
let mut errors = vec![];
Self::scan_ledger_devices(&usb, &mut detected_devices, &mut errors);
Self::scan_trezor_devices(&mut detected_devices, &mut errors);
#[cfg(feature = "keystone")]
Self::scan_keystone_devices(&mut detected_devices, &mut errors);
let num_curr_devices = detected_devices.len();
*self.devices.write() = detected_devices;
if num_curr_devices == 0 && !errors.is_empty() {
return Err(errors[0].clone());
}
Ok(num_curr_devices - num_prev_devices)
}
#[cfg(feature = "hidapi")]
fn scan_ledger_devices(
usb: &hidapi::HidApi,
detected_devices: &mut Vec<Device>,
errors: &mut Vec<RemoteWalletError>,
) {
for device_info in usb.device_list().filter(|&device_info| {
#[cfg(not(any(feature = "linux-static-libusb", feature = "linux-shared-libusb")))]
let is_valid_hid_device =
is_valid_hid_device(device_info.usage_page(), device_info.interface_number());
#[cfg(any(feature = "linux-static-libusb", feature = "linux-shared-libusb"))]
let is_valid_hid_device = true; is_valid_hid_device
&& is_valid_ledger(device_info.vendor_id(), device_info.product_id())
}) {
match usb.open_path(device_info.path()) {
Ok(device) => {
let mut ledger = LedgerWallet::new(device);
let result = ledger.read_device(device_info);
match result {
Ok(info) => {
ledger.pretty_path = info.get_pretty_path();
let path = device_info.path().to_str().unwrap().to_string();
trace!("Found device: {info:?}");
detected_devices.push(Device {
path,
info,
wallet_type: RemoteWalletType::Ledger(Rc::new(ledger)),
})
}
Err(err) => {
error!("Error connecting to ledger device to read info: {err}");
errors.push(err)
}
}
}
Err(err) => error!("Error connecting to ledger device to read info: {err}"),
}
}
}
#[cfg(feature = "hidapi")]
fn scan_trezor_devices(
detected_devices: &mut Vec<Device>,
errors: &mut Vec<RemoteWalletError>,
) {
for device in trezor_client::find_devices(false) {
let mut trezor = match device.connect() {
Ok(t) => t,
Err(err) => {
error!("Error connecting to trezor device: {err}");
continue;
}
};
if let Err(err) = trezor.init_device(None) {
let err = RemoteWalletError::from(err);
error!("Error initializing trezor device: {err}");
errors.push(err);
continue;
}
let wallet = TrezorWallet::new(trezor);
let pubkey = wallet.get_pubkey(&DerivationPath::default(), false).ok();
let locator = Locator {
manufacturer: Manufacturer::Trezor,
pubkey,
};
let info = RemoteWalletInfo::parse_locator(locator);
let path = info.get_pretty_path();
detected_devices.push(Device {
path: path.clone(),
info,
wallet_type: RemoteWalletType::Trezor(Rc::new(wallet)),
});
}
}
#[cfg(all(feature = "hidapi", feature = "keystone"))]
fn scan_keystone_devices(
detected_devices: &mut Vec<Device>,
errors: &mut Vec<RemoteWalletError>,
) {
let Ok(context) = rusb::Context::new() else {
return;
};
let Ok(device_list) = context.devices() else {
return;
};
for device in device_list.iter() {
let Ok(desc) = device.device_descriptor() else {
continue;
};
if !crate::keystone::is_valid_keystone(desc.vendor_id(), desc.product_id()) {
continue;
}
let handle = match device.open() {
Ok(handle) => handle,
Err(err) => {
error!("Failed to open Keystone device: {err}");
errors.push(RemoteWalletError::Hid(format!(
"Failed to open Keystone device {:04x}:{:04x}: {err}",
desc.vendor_id(),
desc.product_id()
)));
continue;
}
};
let mut keystone = match KeystoneWallet::new(device.clone(), handle) {
Ok(keystone) => keystone,
Err(err) => {
error!("Error initializing Keystone USB transport: {err}");
errors.push(err);
continue;
}
};
match keystone.read_device(&device) {
Ok(info) => {
keystone.pretty_path = info.get_pretty_path();
trace!("Found Keystone device: {info:?}");
detected_devices.push(Device {
path: info.host_device_path.clone(),
info,
wallet_type: RemoteWalletType::Keystone(Rc::new(keystone)),
})
}
Err(err) => {
error!("Error connecting to Keystone device: {err}");
errors.push(err);
}
}
}
}
#[cfg(not(feature = "hidapi"))]
pub fn update_devices(&self) -> Result<usize, RemoteWalletError> {
Err(RemoteWalletError::Hid(
"hidapi crate compilation disabled in solana-remote-wallet.".to_string(),
))
}
pub fn list_devices(&self) -> Vec<Device> {
self.devices.read().iter().cloned().collect()
}
pub fn get_wallet(&self, host_device_path: &str) -> Result<Device, RemoteWalletError> {
self.devices
.read()
.iter()
.find(|device| device.info.host_device_path == host_device_path)
.ok_or(RemoteWalletError::PubkeyNotFound)
.cloned()
}
pub fn get_wallet_info(&self, pubkey: &Pubkey) -> Option<RemoteWalletInfo> {
self.devices
.read()
.iter()
.find(|d| &d.info.pubkey == pubkey)
.map(|d| d.info.clone())
}
pub fn try_connect_polling(&self, max_polling_duration: &Duration) -> bool {
let start_time = Instant::now();
while start_time.elapsed() <= *max_polling_duration {
if let Ok(num_devices) = self.update_devices() {
let plural = if num_devices == 1 { "" } else { "s" };
trace!("{num_devices} Remote Wallet{plural} found");
return true;
}
}
false
}
}
#[allow(unused_variables)]
pub trait RemoteWallet<T> {
fn name(&self) -> &str {
"unimplemented"
}
fn read_device(&mut self, dev_info: &T) -> Result<RemoteWalletInfo, RemoteWalletError> {
unimplemented!();
}
fn get_pubkey(
&self,
derivation_path: &DerivationPath,
confirm_key: bool,
) -> Result<Pubkey, RemoteWalletError> {
unimplemented!();
}
fn sign_message(
&self,
derivation_path: &DerivationPath,
data: &[u8],
) -> Result<Signature, RemoteWalletError> {
unimplemented!();
}
fn sign_offchain_message(
&self,
derivation_path: &DerivationPath,
message: &[u8],
) -> Result<Signature, RemoteWalletError> {
unimplemented!();
}
}
#[derive(Debug, Clone)]
pub struct Device {
pub(crate) path: String,
pub(crate) info: RemoteWalletInfo,
pub wallet_type: RemoteWalletType,
}
#[derive(Debug, Clone)]
pub enum RemoteWalletType {
Ledger(Rc<LedgerWallet>),
Trezor(Rc<TrezorWallet>),
#[cfg(feature = "keystone")]
Keystone(Rc<KeystoneWallet>),
}
impl RemoteWalletType {
pub fn get_pubkey(
&self,
derivation_path: &DerivationPath,
confirm_key: bool,
) -> Result<Pubkey, RemoteWalletError> {
match self {
Self::Ledger(wallet) => wallet.get_pubkey(derivation_path, confirm_key),
Self::Trezor(wallet) => wallet.get_pubkey(derivation_path, confirm_key),
#[cfg(feature = "keystone")]
Self::Keystone(wallet) => wallet.get_pubkey(derivation_path, confirm_key),
}
}
pub fn sign_message(
&self,
derivation_path: &DerivationPath,
message: &[u8],
) -> Result<Signature, RemoteWalletError> {
match self {
Self::Ledger(wallet) => wallet.sign_message(derivation_path, message),
Self::Trezor(wallet) => wallet.sign_message(derivation_path, message),
#[cfg(feature = "keystone")]
Self::Keystone(wallet) => wallet.sign_message(derivation_path, message),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct RemoteWalletInfo {
pub model: String,
pub manufacturer: Manufacturer,
pub serial: String,
pub host_device_path: String,
pub pubkey: Pubkey,
pub error: Option<RemoteWalletError>,
}
impl RemoteWalletInfo {
pub fn parse_locator(locator: Locator) -> Self {
RemoteWalletInfo {
manufacturer: locator.manufacturer,
pubkey: locator.pubkey.unwrap_or_default(),
..RemoteWalletInfo::default()
}
}
pub fn get_pretty_path(&self) -> String {
format!("usb://{}/{:?}", self.manufacturer, self.pubkey,)
}
pub(crate) fn matches(&self, other: &Self) -> bool {
self.manufacturer == other.manufacturer
&& (self.pubkey == other.pubkey
|| self.pubkey == Pubkey::default()
|| other.pubkey == Pubkey::default())
}
}
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
}
#[cfg(feature = "hidapi")]
pub fn initialize_wallet_manager() -> Result<Rc<RemoteWalletManager>, RemoteWalletError> {
let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?));
Ok(RemoteWalletManager::new(hidapi))
}
#[cfg(not(feature = "hidapi"))]
pub fn initialize_wallet_manager() -> Result<Rc<RemoteWalletManager>, RemoteWalletError> {
Err(RemoteWalletError::Hid(
"hidapi crate compilation disabled in solana-remote-wallet.".to_string(),
))
}
pub fn maybe_wallet_manager() -> Result<Option<Rc<RemoteWalletManager>>, RemoteWalletError> {
let wallet_manager = initialize_wallet_manager()?;
let device_count = wallet_manager.update_devices()?;
if device_count > 0 {
Ok(Some(wallet_manager))
} else {
drop(wallet_manager);
Ok(None)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_locator() {
let pubkey = solana_pubkey::new_rand();
let locator = Locator {
manufacturer: Manufacturer::Ledger,
pubkey: Some(pubkey),
};
let wallet_info = RemoteWalletInfo::parse_locator(locator);
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: Manufacturer::Ledger,
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey,
error: None,
}));
let locator = Locator {
manufacturer: Manufacturer::Ledger,
pubkey: None,
};
let wallet_info = RemoteWalletInfo::parse_locator(locator);
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: Manufacturer::Ledger,
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey: Pubkey::default(),
error: None,
}));
let locator = Locator {
manufacturer: Manufacturer::Trezor,
pubkey: Some(pubkey),
};
let wallet_info = RemoteWalletInfo::parse_locator(locator);
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "T".to_string(),
manufacturer: Manufacturer::Trezor,
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey,
error: None,
}));
let locator = Locator {
manufacturer: Manufacturer::Trezor,
pubkey: None,
};
let wallet_info = RemoteWalletInfo::parse_locator(locator);
assert!(wallet_info.matches(&RemoteWalletInfo {
model: "T".to_string(),
manufacturer: Manufacturer::Trezor,
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey: Pubkey::default(),
error: None,
}));
}
#[test]
fn test_remote_wallet_info_matches() {
let pubkey = solana_pubkey::new_rand();
let info = RemoteWalletInfo {
manufacturer: Manufacturer::Ledger,
model: "Nano S".to_string(),
serial: "0001".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey,
error: None,
};
let mut test_info = RemoteWalletInfo {
manufacturer: Manufacturer::Unknown,
..RemoteWalletInfo::default()
};
assert!(!info.matches(&test_info));
test_info.manufacturer = Manufacturer::Ledger;
assert!(info.matches(&test_info));
test_info.model = "Other".to_string();
assert!(info.matches(&test_info));
test_info.model = "Nano S".to_string();
assert!(info.matches(&test_info));
test_info.host_device_path = "/host/device/path".to_string();
assert!(info.matches(&test_info));
let another_pubkey = solana_pubkey::new_rand();
test_info.pubkey = another_pubkey;
assert!(!info.matches(&test_info));
test_info.pubkey = pubkey;
assert!(info.matches(&test_info));
let info = RemoteWalletInfo {
manufacturer: Manufacturer::Trezor,
model: "T".to_string(),
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey,
error: None,
};
let mut test_info = RemoteWalletInfo {
manufacturer: Manufacturer::Unknown,
..RemoteWalletInfo::default()
};
assert!(!info.matches(&test_info));
test_info.manufacturer = Manufacturer::Trezor;
assert!(info.matches(&test_info));
test_info.model = "Other".to_string();
assert!(info.matches(&test_info));
test_info.model = "T".to_string();
assert!(info.matches(&test_info));
test_info.host_device_path = "/host/device/path".to_string();
assert!(info.matches(&test_info));
let another_pubkey = solana_pubkey::new_rand();
test_info.pubkey = another_pubkey;
assert!(!info.matches(&test_info));
test_info.pubkey = pubkey;
assert!(info.matches(&test_info));
}
#[test]
fn test_get_pretty_path() {
let pubkey = solana_pubkey::new_rand();
let pubkey_str = pubkey.to_string();
let remote_wallet_info = RemoteWalletInfo {
model: "nano-s".to_string(),
manufacturer: Manufacturer::Ledger,
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey,
error: None,
};
assert_eq!(
remote_wallet_info.get_pretty_path(),
format!("usb://ledger/{pubkey_str}")
);
let remote_wallet_info = RemoteWalletInfo {
model: "T".to_string(),
manufacturer: Manufacturer::Trezor,
serial: "".to_string(),
host_device_path: "/host/device/path".to_string(),
pubkey,
error: None,
};
assert_eq!(
remote_wallet_info.get_pretty_path(),
format!("usb://trezor/{pubkey_str}")
);
}
}