1#[cfg(feature = "hidapi")]
2use {crate::ledger::is_valid_ledger, parking_lot::Mutex};
3use {
4 crate::{
5 ledger::LedgerWallet,
6 ledger_error::LedgerError,
7 locator::{Locator, LocatorError, Manufacturer},
8 },
9 log::*,
10 parking_lot::RwLock,
11 solana_sdk::{
12 derivation_path::{DerivationPath, DerivationPathError},
13 pubkey::Pubkey,
14 signature::{Signature, SignerError},
15 },
16 std::{
17 sync::Arc,
18 time::{Duration, Instant},
19 },
20 thiserror::Error,
21};
22
23const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00;
24const HID_USB_DEVICE_CLASS: u8 = 0;
25
26#[derive(Error, Debug, Clone)]
28pub enum RemoteWalletError {
29 #[error("hidapi error")]
30 Hid(String),
31
32 #[error("device type mismatch")]
33 DeviceTypeMismatch,
34
35 #[error("device with non-supported product ID or vendor ID was detected")]
36 InvalidDevice,
37
38 #[error(transparent)]
39 DerivationPathError(#[from] DerivationPathError),
40
41 #[error("invalid input: {0}")]
42 InvalidInput(String),
43
44 #[error("invalid path: {0}")]
45 InvalidPath(String),
46
47 #[error(transparent)]
48 LedgerError(#[from] LedgerError),
49
50 #[error("no device found")]
51 NoDeviceFound,
52
53 #[error("protocol error: {0}")]
54 Protocol(&'static str),
55
56 #[error("pubkey not found for given address")]
57 PubkeyNotFound,
58
59 #[error("remote wallet operation rejected by the user")]
60 UserCancel,
61
62 #[error(transparent)]
63 LocatorError(#[from] LocatorError),
64}
65
66#[cfg(feature = "hidapi")]
67impl From<hidapi::HidError> for RemoteWalletError {
68 fn from(err: hidapi::HidError) -> RemoteWalletError {
69 RemoteWalletError::Hid(err.to_string())
70 }
71}
72
73impl From<RemoteWalletError> for SignerError {
74 fn from(err: RemoteWalletError) -> SignerError {
75 match err {
76 RemoteWalletError::Hid(hid_error) => SignerError::Connection(hid_error),
77 RemoteWalletError::DeviceTypeMismatch => SignerError::Connection(err.to_string()),
78 RemoteWalletError::InvalidDevice => SignerError::Connection(err.to_string()),
79 RemoteWalletError::InvalidInput(input) => SignerError::InvalidInput(input),
80 RemoteWalletError::LedgerError(e) => SignerError::Protocol(e.to_string()),
81 RemoteWalletError::NoDeviceFound => SignerError::NoDeviceFound,
82 RemoteWalletError::Protocol(e) => SignerError::Protocol(e.to_string()),
83 RemoteWalletError::UserCancel => {
84 SignerError::UserCancel("remote wallet operation rejected by the user".to_string())
85 }
86 _ => SignerError::Custom(err.to_string()),
87 }
88 }
89}
90
91pub struct RemoteWalletManager {
93 #[cfg(feature = "hidapi")]
94 usb: Arc<Mutex<hidapi::HidApi>>,
95 devices: RwLock<Vec<Device>>,
96}
97
98impl RemoteWalletManager {
99 #[cfg(feature = "hidapi")]
101 pub fn new(usb: Arc<Mutex<hidapi::HidApi>>) -> Arc<Self> {
102 Arc::new(Self {
103 usb,
104 devices: RwLock::new(Vec::new()),
105 })
106 }
107
108 #[cfg(feature = "hidapi")]
111 pub fn update_devices(&self) -> Result<usize, RemoteWalletError> {
112 let mut usb = self.usb.lock();
113 usb.refresh_devices()?;
114 let devices = usb.device_list();
115 let num_prev_devices = self.devices.read().len();
116
117 let mut detected_devices = vec![];
118 let mut errors = vec![];
119 for device_info in devices.filter(|&device_info| {
120 is_valid_hid_device(device_info.usage_page(), device_info.interface_number())
121 && is_valid_ledger(device_info.vendor_id(), device_info.product_id())
122 }) {
123 match usb.open_path(device_info.path()) {
124 Ok(device) => {
125 let mut ledger = LedgerWallet::new(device);
126 let result = ledger.read_device(device_info);
127 match result {
128 Ok(info) => {
129 ledger.pretty_path = info.get_pretty_path();
130 let path = device_info.path().to_str().unwrap().to_string();
131 trace!("Found device: {:?}", info);
132 detected_devices.push(Device {
133 path,
134 info,
135 wallet_type: RemoteWalletType::Ledger(Arc::new(ledger)),
136 })
137 }
138 Err(err) => {
139 error!("Error connecting to ledger device to read info: {}", err);
140 errors.push(err)
141 }
142 }
143 }
144 Err(err) => error!("Error connecting to ledger device to read info: {}", err),
145 }
146 }
147
148 let num_curr_devices = detected_devices.len();
149 *self.devices.write() = detected_devices;
150
151 if num_curr_devices == 0 && !errors.is_empty() {
152 return Err(errors[0].clone());
153 }
154
155 Ok(num_curr_devices - num_prev_devices)
156 }
157
158 #[cfg(not(feature = "hidapi"))]
159 pub fn update_devices(&self) -> Result<usize, RemoteWalletError> {
160 Err(RemoteWalletError::Hid(
161 "hidapi crate compilation disabled in safecoin-remote-wallet.".to_string(),
162 ))
163 }
164
165 pub fn list_devices(&self) -> Vec<RemoteWalletInfo> {
167 self.devices.read().iter().map(|d| d.info.clone()).collect()
168 }
169
170 #[allow(unreachable_patterns)]
172 pub fn get_ledger(
173 &self,
174 host_device_path: &str,
175 ) -> Result<Arc<LedgerWallet>, RemoteWalletError> {
176 self.devices
177 .read()
178 .iter()
179 .find(|device| device.info.host_device_path == host_device_path)
180 .ok_or(RemoteWalletError::PubkeyNotFound)
181 .and_then(|device| match &device.wallet_type {
182 RemoteWalletType::Ledger(ledger) => Ok(ledger.clone()),
183 _ => Err(RemoteWalletError::DeviceTypeMismatch),
184 })
185 }
186
187 pub fn get_wallet_info(&self, pubkey: &Pubkey) -> Option<RemoteWalletInfo> {
189 self.devices
190 .read()
191 .iter()
192 .find(|d| &d.info.pubkey == pubkey)
193 .map(|d| d.info.clone())
194 }
195
196 pub fn try_connect_polling(&self, max_polling_duration: &Duration) -> bool {
198 let start_time = Instant::now();
199 while start_time.elapsed() <= *max_polling_duration {
200 if let Ok(num_devices) = self.update_devices() {
201 let plural = if num_devices == 1 { "" } else { "s" };
202 trace!("{} Remote Wallet{} found", num_devices, plural);
203 return true;
204 }
205 }
206 false
207 }
208}
209
210#[allow(unused_variables)]
212pub trait RemoteWallet<T> {
213 fn name(&self) -> &str {
214 "unimplemented"
215 }
216
217 fn read_device(&mut self, dev_info: &T) -> Result<RemoteWalletInfo, RemoteWalletError> {
219 unimplemented!();
220 }
221
222 fn get_pubkey(
224 &self,
225 derivation_path: &DerivationPath,
226 confirm_key: bool,
227 ) -> Result<Pubkey, RemoteWalletError> {
228 unimplemented!();
229 }
230
231 fn sign_message(
233 &self,
234 derivation_path: &DerivationPath,
235 data: &[u8],
236 ) -> Result<Signature, RemoteWalletError> {
237 unimplemented!();
238 }
239}
240
241#[derive(Debug)]
243pub struct Device {
244 #[allow(dead_code)]
245 pub(crate) path: String,
246 pub(crate) info: RemoteWalletInfo,
247 pub wallet_type: RemoteWalletType,
248}
249
250#[derive(Debug)]
252pub enum RemoteWalletType {
253 Ledger(Arc<LedgerWallet>),
254}
255
256#[derive(Debug, Default, Clone)]
258pub struct RemoteWalletInfo {
259 pub model: String,
261 pub manufacturer: Manufacturer,
263 pub serial: String,
265 pub host_device_path: String,
267 pub pubkey: Pubkey,
269 pub error: Option<RemoteWalletError>,
271}
272
273impl RemoteWalletInfo {
274 pub fn parse_locator(locator: Locator) -> Self {
275 RemoteWalletInfo {
276 manufacturer: locator.manufacturer,
277 pubkey: locator.pubkey.unwrap_or_default(),
278 ..RemoteWalletInfo::default()
279 }
280 }
281
282 pub fn get_pretty_path(&self) -> String {
283 format!("usb://{}/{:?}", self.manufacturer, self.pubkey,)
284 }
285
286 pub(crate) fn matches(&self, other: &Self) -> bool {
287 self.manufacturer == other.manufacturer
288 && (self.pubkey == other.pubkey
289 || self.pubkey == Pubkey::default()
290 || other.pubkey == Pubkey::default())
291 }
292}
293
294pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
296 usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32
297}
298
299#[cfg(feature = "hidapi")]
301pub fn initialize_wallet_manager() -> Result<Arc<RemoteWalletManager>, RemoteWalletError> {
302 let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?));
303 Ok(RemoteWalletManager::new(hidapi))
304}
305#[cfg(not(feature = "hidapi"))]
306pub fn initialize_wallet_manager() -> Result<Arc<RemoteWalletManager>, RemoteWalletError> {
307 Err(RemoteWalletError::Hid(
308 "hidapi crate compilation disabled in safecoin-remote-wallet.".to_string(),
309 ))
310}
311
312pub fn maybe_wallet_manager() -> Result<Option<Arc<RemoteWalletManager>>, RemoteWalletError> {
313 let wallet_manager = initialize_wallet_manager()?;
314 let device_count = wallet_manager.update_devices()?;
315 if device_count > 0 {
316 Ok(Some(wallet_manager))
317 } else {
318 drop(wallet_manager);
319 Ok(None)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_parse_locator() {
329 let pubkey = solana_sdk::pubkey::new_rand();
330 let locator = Locator {
331 manufacturer: Manufacturer::Ledger,
332 pubkey: Some(pubkey),
333 };
334 let wallet_info = RemoteWalletInfo::parse_locator(locator);
335 assert!(wallet_info.matches(&RemoteWalletInfo {
336 model: "nano-s".to_string(),
337 manufacturer: Manufacturer::Ledger,
338 serial: "".to_string(),
339 host_device_path: "/host/device/path".to_string(),
340 pubkey,
341 error: None,
342 }));
343
344 let locator = Locator {
346 manufacturer: Manufacturer::Ledger,
347 pubkey: None,
348 };
349 let wallet_info = RemoteWalletInfo::parse_locator(locator);
350 assert!(wallet_info.matches(&RemoteWalletInfo {
351 model: "nano-s".to_string(),
352 manufacturer: Manufacturer::Ledger,
353 serial: "".to_string(),
354 host_device_path: "/host/device/path".to_string(),
355 pubkey: Pubkey::default(),
356 error: None,
357 }));
358 }
359
360 #[test]
361 fn test_remote_wallet_info_matches() {
362 let pubkey = solana_sdk::pubkey::new_rand();
363 let info = RemoteWalletInfo {
364 manufacturer: Manufacturer::Ledger,
365 model: "Nano S".to_string(),
366 serial: "0001".to_string(),
367 host_device_path: "/host/device/path".to_string(),
368 pubkey,
369 error: None,
370 };
371 let mut test_info = RemoteWalletInfo {
372 manufacturer: Manufacturer::Unknown,
373 ..RemoteWalletInfo::default()
374 };
375 assert!(!info.matches(&test_info));
376 test_info.manufacturer = Manufacturer::Ledger;
377 assert!(info.matches(&test_info));
378 test_info.model = "Other".to_string();
379 assert!(info.matches(&test_info));
380 test_info.model = "Nano S".to_string();
381 assert!(info.matches(&test_info));
382 test_info.host_device_path = "/host/device/path".to_string();
383 assert!(info.matches(&test_info));
384 let another_pubkey = solana_sdk::pubkey::new_rand();
385 test_info.pubkey = another_pubkey;
386 assert!(!info.matches(&test_info));
387 test_info.pubkey = pubkey;
388 assert!(info.matches(&test_info));
389 }
390
391 #[test]
392 fn test_get_pretty_path() {
393 let pubkey = solana_sdk::pubkey::new_rand();
394 let pubkey_str = pubkey.to_string();
395 let remote_wallet_info = RemoteWalletInfo {
396 model: "nano-s".to_string(),
397 manufacturer: Manufacturer::Ledger,
398 serial: "".to_string(),
399 host_device_path: "/host/device/path".to_string(),
400 pubkey,
401 error: None,
402 };
403 assert_eq!(
404 remote_wallet_info.get_pretty_path(),
405 format!("usb://ledger/{}", pubkey_str)
406 );
407 }
408}