1#[cfg(feature = "hidapi")]
2use {crate::ledger::is_valid_ledger, parking_lot::Mutex, std::sync::Arc};
3use {
4 crate::{
5 ledger::LedgerWallet,
6 ledger_error::LedgerError,
7 locator::{Locator, LocatorError, Manufacturer},
8 },
9 log::*,
10 parking_lot::RwLock,
11 solana_derivation_path::{DerivationPath, DerivationPathError},
12 solana_sdk::{
13 pubkey::Pubkey,
14 signature::{Signature, SignerError},
15 },
16 std::{
17 rc::Rc,
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>>) -> Rc<Self> {
102 Rc::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(Rc::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 solana-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<Rc<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(
234 &self,
235 derivation_path: &DerivationPath,
236 data: &[u8],
237 ) -> Result<Signature, RemoteWalletError> {
238 unimplemented!();
239 }
240
241 fn sign_offchain_message(
244 &self,
245 derivation_path: &DerivationPath,
246 message: &[u8],
247 ) -> Result<Signature, RemoteWalletError> {
248 unimplemented!();
249 }
250}
251
252#[derive(Debug)]
254pub struct Device {
255 pub(crate) path: String,
256 pub(crate) info: RemoteWalletInfo,
257 pub wallet_type: RemoteWalletType,
258}
259
260#[derive(Debug)]
262pub enum RemoteWalletType {
263 Ledger(Rc<LedgerWallet>),
264}
265
266#[derive(Debug, Default, Clone)]
268pub struct RemoteWalletInfo {
269 pub model: String,
271 pub manufacturer: Manufacturer,
273 pub serial: String,
275 pub host_device_path: String,
277 pub pubkey: Pubkey,
279 pub error: Option<RemoteWalletError>,
281}
282
283impl RemoteWalletInfo {
284 pub fn parse_locator(locator: Locator) -> Self {
285 RemoteWalletInfo {
286 manufacturer: locator.manufacturer,
287 pubkey: locator.pubkey.unwrap_or_default(),
288 ..RemoteWalletInfo::default()
289 }
290 }
291
292 pub fn get_pretty_path(&self) -> String {
293 format!("usb://{}/{:?}", self.manufacturer, self.pubkey,)
294 }
295
296 pub(crate) fn matches(&self, other: &Self) -> bool {
297 self.manufacturer == other.manufacturer
298 && (self.pubkey == other.pubkey
299 || self.pubkey == Pubkey::default()
300 || other.pubkey == Pubkey::default())
301 }
302}
303
304pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
306 usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32
307}
308
309#[cfg(feature = "hidapi")]
311pub fn initialize_wallet_manager() -> Result<Rc<RemoteWalletManager>, RemoteWalletError> {
312 let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?));
313 Ok(RemoteWalletManager::new(hidapi))
314}
315#[cfg(not(feature = "hidapi"))]
316pub fn initialize_wallet_manager() -> Result<Rc<RemoteWalletManager>, RemoteWalletError> {
317 Err(RemoteWalletError::Hid(
318 "hidapi crate compilation disabled in solana-remote-wallet.".to_string(),
319 ))
320}
321
322pub fn maybe_wallet_manager() -> Result<Option<Rc<RemoteWalletManager>>, RemoteWalletError> {
323 let wallet_manager = initialize_wallet_manager()?;
324 let device_count = wallet_manager.update_devices()?;
325 if device_count > 0 {
326 Ok(Some(wallet_manager))
327 } else {
328 drop(wallet_manager);
329 Ok(None)
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_parse_locator() {
339 let pubkey = solana_sdk::pubkey::new_rand();
340 let locator = Locator {
341 manufacturer: Manufacturer::Ledger,
342 pubkey: Some(pubkey),
343 };
344 let wallet_info = RemoteWalletInfo::parse_locator(locator);
345 assert!(wallet_info.matches(&RemoteWalletInfo {
346 model: "nano-s".to_string(),
347 manufacturer: Manufacturer::Ledger,
348 serial: "".to_string(),
349 host_device_path: "/host/device/path".to_string(),
350 pubkey,
351 error: None,
352 }));
353
354 let locator = Locator {
356 manufacturer: Manufacturer::Ledger,
357 pubkey: None,
358 };
359 let wallet_info = RemoteWalletInfo::parse_locator(locator);
360 assert!(wallet_info.matches(&RemoteWalletInfo {
361 model: "nano-s".to_string(),
362 manufacturer: Manufacturer::Ledger,
363 serial: "".to_string(),
364 host_device_path: "/host/device/path".to_string(),
365 pubkey: Pubkey::default(),
366 error: None,
367 }));
368 }
369
370 #[test]
371 fn test_remote_wallet_info_matches() {
372 let pubkey = solana_sdk::pubkey::new_rand();
373 let info = RemoteWalletInfo {
374 manufacturer: Manufacturer::Ledger,
375 model: "Nano S".to_string(),
376 serial: "0001".to_string(),
377 host_device_path: "/host/device/path".to_string(),
378 pubkey,
379 error: None,
380 };
381 let mut test_info = RemoteWalletInfo {
382 manufacturer: Manufacturer::Unknown,
383 ..RemoteWalletInfo::default()
384 };
385 assert!(!info.matches(&test_info));
386 test_info.manufacturer = Manufacturer::Ledger;
387 assert!(info.matches(&test_info));
388 test_info.model = "Other".to_string();
389 assert!(info.matches(&test_info));
390 test_info.model = "Nano S".to_string();
391 assert!(info.matches(&test_info));
392 test_info.host_device_path = "/host/device/path".to_string();
393 assert!(info.matches(&test_info));
394 let another_pubkey = solana_sdk::pubkey::new_rand();
395 test_info.pubkey = another_pubkey;
396 assert!(!info.matches(&test_info));
397 test_info.pubkey = pubkey;
398 assert!(info.matches(&test_info));
399 }
400
401 #[test]
402 fn test_get_pretty_path() {
403 let pubkey = solana_sdk::pubkey::new_rand();
404 let pubkey_str = pubkey.to_string();
405 let remote_wallet_info = RemoteWalletInfo {
406 model: "nano-s".to_string(),
407 manufacturer: Manufacturer::Ledger,
408 serial: "".to_string(),
409 host_device_path: "/host/device/path".to_string(),
410 pubkey,
411 error: None,
412 };
413 assert_eq!(
414 remote_wallet_info.get_pretty_path(),
415 format!("usb://ledger/{pubkey_str}")
416 );
417 }
418}