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