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