solana_remote_wallet/
ledger.rs

1use {
2    crate::remote_wallet::{
3        RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager,
4    },
5    console::Emoji,
6    dialoguer::{theme::ColorfulTheme, Select},
7    semver::Version as FirmwareVersion,
8    solana_derivation_path::DerivationPath,
9    std::{fmt, rc::Rc},
10};
11#[cfg(feature = "hidapi")]
12use {
13    crate::{ledger_error::LedgerError, locator::Manufacturer},
14    log::*,
15    num_traits::FromPrimitive,
16    solana_pubkey::Pubkey,
17    solana_signature::Signature,
18    std::{cmp::min, convert::TryFrom},
19};
20
21static CHECK_MARK: Emoji = Emoji("✅ ", "");
22
23const DEPRECATE_VERSION_BEFORE: FirmwareVersion = FirmwareVersion::new(0, 2, 0);
24
25const APDU_TAG: u8 = 0x05;
26const APDU_CLA: u8 = 0xe0;
27const APDU_PAYLOAD_HEADER_LEN: usize = 7;
28const DEPRECATED_APDU_PAYLOAD_HEADER_LEN: usize = 8;
29const P1_NON_CONFIRM: u8 = 0x00;
30const P1_CONFIRM: u8 = 0x01;
31const P2_EXTEND: u8 = 0x01;
32const P2_MORE: u8 = 0x02;
33const MAX_CHUNK_SIZE: usize = 255;
34
35const APDU_SUCCESS_CODE: usize = 0x9000;
36
37/// Ledger vendor ID
38const LEDGER_VID: u16 = 0x2c97;
39/// Ledger product IDs
40const LEDGER_NANO_S_PIDS: [u16; 33] = [
41    0x0001, 0x1000, 0x1001, 0x1002, 0x1003, 0x1004, 0x1005, 0x1006, 0x1007, 0x1008, 0x1009, 0x100a,
42    0x100b, 0x100c, 0x100d, 0x100e, 0x100f, 0x1010, 0x1011, 0x1012, 0x1013, 0x1014, 0x1015, 0x1016,
43    0x1017, 0x1018, 0x1019, 0x101a, 0x101b, 0x101c, 0x101d, 0x101e, 0x101f,
44];
45const LEDGER_NANO_X_PIDS: [u16; 33] = [
46    0x0004, 0x4000, 0x4001, 0x4002, 0x4003, 0x4004, 0x4005, 0x4006, 0x4007, 0x4008, 0x4009, 0x400a,
47    0x400b, 0x400c, 0x400d, 0x400e, 0x400f, 0x4010, 0x4011, 0x4012, 0x4013, 0x4014, 0x4015, 0x4016,
48    0x4017, 0x4018, 0x4019, 0x401a, 0x401b, 0x401c, 0x401d, 0x401e, 0x401f,
49];
50const LEDGER_NANO_S_PLUS_PIDS: [u16; 33] = [
51    0x0005, 0x5000, 0x5001, 0x5002, 0x5003, 0x5004, 0x5005, 0x5006, 0x5007, 0x5008, 0x5009, 0x500a,
52    0x500b, 0x500c, 0x500d, 0x500e, 0x500f, 0x5010, 0x5011, 0x5012, 0x5013, 0x5014, 0x5015, 0x5016,
53    0x5017, 0x5018, 0x5019, 0x501a, 0x501b, 0x501c, 0x501d, 0x501e, 0x501f,
54];
55const LEDGER_STAX_PIDS: [u16; 33] = [
56    0x0006, 0x6000, 0x6001, 0x6002, 0x6003, 0x6004, 0x6005, 0x6006, 0x6007, 0x6008, 0x6009, 0x600a,
57    0x600b, 0x600c, 0x600d, 0x600e, 0x600f, 0x6010, 0x6011, 0x6012, 0x6013, 0x6014, 0x6015, 0x6016,
58    0x6017, 0x6018, 0x6019, 0x601a, 0x601b, 0x601c, 0x601d, 0x601e, 0x601f,
59];
60const LEDGER_FLEX_PIDS: [u16; 33] = [
61    0x0007, 0x7000, 0x7001, 0x7002, 0x7003, 0x7004, 0x7005, 0x7006, 0x7007, 0x7008, 0x7009, 0x700a,
62    0x700b, 0x700c, 0x700d, 0x700e, 0x700f, 0x7010, 0x7011, 0x7012, 0x7013, 0x7014, 0x7015, 0x7016,
63    0x7017, 0x7018, 0x7019, 0x701a, 0x701b, 0x701c, 0x701d, 0x701e, 0x701f,
64];
65const LEDGER_TRANSPORT_HEADER_LEN: usize = 5;
66
67const HID_PACKET_SIZE: usize = 64 + HID_PREFIX_ZERO;
68
69#[cfg(windows)]
70const HID_PREFIX_ZERO: usize = 1;
71#[cfg(not(windows))]
72const HID_PREFIX_ZERO: usize = 0;
73
74mod commands {
75    pub const DEPRECATED_GET_APP_CONFIGURATION: u8 = 0x01;
76    pub const DEPRECATED_GET_PUBKEY: u8 = 0x02;
77    pub const DEPRECATED_SIGN_MESSAGE: u8 = 0x03;
78    pub const GET_APP_CONFIGURATION: u8 = 0x04;
79    pub const GET_PUBKEY: u8 = 0x05;
80    pub const SIGN_MESSAGE: u8 = 0x06;
81    pub const SIGN_OFFCHAIN_MESSAGE: u8 = 0x07;
82}
83
84enum ConfigurationVersion {
85    Deprecated(Vec<u8>),
86    Current(Vec<u8>),
87}
88
89#[derive(Debug)]
90pub enum PubkeyDisplayMode {
91    Short,
92    Long,
93}
94
95#[derive(Debug)]
96pub struct LedgerSettings {
97    pub enable_blind_signing: bool,
98    pub pubkey_display: PubkeyDisplayMode,
99}
100
101/// Ledger Wallet device
102pub struct LedgerWallet {
103    #[cfg(feature = "hidapi")]
104    pub device: hidapi::HidDevice,
105    pub pretty_path: String,
106    pub version: FirmwareVersion,
107}
108
109impl fmt::Debug for LedgerWallet {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "HidDevice")
112    }
113}
114
115#[cfg(feature = "hidapi")]
116impl LedgerWallet {
117    pub fn new(device: hidapi::HidDevice) -> Self {
118        Self {
119            device,
120            pretty_path: String::default(),
121            version: FirmwareVersion::new(0, 0, 0),
122        }
123    }
124
125    // Transport Protocol:
126    //		* Communication Channel Id		(2 bytes big endian )
127    //		* Command Tag				(1 byte)
128    //		* Packet Sequence ID			(2 bytes big endian)
129    //		* Payload				(Optional)
130    //
131    // Payload
132    //		* APDU Total Length			(2 bytes big endian)
133    //		* APDU_CLA				(1 byte)
134    //		* APDU_INS				(1 byte)
135    //		* APDU_P1				(1 byte)
136    //		* APDU_P2				(1 byte)
137    //		* APDU_LENGTH 	        (1 byte (2 bytes DEPRECATED))
138    //		* APDU_Payload				(Variable)
139    //
140    fn write(
141        &self,
142        command: u8,
143        p1: u8,
144        p2: u8,
145        data: &[u8],
146        outdated_app: bool,
147    ) -> Result<(), RemoteWalletError> {
148        let data_len = data.len();
149        let mut offset = 0;
150        let mut sequence_number = 0;
151        let mut hid_chunk = [0_u8; HID_PACKET_SIZE];
152
153        while sequence_number == 0 || offset < data_len {
154            let header = if sequence_number == 0 {
155                if outdated_app {
156                    LEDGER_TRANSPORT_HEADER_LEN + DEPRECATED_APDU_PAYLOAD_HEADER_LEN
157                } else {
158                    LEDGER_TRANSPORT_HEADER_LEN + APDU_PAYLOAD_HEADER_LEN
159                }
160            } else {
161                LEDGER_TRANSPORT_HEADER_LEN
162            };
163            let size = min(64 - header, data_len - offset);
164            {
165                let chunk = &mut hid_chunk[HID_PREFIX_ZERO..];
166                chunk[0..5].copy_from_slice(&[
167                    0x01,
168                    0x01,
169                    APDU_TAG,
170                    (sequence_number >> 8) as u8,
171                    (sequence_number & 0xff) as u8,
172                ]);
173
174                if sequence_number == 0 {
175                    if outdated_app {
176                        let data_len = data.len() + 6;
177                        chunk[5..13].copy_from_slice(&[
178                            (data_len >> 8) as u8,
179                            (data_len & 0xff) as u8,
180                            APDU_CLA,
181                            command,
182                            p1,
183                            p2,
184                            (data.len() >> 8) as u8,
185                            data.len() as u8,
186                        ]);
187                    } else {
188                        let data_len = data.len() + 5;
189                        chunk[5..12].copy_from_slice(&[
190                            (data_len >> 8) as u8,
191                            (data_len & 0xff) as u8,
192                            APDU_CLA,
193                            command,
194                            p1,
195                            p2,
196                            data.len() as u8,
197                        ]);
198                    }
199                }
200
201                chunk[header..header + size].copy_from_slice(&data[offset..offset + size]);
202            }
203            trace!("Ledger write {:?}", &hid_chunk[..]);
204            let n = self.device.write(&hid_chunk[..])?;
205            if n < size + header {
206                return Err(RemoteWalletError::Protocol("Write data size mismatch"));
207            }
208            offset += size;
209            sequence_number += 1;
210            if sequence_number >= 0xffff {
211                return Err(RemoteWalletError::Protocol(
212                    "Maximum sequence number reached",
213                ));
214            }
215        }
216        Ok(())
217    }
218
219    // Transport Protocol:
220    //		* Communication Channel Id		(2 bytes big endian )
221    //		* Command Tag				(1 byte)
222    //		* Packet Sequence ID			(2 bytes big endian)
223    //		* Payload				(Optional)
224    //
225    // Payload
226    //		* APDU_LENGTH				(1 byte)
227    //		* APDU_Payload				(Variable)
228    //
229    fn read(&self) -> Result<Vec<u8>, RemoteWalletError> {
230        let mut message_size = 0;
231        let mut message = Vec::new();
232
233        // terminate the loop if `sequence_number` reaches its max_value and report error
234        for chunk_index in 0..=0xffff {
235            let mut chunk: [u8; HID_PACKET_SIZE] = [0; HID_PACKET_SIZE];
236            let chunk_size = self.device.read(&mut chunk)?;
237            trace!("Ledger read {:?}", &chunk[..]);
238            if chunk_size < LEDGER_TRANSPORT_HEADER_LEN
239                || chunk[0] != 0x01
240                || chunk[1] != 0x01
241                || chunk[2] != APDU_TAG
242            {
243                return Err(RemoteWalletError::Protocol("Unexpected chunk header"));
244            }
245            let seq = (chunk[3] as usize) << 8 | (chunk[4] as usize);
246            if seq != chunk_index {
247                return Err(RemoteWalletError::Protocol("Unexpected chunk header"));
248            }
249
250            let mut offset = 5;
251            if seq == 0 {
252                // Read message size and status word.
253                if chunk_size < 7 {
254                    return Err(RemoteWalletError::Protocol("Unexpected chunk header"));
255                }
256                message_size = (chunk[5] as usize) << 8 | (chunk[6] as usize);
257                offset += 2;
258            }
259            message.extend_from_slice(&chunk[offset..chunk_size]);
260            message.truncate(message_size);
261            if message.len() == message_size {
262                break;
263            }
264        }
265        if message.len() < 2 {
266            return Err(RemoteWalletError::Protocol("No status word"));
267        }
268        let status =
269            (message[message.len() - 2] as usize) << 8 | (message[message.len() - 1] as usize);
270        trace!("Read status {:x}", status);
271        Self::parse_status(status)?;
272        let new_len = message.len() - 2;
273        message.truncate(new_len);
274        Ok(message)
275    }
276
277    fn _send_apdu(
278        &self,
279        command: u8,
280        p1: u8,
281        p2: u8,
282        data: &[u8],
283        outdated_app: bool,
284    ) -> Result<Vec<u8>, RemoteWalletError> {
285        self.write(command, p1, p2, data, outdated_app)?;
286        if p1 == P1_CONFIRM && is_last_part(p2) {
287            println!(
288                "Waiting for your approval on {} {}",
289                self.name(),
290                self.pretty_path
291            );
292            let result = self.read()?;
293            println!("{CHECK_MARK}Approved");
294            Ok(result)
295        } else {
296            self.read()
297        }
298    }
299
300    fn send_apdu(
301        &self,
302        command: u8,
303        p1: u8,
304        p2: u8,
305        data: &[u8],
306    ) -> Result<Vec<u8>, RemoteWalletError> {
307        self._send_apdu(command, p1, p2, data, self.outdated_app())
308    }
309
310    fn get_firmware_version(&self) -> Result<FirmwareVersion, RemoteWalletError> {
311        self.get_configuration_vector().map(|config| match config {
312            ConfigurationVersion::Current(config) => {
313                FirmwareVersion::new(config[2].into(), config[3].into(), config[4].into())
314            }
315            ConfigurationVersion::Deprecated(config) => {
316                FirmwareVersion::new(config[1].into(), config[2].into(), config[3].into())
317            }
318        })
319    }
320
321    pub fn get_settings(&self) -> Result<LedgerSettings, RemoteWalletError> {
322        self.get_configuration_vector().map(|config| match config {
323            ConfigurationVersion::Current(config) => {
324                let enable_blind_signing = config[0] != 0;
325                let pubkey_display = if config[1] == 0 {
326                    PubkeyDisplayMode::Long
327                } else {
328                    PubkeyDisplayMode::Short
329                };
330                LedgerSettings {
331                    enable_blind_signing,
332                    pubkey_display,
333                }
334            }
335            ConfigurationVersion::Deprecated(_) => LedgerSettings {
336                enable_blind_signing: false,
337                pubkey_display: PubkeyDisplayMode::Short,
338            },
339        })
340    }
341
342    fn get_configuration_vector(&self) -> Result<ConfigurationVersion, RemoteWalletError> {
343        if let Ok(config) = self._send_apdu(commands::GET_APP_CONFIGURATION, 0, 0, &[], false) {
344            if config.len() != 5 {
345                return Err(RemoteWalletError::Protocol("Version packet size mismatch"));
346            }
347            Ok(ConfigurationVersion::Current(config))
348        } else {
349            let config =
350                self._send_apdu(commands::DEPRECATED_GET_APP_CONFIGURATION, 0, 0, &[], true)?;
351            if config.len() != 4 {
352                return Err(RemoteWalletError::Protocol("Version packet size mismatch"));
353            }
354            Ok(ConfigurationVersion::Deprecated(config))
355        }
356    }
357
358    fn outdated_app(&self) -> bool {
359        self.version < DEPRECATE_VERSION_BEFORE
360    }
361
362    fn parse_status(status: usize) -> Result<(), RemoteWalletError> {
363        if status == APDU_SUCCESS_CODE {
364            Ok(())
365        } else if let Some(err) = LedgerError::from_usize(status) {
366            Err(err.into())
367        } else {
368            Err(RemoteWalletError::Protocol("Unknown error"))
369        }
370    }
371}
372
373#[cfg(not(feature = "hidapi"))]
374impl RemoteWallet<Self> for LedgerWallet {}
375#[cfg(feature = "hidapi")]
376impl RemoteWallet<hidapi::DeviceInfo> for LedgerWallet {
377    fn name(&self) -> &str {
378        "Ledger hardware wallet"
379    }
380
381    fn read_device(
382        &mut self,
383        dev_info: &hidapi::DeviceInfo,
384    ) -> Result<RemoteWalletInfo, RemoteWalletError> {
385        let manufacturer = dev_info
386            .manufacturer_string()
387            .and_then(|s| Manufacturer::try_from(s).ok())
388            .unwrap_or_default();
389        let model = dev_info
390            .product_string()
391            .unwrap_or("Unknown")
392            .to_lowercase()
393            .replace(' ', "-");
394        let serial = dev_info.serial_number().unwrap_or("Unknown").to_string();
395        let host_device_path = dev_info.path().to_string_lossy().to_string();
396        let version = self.get_firmware_version()?;
397        self.version = version;
398        let pubkey_result = self.get_pubkey(&DerivationPath::default(), false);
399        let (pubkey, error) = match pubkey_result {
400            Ok(pubkey) => (pubkey, None),
401            Err(err) => (Pubkey::default(), Some(err)),
402        };
403        Ok(RemoteWalletInfo {
404            model,
405            manufacturer,
406            serial,
407            host_device_path,
408            pubkey,
409            error,
410        })
411    }
412
413    fn get_pubkey(
414        &self,
415        derivation_path: &DerivationPath,
416        confirm_key: bool,
417    ) -> Result<Pubkey, RemoteWalletError> {
418        let derivation_path = extend_and_serialize(derivation_path);
419
420        let key = self.send_apdu(
421            if self.outdated_app() {
422                commands::DEPRECATED_GET_PUBKEY
423            } else {
424                commands::GET_PUBKEY
425            },
426            if confirm_key {
427                P1_CONFIRM
428            } else {
429                P1_NON_CONFIRM
430            },
431            0,
432            &derivation_path,
433        )?;
434        Pubkey::try_from(key).map_err(|_| RemoteWalletError::Protocol("Key packet size mismatch"))
435    }
436
437    fn sign_message(
438        &self,
439        derivation_path: &DerivationPath,
440        data: &[u8],
441    ) -> Result<Signature, RemoteWalletError> {
442        // If the first byte of the data is 0xff then it is an off-chain message
443        // because it starts with the Domain Specifier b"\xffsolana offchain".
444        // On-chain messages, in contrast, start with either 0x80 (MESSAGE_VERSION_PREFIX)
445        // or the number of signatures (0x00 - 0x13).
446        if !data.is_empty() && data[0] == 0xff {
447            return self.sign_offchain_message(derivation_path, data);
448        }
449        let mut payload = if self.outdated_app() {
450            extend_and_serialize(derivation_path)
451        } else {
452            extend_and_serialize_multiple(&[derivation_path])
453        };
454        if data.len() > u16::MAX as usize {
455            return Err(RemoteWalletError::InvalidInput(
456                "Message to sign is too long".to_string(),
457            ));
458        }
459
460        // Check to see if this data needs to be split up and
461        // sent in chunks.
462        let max_size = MAX_CHUNK_SIZE - payload.len();
463        let empty = vec![];
464        let (data, remaining_data) = if data.len() > max_size {
465            data.split_at(max_size)
466        } else {
467            (data, empty.as_ref())
468        };
469
470        // Pack the first chunk
471        if self.outdated_app() {
472            for byte in (data.len() as u16).to_be_bytes().iter() {
473                payload.push(*byte);
474            }
475        }
476        payload.extend_from_slice(data);
477        trace!("Serialized payload length {:?}", payload.len());
478
479        let p2 = if remaining_data.is_empty() {
480            0
481        } else {
482            P2_MORE
483        };
484
485        let p1 = P1_CONFIRM;
486        let mut result = self.send_apdu(
487            if self.outdated_app() {
488                commands::DEPRECATED_SIGN_MESSAGE
489            } else {
490                commands::SIGN_MESSAGE
491            },
492            p1,
493            p2,
494            &payload,
495        )?;
496
497        // Pack and send the remaining chunks
498        if !remaining_data.is_empty() {
499            let mut chunks: Vec<_> = remaining_data
500                .chunks(MAX_CHUNK_SIZE)
501                .map(|data| {
502                    let mut payload = if self.outdated_app() {
503                        (data.len() as u16).to_be_bytes().to_vec()
504                    } else {
505                        vec![]
506                    };
507                    payload.extend_from_slice(data);
508                    let p2 = P2_EXTEND | P2_MORE;
509                    (p2, payload)
510                })
511                .collect();
512
513            // Clear the P2_MORE bit on the last item.
514            chunks.last_mut().unwrap().0 &= !P2_MORE;
515
516            for (p2, payload) in chunks {
517                result = self.send_apdu(
518                    if self.outdated_app() {
519                        commands::DEPRECATED_SIGN_MESSAGE
520                    } else {
521                        commands::SIGN_MESSAGE
522                    },
523                    p1,
524                    p2,
525                    &payload,
526                )?;
527            }
528        }
529
530        Signature::try_from(result)
531            .map_err(|_| RemoteWalletError::Protocol("Signature packet size mismatch"))
532    }
533
534    fn sign_offchain_message(
535        &self,
536        derivation_path: &DerivationPath,
537        message: &[u8],
538    ) -> Result<Signature, RemoteWalletError> {
539        if message.len()
540            > solana_offchain_message::v0::OffchainMessage::MAX_LEN_LEDGER
541                + solana_offchain_message::v0::OffchainMessage::HEADER_LEN
542        {
543            return Err(RemoteWalletError::InvalidInput(
544                "Off-chain message to sign is too long".to_string(),
545            ));
546        }
547
548        let mut data = extend_and_serialize_multiple(&[derivation_path]);
549        data.extend_from_slice(message);
550
551        let p1 = P1_CONFIRM;
552        let mut p2 = 0;
553        let mut payload = data.as_slice();
554        while payload.len() > MAX_CHUNK_SIZE {
555            let chunk = &payload[..MAX_CHUNK_SIZE];
556            self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2 | P2_MORE, chunk)?;
557            payload = &payload[MAX_CHUNK_SIZE..];
558            p2 |= P2_EXTEND;
559        }
560
561        let result = self.send_apdu(commands::SIGN_OFFCHAIN_MESSAGE, p1, p2, payload)?;
562        Signature::try_from(result)
563            .map_err(|_| RemoteWalletError::Protocol("Signature packet size mismatch"))
564    }
565}
566
567/// Check if the detected device is a valid `Ledger device` by checking both the product ID and the vendor ID
568pub fn is_valid_ledger(vendor_id: u16, product_id: u16) -> bool {
569    let product_ids = [
570        LEDGER_NANO_S_PIDS,
571        LEDGER_NANO_X_PIDS,
572        LEDGER_NANO_S_PLUS_PIDS,
573        LEDGER_STAX_PIDS,
574        LEDGER_FLEX_PIDS,
575    ];
576    vendor_id == LEDGER_VID && product_ids.iter().any(|pids| pids.contains(&product_id))
577}
578
579/// Build the derivation path byte array from a DerivationPath selection
580fn extend_and_serialize(derivation_path: &DerivationPath) -> Vec<u8> {
581    let byte = if derivation_path.change().is_some() {
582        4
583    } else if derivation_path.account().is_some() {
584        3
585    } else {
586        2
587    };
588    let mut concat_derivation = vec![byte];
589    for index in derivation_path.path() {
590        concat_derivation.extend_from_slice(&index.to_bits().to_be_bytes());
591    }
592    concat_derivation
593}
594
595fn extend_and_serialize_multiple(derivation_paths: &[&DerivationPath]) -> Vec<u8> {
596    let mut concat_derivation = vec![derivation_paths.len() as u8];
597    for derivation_path in derivation_paths {
598        concat_derivation.append(&mut extend_and_serialize(derivation_path));
599    }
600    concat_derivation
601}
602
603/// Choose a Ledger wallet based on matching info fields
604pub fn get_ledger_from_info(
605    info: RemoteWalletInfo,
606    keypair_name: &str,
607    wallet_manager: &RemoteWalletManager,
608) -> Result<Rc<LedgerWallet>, RemoteWalletError> {
609    let devices = wallet_manager.list_devices();
610    let mut matches = devices
611        .iter()
612        .filter(|&device_info| device_info.matches(&info));
613    if matches
614        .clone()
615        .all(|device_info| device_info.error.is_some())
616    {
617        let first_device = matches.next();
618        if let Some(device) = first_device {
619            return Err(device.error.clone().unwrap());
620        }
621    }
622    let mut matches: Vec<(String, String)> = matches
623        .filter(|&device_info| device_info.error.is_none())
624        .map(|device_info| {
625            let query_item = format!("{} ({})", device_info.get_pretty_path(), device_info.model,);
626            (device_info.host_device_path.clone(), query_item)
627        })
628        .collect();
629    if matches.is_empty() {
630        return Err(RemoteWalletError::NoDeviceFound);
631    }
632    matches.sort_by(|a, b| a.1.cmp(&b.1));
633    let (host_device_paths, items): (Vec<String>, Vec<String>) = matches.into_iter().unzip();
634
635    let wallet_host_device_path = if host_device_paths.len() > 1 {
636        let selection = Select::with_theme(&ColorfulTheme::default())
637            .with_prompt(format!(
638                "Multiple hardware wallets found. Please select a device for {keypair_name:?}"
639            ))
640            .default(0)
641            .items(&items[..])
642            .interact()
643            .unwrap();
644        &host_device_paths[selection]
645    } else {
646        &host_device_paths[0]
647    };
648    wallet_manager.get_ledger(wallet_host_device_path)
649}
650
651//
652fn is_last_part(p2: u8) -> bool {
653    p2 & P2_MORE == 0
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659
660    #[test]
661    fn test_is_last_part() {
662        // Bytes with bit-2 set to 0 should return true
663        assert!(is_last_part(0b00));
664        assert!(is_last_part(0b01));
665        assert!(is_last_part(0b101));
666        assert!(is_last_part(0b1001));
667        assert!(is_last_part(0b1101));
668
669        // Bytes with bit-2 set to 1 should return false
670        assert!(!is_last_part(0b10));
671        assert!(!is_last_part(0b11));
672        assert!(!is_last_part(0b110));
673        assert!(!is_last_part(0b111));
674        assert!(!is_last_part(0b1010));
675
676        // Test implementation-specific uses
677        let p2 = 0;
678        assert!(is_last_part(p2));
679        let p2 = P2_EXTEND | P2_MORE;
680        assert!(!is_last_part(p2));
681        assert!(is_last_part(p2 & !P2_MORE));
682    }
683
684    #[test]
685    fn test_parse_status() {
686        LedgerWallet::parse_status(APDU_SUCCESS_CODE).expect("unexpected result");
687        if let RemoteWalletError::LedgerError(err) = LedgerWallet::parse_status(0x6985).unwrap_err()
688        {
689            assert_eq!(err, LedgerError::UserCancel);
690        }
691        if let RemoteWalletError::Protocol(err) = LedgerWallet::parse_status(0x6fff).unwrap_err() {
692            assert_eq!(err, "Unknown error");
693        }
694    }
695}