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")]
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 is_valid_hid_device(device_info.usage_page(), device_info.interface_number())
120 && is_valid_ledger(device_info.vendor_id(), device_info.product_id())
121 }) {
122 match usb.open_path(device_info.path()) {
123 Ok(device) => {
124 let mut ledger = LedgerWallet::new(device);
125 let result = ledger.read_device(device_info);
126 match result {
127 Ok(info) => {
128 ledger.pretty_path = info.get_pretty_path();
129 let path = device_info.path().to_str().unwrap().to_string();
130 trace!("Found device: {:?}", info);
131 detected_devices.push(Device {
132 path,
133 info,
134 wallet_type: RemoteWalletType::Ledger(Rc::new(ledger)),
135 })
136 }
137 Err(err) => {
138 error!("Error connecting to ledger device to read info: {}", err);
139 errors.push(err)
140 }
141 }
142 }
143 Err(err) => error!("Error connecting to ledger device to read info: {}", err),
144 }
145 }
146
147 let num_curr_devices = detected_devices.len();
148 *self.devices.write() = detected_devices;
149
150 if num_curr_devices == 0 && !errors.is_empty() {
151 return Err(errors[0].clone());
152 }
153
154 Ok(num_curr_devices - num_prev_devices)
155 }
156
157 #[cfg(not(feature = "hidapi"))]
158 pub fn update_devices(&self) -> Result<usize, RemoteWalletError> {
159 Err(RemoteWalletError::Hid(
160 "hidapi crate compilation disabled in solana-remote-wallet.".to_string(),
161 ))
162 }
163
164 pub fn list_devices(&self) -> Vec<RemoteWalletInfo> {
166 self.devices.read().iter().map(|d| d.info.clone()).collect()
167 }
168
169 #[allow(unreachable_patterns)]
171 pub fn get_ledger(
172 &self,
173 host_device_path: &str,
174 ) -> Result<Rc<LedgerWallet>, RemoteWalletError> {
175 self.devices
176 .read()
177 .iter()
178 .find(|device| device.info.host_device_path == host_device_path)
179 .ok_or(RemoteWalletError::PubkeyNotFound)
180 .and_then(|device| match &device.wallet_type {
181 RemoteWalletType::Ledger(ledger) => Ok(ledger.clone()),
182 _ => Err(RemoteWalletError::DeviceTypeMismatch),
183 })
184 }
185
186 pub fn get_wallet_info(&self, pubkey: &Pubkey) -> Option<RemoteWalletInfo> {
188 self.devices
189 .read()
190 .iter()
191 .find(|d| &d.info.pubkey == pubkey)
192 .map(|d| d.info.clone())
193 }
194
195 pub fn try_connect_polling(&self, max_polling_duration: &Duration) -> bool {
197 let start_time = Instant::now();
198 while start_time.elapsed() <= *max_polling_duration {
199 if let Ok(num_devices) = self.update_devices() {
200 let plural = if num_devices == 1 { "" } else { "s" };
201 trace!("{} Remote Wallet{} found", num_devices, plural);
202 return true;
203 }
204 }
205 false
206 }
207}
208
209#[allow(unused_variables)]
211pub trait RemoteWallet<T> {
212 fn name(&self) -> &str {
213 "unimplemented"
214 }
215
216 fn read_device(&mut self, dev_info: &T) -> Result<RemoteWalletInfo, RemoteWalletError> {
218 unimplemented!();
219 }
220
221 fn get_pubkey(
223 &self,
224 derivation_path: &DerivationPath,
225 confirm_key: bool,
226 ) -> Result<Pubkey, RemoteWalletError> {
227 unimplemented!();
228 }
229
230 fn sign_message(
233 &self,
234 derivation_path: &DerivationPath,
235 data: &[u8],
236 ) -> Result<Signature, RemoteWalletError> {
237 unimplemented!();
238 }
239
240 fn sign_offchain_message(
243 &self,
244 derivation_path: &DerivationPath,
245 message: &[u8],
246 ) -> Result<Signature, RemoteWalletError> {
247 unimplemented!();
248 }
249}
250
251#[derive(Debug)]
253pub struct Device {
254 pub(crate) path: String,
255 pub(crate) info: RemoteWalletInfo,
256 pub wallet_type: RemoteWalletType,
257}
258
259#[derive(Debug)]
261pub enum RemoteWalletType {
262 Ledger(Rc<LedgerWallet>),
263}
264
265#[derive(Debug, Default, Clone)]
267pub struct RemoteWalletInfo {
268 pub model: String,
270 pub manufacturer: Manufacturer,
272 pub serial: String,
274 pub host_device_path: String,
276 pub pubkey: Pubkey,
278 pub error: Option<RemoteWalletError>,
280}
281
282impl RemoteWalletInfo {
283 pub fn parse_locator(locator: Locator) -> Self {
284 RemoteWalletInfo {
285 manufacturer: locator.manufacturer,
286 pubkey: locator.pubkey.unwrap_or_default(),
287 ..RemoteWalletInfo::default()
288 }
289 }
290
291 pub fn get_pretty_path(&self) -> String {
292 format!("usb://{}/{:?}", self.manufacturer, self.pubkey,)
293 }
294
295 pub(crate) fn matches(&self, other: &Self) -> bool {
296 self.manufacturer == other.manufacturer
297 && (self.pubkey == other.pubkey
298 || self.pubkey == Pubkey::default()
299 || other.pubkey == Pubkey::default())
300 }
301}
302
303pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool {
305 usage_page == HID_GLOBAL_USAGE_PAGE || interface_number == HID_USB_DEVICE_CLASS as i32
306}
307
308#[cfg(feature = "hidapi")]
310pub fn initialize_wallet_manager() -> Result<Rc<RemoteWalletManager>, RemoteWalletError> {
311 let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?));
312 Ok(RemoteWalletManager::new(hidapi))
313}
314#[cfg(not(feature = "hidapi"))]
315pub fn initialize_wallet_manager() -> Result<Rc<RemoteWalletManager>, RemoteWalletError> {
316 Err(RemoteWalletError::Hid(
317 "hidapi crate compilation disabled in solana-remote-wallet.".to_string(),
318 ))
319}
320
321pub fn maybe_wallet_manager() -> Result<Option<Rc<RemoteWalletManager>>, RemoteWalletError> {
322 let wallet_manager = initialize_wallet_manager()?;
323 let device_count = wallet_manager.update_devices()?;
324 if device_count > 0 {
325 Ok(Some(wallet_manager))
326 } else {
327 drop(wallet_manager);
328 Ok(None)
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335
336 #[test]
337 fn test_parse_locator() {
338 let pubkey = solana_pubkey::new_rand();
339 let locator = Locator {
340 manufacturer: Manufacturer::Ledger,
341 pubkey: Some(pubkey),
342 };
343 let wallet_info = RemoteWalletInfo::parse_locator(locator);
344 assert!(wallet_info.matches(&RemoteWalletInfo {
345 model: "nano-s".to_string(),
346 manufacturer: Manufacturer::Ledger,
347 serial: "".to_string(),
348 host_device_path: "/host/device/path".to_string(),
349 pubkey,
350 error: None,
351 }));
352
353 let locator = Locator {
355 manufacturer: Manufacturer::Ledger,
356 pubkey: None,
357 };
358 let wallet_info = RemoteWalletInfo::parse_locator(locator);
359 assert!(wallet_info.matches(&RemoteWalletInfo {
360 model: "nano-s".to_string(),
361 manufacturer: Manufacturer::Ledger,
362 serial: "".to_string(),
363 host_device_path: "/host/device/path".to_string(),
364 pubkey: Pubkey::default(),
365 error: None,
366 }));
367 }
368
369 #[test]
370 fn test_remote_wallet_info_matches() {
371 let pubkey = solana_pubkey::new_rand();
372 let info = RemoteWalletInfo {
373 manufacturer: Manufacturer::Ledger,
374 model: "Nano S".to_string(),
375 serial: "0001".to_string(),
376 host_device_path: "/host/device/path".to_string(),
377 pubkey,
378 error: None,
379 };
380 let mut test_info = RemoteWalletInfo {
381 manufacturer: Manufacturer::Unknown,
382 ..RemoteWalletInfo::default()
383 };
384 assert!(!info.matches(&test_info));
385 test_info.manufacturer = Manufacturer::Ledger;
386 assert!(info.matches(&test_info));
387 test_info.model = "Other".to_string();
388 assert!(info.matches(&test_info));
389 test_info.model = "Nano S".to_string();
390 assert!(info.matches(&test_info));
391 test_info.host_device_path = "/host/device/path".to_string();
392 assert!(info.matches(&test_info));
393 let another_pubkey = solana_pubkey::new_rand();
394 test_info.pubkey = another_pubkey;
395 assert!(!info.matches(&test_info));
396 test_info.pubkey = pubkey;
397 assert!(info.matches(&test_info));
398 }
399
400 #[test]
401 fn test_get_pretty_path() {
402 let pubkey = solana_pubkey::new_rand();
403 let pubkey_str = pubkey.to_string();
404 let remote_wallet_info = RemoteWalletInfo {
405 model: "nano-s".to_string(),
406 manufacturer: Manufacturer::Ledger,
407 serial: "".to_string(),
408 host_device_path: "/host/device/path".to_string(),
409 pubkey,
410 error: None,
411 };
412 assert_eq!(
413 remote_wallet_info.get_pretty_path(),
414 format!("usb://ledger/{pubkey_str}")
415 );
416 }
417}