Skip to main content

bhwi/
common.rs

1//! Device-independent commands, responses, and message routing.
2//!
3//! Command support and required context vary by device. Interpreters perform no I/O:
4//! callers route [`Transmit`] messages to the device, PIN server, or host and return
5//! the resulting bytes to [`crate::Interpreter::exchange`].
6
7#[cfg(feature = "bitbox")]
8use crate::bitbox;
9#[cfg(feature = "coldcard")]
10use crate::coldcard;
11#[cfg(feature = "jade")]
12use crate::jade;
13#[cfg(feature = "keepkey")]
14use crate::keepkey;
15#[cfg(feature = "ledger")]
16use crate::ledger;
17use crate::miniscript::descriptor::{DescriptorPublicKey, WalletPolicy};
18#[cfg(feature = "specter")]
19use crate::specter;
20#[cfg(feature = "trezor")]
21use crate::trezor;
22use bitcoin::Network;
23use bitcoin::address::AddressType;
24use bitcoin::bip32::{DerivationPath, Fingerprint, Xpub};
25use bitcoin::psbt::Psbt;
26use bitcoin::secp256k1::ecdsa::Signature;
27
28mod adapters;
29
30/// Options for starting a device session or unlocking a wallet.
31#[derive(Default)]
32pub struct UnlockOptions {
33    /// Requested network, where supported by the backend.
34    ///
35    /// Ledger requires a network to select its Bitcoin application. Trezor and
36    /// KeepKey use it to select the session network; other common adapters ignore it.
37    pub network: Option<Network>,
38}
39
40/// Options for initializing a new device wallet.
41#[derive(Clone, Debug, Default)]
42pub struct SetupOptions {
43    /// Requested device label; empty labels are omitted by Trezor and KeepKey.
44    pub label: String,
45    /// Backup passphrase supplied by the caller.
46    ///
47    /// BitBox rejects nonempty values; the Trezor and KeepKey adapters do not use it.
48    pub backup_passphrase: String,
49}
50
51/// Options for restoring a device wallet.
52#[derive(Clone, Debug)]
53pub struct RestoreOptions {
54    /// Requested device label; empty labels are omitted by Trezor and KeepKey.
55    pub label: String,
56    /// Number of recovery words.
57    ///
58    /// Defaults to 24. Trezor and KeepKey accept 12, 18, or 24; BitBox ignores this field.
59    pub word_count: i32,
60}
61
62impl Default for RestoreOptions {
63    fn default() -> Self {
64        Self {
65            label: String::new(),
66            word_count: 24,
67        }
68    }
69}
70
71/// Inputs for deriving or displaying an address.
72///
73/// Display flags request device confirmation where supported. Some backends ignore
74/// the flag or always display; Specter-DIY rejects requests for undisplayed addresses.
75#[derive(Clone, Debug)]
76pub enum DisplayAddress {
77    /// Derives an address from a device key path.
78    ByPath {
79        /// Key derivation path for the address.
80        path: DerivationPath,
81        /// Whether to request address confirmation on the device.
82        display: bool,
83        /// Requested script format, or the backend's default or path-inferred format.
84        address_format: Option<AddressType>,
85    },
86    /// Derives an address from a registered descriptor or wallet policy.
87    ///
88    /// Ledger, BitBox, and Specter-DIY require their corresponding [`DeviceContext`].
89    /// Trezor and KeepKey do not support this form.
90    ByDescriptor {
91        /// Address index within the selected descriptor branch.
92        index: u32,
93        /// Whether to select the change branch rather than the receiving branch.
94        change: bool,
95        /// Whether to request address confirmation on the device.
96        display: bool,
97        /// Registered descriptor name used by Jade and Coldcard.
98        ///
99        /// Policy-based backends use the supplied context instead.
100        descriptor_name: String,
101    },
102    /// Displays a multisig address using Python HWI-compatible inputs.
103    ByMultisig(
104        /// Multisig script wrapper, threshold, and concrete keys.
105        MultisigDisplayAddress,
106    ),
107}
108
109/// Sans-I/O inputs for Python HWI's `display_multisig_address` operation.
110///
111/// `threshold`, `sorted`, and `keys` correspond to HWI's
112/// `MultisigDescriptor.thresh`, `is_sorted`, and `pubkeys`, respectively.
113#[derive(Clone, Debug)]
114pub struct MultisigDisplayAddress {
115    /// Number of keys required to authorize a spend.
116    pub threshold: u8,
117    /// Script wrapper used to derive the address.
118    pub address_type: MultisigAddressType,
119    /// Whether keys use BIP67 sorting (`sortedmulti` rather than `multi`).
120    pub sorted: bool,
121    /// Descriptor keys, including origins and concrete address derivations.
122    pub keys: Vec<DescriptorPublicKey>,
123}
124
125/// A script wrapper for a multisig address.
126#[derive(Clone, Copy, Debug)]
127pub enum MultisigAddressType {
128    /// Legacy P2SH multisig.
129    Legacy,
130    /// P2SH-wrapped P2WSH multisig.
131    ShWit,
132    /// Native P2WSH multisig.
133    Wit,
134}
135
136/// A device-independent hardware-wallet operation.
137///
138/// Commands are not supported uniformly. Conversion to a device command can fail
139/// for unsupported operations or missing or mismatched [`DeviceContext`].
140#[allow(clippy::large_enum_variant)]
141pub enum Command {
142    /// Creates a backup on the device or retrieves a backup file.
143    Backup,
144    /// Initializes a device wallet using caller-supplied management data.
145    Setup(
146        /// Requested label and backup options.
147        SetupOptions,
148        /// Device-specific setup inputs, such as host entropy.
149        Option<DeviceContext>,
150    ),
151    /// Erases the device's wallet material.
152    Wipe,
153    /// Starts wallet recovery using caller-supplied management data.
154    Restore(
155        /// Requested label and recovery word count.
156        RestoreOptions,
157        /// Device-specific recovery inputs, such as a U2F counter.
158        Option<DeviceContext>,
159    ),
160    /// Toggles the device's BIP-39 passphrase-protection setting.
161    TogglePassphrase,
162    /// Retrieves the wallet's master key fingerprint.
163    GetMasterFingerprint,
164    /// Retrieves backend-specific firmware or application information.
165    GetVersion,
166    /// Retrieves an extended public key.
167    GetXpub {
168        /// Key derivation path.
169        path: DerivationPath,
170        /// Whether to request confirmation on the device, where supported.
171        display: bool,
172    },
173    /// Derives or displays an address.
174    DisplayAddress(
175        /// Address derivation inputs and requested display behavior.
176        DisplayAddress,
177        /// Device-specific policy context required by some address forms.
178        Option<DeviceContext>,
179    ),
180    /// Registers a named wallet policy on a supporting device.
181    ///
182    /// Trezor and KeepKey do not support wallet registration.
183    RegisterWallet {
184        /// Wallet name shown or stored by the device.
185        name: String,
186        /// Wallet policy, including its descriptor keys and origins.
187        policy: WalletPolicy,
188    },
189    /// Signs the inputs of a partially signed Bitcoin transaction.
190    ///
191    /// Ledger requires policy context even when no wallet HMAC is needed. BitBox
192    /// accepts optional policy context; Coldcard, Trezor, and KeepKey require `None`.
193    SignTx(
194        /// Transaction and metadata to sign.
195        Psbt,
196        /// Optional device-specific policy inputs.
197        Option<DeviceContext>,
198    ),
199    /// Signs a message with the key at a derivation path.
200    SignMessage {
201        /// Message bytes in the form accepted by the device protocol.
202        message: Vec<u8>,
203        /// Signing key derivation path.
204        path: DerivationPath,
205    },
206    /// Starts a session, authenticates, pairs, or opens an app, depending on the backend.
207    ///
208    /// This does not uniformly mean that a device's PIN lock is cleared.
209    Unlock {
210        /// Requested session network and unlock options.
211        options: UnlockOptions,
212    },
213    /// Requests a scrambled PIN-entry matrix from a supporting device.
214    PromptPin,
215    /// Submits scrambled keypad positions, not literal PIN digits.
216    SendPin(
217        /// Management context containing the caller-supplied PIN positions.
218        Option<DeviceContext>,
219    ),
220}
221
222/// Device-specific inputs required by certain shared commands.
223///
224/// Management context carries caller-provided entropy, PIN positions, timestamps,
225/// or counters. Interpreters neither collect these inputs nor perform transport I/O.
226#[allow(clippy::large_enum_variant)]
227#[derive(Clone, Debug)]
228pub enum DeviceContext {
229    /// Wallet policy for Ledger signing and descriptor-based address display.
230    #[cfg(feature = "ledger")]
231    Ledger {
232        /// Policy used to derive addresses or identify signing inputs.
233        wallet_policy: ledger::LedgerWalletPolicy,
234        /// Registration HMAC, or `None` for a policy used without a token.
235        ///
236        /// The Ledger context itself is still required when this is `None`.
237        wallet_hmac: Option<[u8; 32]>,
238    },
239    /// Wallet policy for BitBox descriptor display or policy-based signing.
240    #[cfg(feature = "bitbox")]
241    BitBox {
242        /// Descriptor policy with the key origins required by the device.
243        policy: WalletPolicy,
244    },
245    /// Required context for BitBox02 setup and restore operations.
246    #[cfg(feature = "bitbox")]
247    BitBoxManagement(
248        /// Caller-supplied setup mode and time information.
249        bitbox::ManagementContext,
250    ),
251    /// Caller-supplied inputs for Trezor setup, restore, or PIN submission.
252    #[cfg(feature = "trezor")]
253    TrezorManagement(
254        /// Host entropy, U2F counter, or scrambled keypad positions.
255        trezor::ManagementContext,
256    ),
257    /// Caller-supplied inputs for KeepKey setup, restore, or PIN submission.
258    #[cfg(feature = "keepkey")]
259    KeepKeyManagement(
260        /// Host entropy, U2F counter, or scrambled keypad positions.
261        keepkey::ManagementContext,
262    ),
263    /// Wallet policy required for Specter-DIY descriptor address display.
264    #[cfg(feature = "specter")]
265    Specter {
266        /// Descriptor policy used to prepare the address display request.
267        policy: WalletPolicy,
268    },
269}
270
271/// The result of a shared hardware-wallet command.
272pub enum Response {
273    /// The outcome of a backup operation.
274    Backup(
275        /// Backup completion or downloaded file contents.
276        DeviceBackup,
277    ),
278    /// Backend-specific outcome of a management or PIN request.
279    ///
280    /// Trezor PIN prompting and KeepKey passphrase toggling can return `true`
281    /// while PIN entry is still pending.
282    DeviceAction(
283        /// Whether the backend accepted or completed the request.
284        bool,
285    ),
286    /// A terminal outcome without another result.
287    ///
288    /// Ledger also uses this for failed or unsupported signing; it does not
289    /// guarantee success.
290    TaskDone,
291    /// The device reports that an operation is still busy.
292    TaskBusy,
293    /// Backend-specific device or application information.
294    Info(
295        /// Information reported or derived by the backend.
296        Info,
297    ),
298    /// The wallet's master key fingerprint.
299    MasterFingerprint(
300        /// Four-byte BIP-32 fingerprint.
301        Fingerprint,
302    ),
303    /// An extended public key.
304    Xpub(
305        /// Derived BIP-32 extended public key.
306        Xpub,
307    ),
308    /// Coldcard peer public-key material for establishing encryption.
309    EncryptionKey(
310        /// Uncompressed secp256k1 public key without the `0x04` prefix.
311        [u8; 64],
312    ),
313    /// An ECDSA message signature and its backend-specific header.
314    Signature(
315        /// Compact-message signature header, not normalized across backends.
316        u8,
317        /// ECDSA signature, excluding the header byte.
318        Signature,
319    ),
320    /// A PSBT containing signatures returned by the device.
321    SignedPsbt(
322        /// Partially signed transaction; not necessarily finalized or fully signed.
323        Psbt,
324    ),
325    /// An address returned by the device.
326    Address(
327        /// Address in the device protocol's string encoding.
328        String,
329    ),
330    /// The outcome of registering a wallet policy.
331    WalletRegistration(
332        /// Registration state and any authentication token.
333        WalletRegistration,
334    ),
335}
336
337/// Completion state of a wallet-registration request.
338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
339pub enum WalletRegistration {
340    /// Registration completed, with or without an authentication token.
341    Complete {
342        /// Device-issued wallet HMAC, when the backend provides one.
343        hmac: Option<[u8; 32]>,
344    },
345    /// The request was submitted but still requires confirmation on the device.
346    PendingUserConfirmation,
347}
348
349impl WalletRegistration {
350    /// Returns the registration HMAC, if available.
351    ///
352    /// Returns `None` both for completion without a token and for pending user
353    /// confirmation. Inspect the variant to distinguish those states.
354    pub fn hmac(self) -> Option<[u8; 32]> {
355        match self {
356            Self::Complete { hmac } => hmac,
357            Self::PendingUserConfirmation => None,
358        }
359    }
360}
361
362/// Completion or file contents returned by a backup operation.
363#[derive(Clone, Debug, Eq, PartialEq)]
364pub enum DeviceBackup {
365    /// The device completed its backup without returning a downloaded file.
366    Complete,
367    /// A backup file downloaded from the device.
368    File(
369        /// File bytes in the device's backup format.
370        Vec<u8>,
371    ),
372}
373
374/// Backend-specific device, firmware, and session information.
375///
376/// Optional fields may be unreported, not `false`. The meaning and availability of
377/// names and network lists depend on the backend.
378#[derive(Debug, Clone, Default)]
379pub struct Info {
380    /// Firmware or application version string reported by the backend.
381    pub version: String,
382    /// Networks reported by the backend, or an empty list when unreported.
383    ///
384    /// This may describe the active application or selected session network rather
385    /// than every network supported by the hardware.
386    pub networks: Vec<Network>,
387    /// Backend-specific device, model, or application name, when reported.
388    pub firmware: Option<String>,
389    /// Whether the device has initialized wallet material, when reported by the firmware.
390    pub initialized: Option<bool>,
391    /// User-set device name, when the device protocol reports one.
392    pub label: Option<String>,
393    /// Whether the device can take a passphrase on its own screen, when it reports the capability.
394    pub on_device_passphrase_entry: Option<bool>,
395    /// Whether the device is waiting for a PIN from the host, when it reports a lock state.
396    pub needs_pin_sent: Option<bool>,
397    /// Whether the BIP-39 passphrase is expected from the host, when reported or derived.
398    pub needs_passphrase_sent: Option<bool>,
399}
400
401/// A device request for input that the caller must collect from the user.
402#[derive(Clone, Debug, Eq, PartialEq)]
403pub enum HostRequest {
404    /// Requests positions on the scrambled keypad displayed by the device.
405    PinMatrix {
406        /// Whether the device asks for the current PIN or a new PIN confirmation.
407        kind: PinMatrixRequestKind,
408    },
409    /// Requests a character or control action during cipher-based recovery.
410    RecoveryCharacter {
411        /// Zero-based recovery word position reported by the device.
412        word_position: u32,
413        /// Zero-based character position within the current recovery word.
414        character_position: u32,
415    },
416}
417
418/// The purpose of a scrambled PIN-matrix request.
419#[derive(Clone, Copy, Debug, Eq, PartialEq)]
420pub enum PinMatrixRequestKind {
421    /// Entry of the current PIN.
422    Current,
423    /// First entry of a new PIN.
424    NewFirst,
425    /// Confirmation of the new PIN.
426    NewSecond,
427    /// A request kind not recognized by this library.
428    Unknown(
429        /// Raw protocol value.
430        i32,
431    ),
432}
433
434/// A typed answer to a [`HostRequest`].
435///
436/// Encode the answer with [`into_bytes_for`](Self::into_bytes_for) before returning
437/// it to the interpreter. `Debug` output redacts PIN positions and recovery characters.
438#[derive(Eq, PartialEq)]
439pub enum HostResponse {
440    /// Scrambled keypad positions, never the literal PIN digits.
441    PinPositions(
442        /// Nonempty ASCII digits representing the selected positions.
443        String,
444    ),
445    /// A recovery-cipher character selected using the device's displayed mapping.
446    RecoveryCharacter(
447        /// One lowercase ASCII character.
448        char,
449    ),
450    /// Deletes the previous recovery character.
451    RecoveryDelete,
452    /// Advances to the next recovery word.
453    RecoveryNextWord,
454    /// Completes recovery after the last word.
455    RecoveryDone,
456}
457
458impl core::fmt::Debug for HostResponse {
459    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
460        match self {
461            Self::PinPositions(_) => f.write_str("PinPositions(<redacted>)"),
462            Self::RecoveryCharacter(_) => f.write_str("RecoveryCharacter(<redacted>)"),
463            Self::RecoveryDelete => f.write_str("RecoveryDelete"),
464            Self::RecoveryNextWord => f.write_str("RecoveryNextWord"),
465            Self::RecoveryDone => f.write_str("RecoveryDone"),
466        }
467    }
468}
469
470fn zeroize_string(value: &mut String) {
471    zeroize::Zeroize::zeroize(value);
472}
473
474impl HostResponse {
475    /// Validates and encodes this answer for the given host request.
476    ///
477    /// PIN positions encode as ASCII bytes. Validation requires nonempty ASCII
478    /// digits but does not restrict them to the range 1–9. Recovery characters
479    /// must be lowercase ASCII. Delete encodes as `0x08` and is allowed unless both
480    /// positions are zero; next-word encodes as a space and requires a character
481    /// position of at least 3. Done encodes as a newline and requires a word position
482    /// of 11, 17, or 23 and a character position of at least 3.
483    ///
484    /// # Errors
485    ///
486    /// Returns an [`ErrorKind::InvalidInput`] error if the answer does not match the request,
487    /// its contents are invalid, or a recovery action is invalid at that position.
488    ///
489    /// # Examples
490    ///
491    /// Encode synthetic keypad positions, not literal PIN digits:
492    ///
493    /// ```
494    /// use bhwi::common::{ErrorKind, HostRequest, HostResponse, PinMatrixRequestKind};
495    ///
496    /// let request = HostRequest::PinMatrix {
497    ///     kind: PinMatrixRequestKind::Current,
498    /// };
499    /// let bytes = HostResponse::PinPositions("123".into())
500    ///     .into_bytes_for(&request)
501    ///     .unwrap();
502    /// assert_eq!(bytes, b"123");
503    /// let error = HostResponse::PinPositions(String::new())
504    ///     .into_bytes_for(&request)
505    ///     .unwrap_err();
506    /// assert_eq!(error.kind(), ErrorKind::InvalidInput);
507    /// ```
508    pub fn into_bytes_for(self, request: &HostRequest) -> Result<Vec<u8>, Error> {
509        match (request, self) {
510            (HostRequest::PinMatrix { .. }, Self::PinPositions(mut positions)) => {
511                if positions.is_empty() || !positions.bytes().all(|byte| byte.is_ascii_digit()) {
512                    zeroize_string(&mut positions);
513                    return Err(Error::new(
514                        ErrorKind::InvalidInput,
515                        "PIN positions must contain ASCII digits",
516                    ));
517                }
518                Ok(positions.into_bytes())
519            }
520            (HostRequest::RecoveryCharacter { .. }, Self::RecoveryCharacter(character)) => {
521                if !character.is_ascii_lowercase() {
522                    return Err(Error::new(
523                        ErrorKind::InvalidInput,
524                        "recovery cipher response must be one lowercase ASCII character",
525                    ));
526                }
527                Ok(vec![character as u8])
528            }
529            (
530                HostRequest::RecoveryCharacter {
531                    word_position,
532                    character_position,
533                },
534                action,
535            ) => {
536                let byte = match action {
537                    Self::RecoveryDelete if *word_position != 0 || *character_position != 0 => 0x08,
538                    Self::RecoveryNextWord if *character_position >= 3 => b' ',
539                    Self::RecoveryDone
540                        if matches!(*word_position, 11 | 17 | 23) && *character_position >= 3 =>
541                    {
542                        b'\n'
543                    }
544                    Self::RecoveryDelete | Self::RecoveryNextWord | Self::RecoveryDone => {
545                        return Err(Error::new(
546                            ErrorKind::InvalidInput,
547                            "recovery action is not valid at this position",
548                        ));
549                    }
550                    Self::PinPositions(mut positions) => {
551                        zeroize_string(&mut positions);
552                        return Err(Error::new(
553                            ErrorKind::InvalidInput,
554                            "host response does not match request",
555                        ));
556                    }
557                    Self::RecoveryCharacter(_) => {
558                        return Err(Error::new(
559                            ErrorKind::InvalidInput,
560                            "host response does not match request",
561                        ));
562                    }
563                };
564                Ok(vec![byte])
565            }
566            _ => Err(Error::new(
567                ErrorKind::InvalidInput,
568                "host response does not match request",
569            )),
570        }
571    }
572}
573
574/// The destination of a shared interpreter's next request.
575pub enum Recipient {
576    /// The hardware wallet or its emulator.
577    Device,
578    /// An external PIN-server HTTP endpoint.
579    PinServer {
580        /// URL to which the caller sends the transmission payload.
581        url: String,
582    },
583    /// User interaction handled by the caller rather than a transport.
584    Host(
585        /// Request to answer with [`HostResponse::into_bytes_for`].
586        HostRequest,
587    ),
588}
589
590/// A routed request produced by a shared interpreter.
591///
592/// Send device or PIN-server payloads to their recipient and return the response
593/// bytes to [`crate::Interpreter::exchange`]. For host requests, collect a typed
594/// [`HostResponse`] and encode it with [`HostResponse::into_bytes_for`].
595///
596/// Converting a byte vector targets the device with `encrypted` set to `false`.
597/// Converting a [`HostRequest`] targets the host with an empty payload.
598pub struct Transmit {
599    /// Destination of the payload or host-interaction request.
600    pub recipient: Recipient,
601    /// Encoded protocol payload, ready to route; empty for a converted host request.
602    pub payload: Vec<u8>,
603    /// Whether the payload is already encrypted.
604    ///
605    /// Transports may use this flag for framing; callers must not encrypt the
606    /// payload again.
607    pub encrypted: bool,
608}
609
610/// The kind of failure an error reports, shared by every device.
611#[non_exhaustive]
612#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
613pub enum ErrorKind {
614    /// The user declined on the device or dismissed a prompt.
615    UserCancelled,
616    /// The device's login step failed without saying why.
617    AuthenticationRefused,
618    /// The host could not get an answer from the user.
619    HostUnavailable,
620    /// The device needs its PIN before it will answer.
621    Locked,
622    /// The device is reachable but not ready to run commands.
623    NotReady,
624    /// A PIN step was requested but the device is already unlocked.
625    AlreadyUnlocked,
626    /// The device has no wallet.
627    NotInitialized,
628    /// The device already has a wallet.
629    AlreadyInitialized,
630    /// The PIN was wrong.
631    WrongPin,
632    /// A key or address belongs to another network.
633    WrongNetwork,
634    /// The item being created already exists on the device.
635    Duplicate,
636    /// The device or firmware cannot run this command.
637    Unsupported,
638    /// The device cannot display this address type.
639    UnsupportedDisplayAddress,
640    /// The command needs data the caller did not supply.
641    MissingContext,
642    /// The caller's data is malformed or was rejected as invalid.
643    InvalidInput,
644    /// The device refused for a reason no other kind covers.
645    Rejected,
646    /// The device reported an internal failure.
647    DeviceFailure,
648    /// Host and device disagreed on the protocol.
649    Protocol,
650    /// A valid reply that is not the one expected.
651    UnexpectedResponse,
652    /// Bytes could not be encoded or decoded.
653    Serialization,
654    /// Channel encryption or decryption failed.
655    Encryption,
656    /// I/O with the device or a helper service failed.
657    Transport,
658    /// The device went away.
659    Disconnected,
660    /// No other kind applies.
661    Other,
662}
663
664/// A device's own error code, in that vendor's numbering.
665#[non_exhaustive]
666#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
667pub enum DeviceCode {
668    /// A Ledger status word.
669    Ledger(u16),
670    /// A Trezor failure code.
671    Trezor(i32),
672    /// A KeepKey failure code.
673    KeepKey(i32),
674    /// A Jade RPC error code.
675    Jade(i32),
676    /// A BitBox02 error code.
677    BitBox(i32),
678}
679
680/// A failure with its kind, the device's code and message, and any data the device sent.
681#[derive(Debug, Clone, PartialEq, Eq)]
682pub struct Error {
683    kind: ErrorKind,
684    device_code: Option<DeviceCode>,
685    message: String,
686    data: Option<Vec<u8>>,
687}
688
689impl Error {
690    /// Creates an error of `kind` with `message`.
691    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
692        Self {
693            kind,
694            device_code: None,
695            message: message.into(),
696            data: None,
697        }
698    }
699
700    /// Sets the device's own error code.
701    pub fn with_device_code(mut self, code: DeviceCode) -> Self {
702        self.device_code = Some(code);
703        self
704    }
705
706    /// Sets data the device sent with the error; it is never printed.
707    pub fn with_data(mut self, data: Vec<u8>) -> Self {
708        self.data = Some(data);
709        self
710    }
711
712    /// Returns the failure kind.
713    pub fn kind(&self) -> ErrorKind {
714        self.kind
715    }
716
717    /// Returns the device's own error code, if it sent one.
718    pub fn device_code(&self) -> Option<DeviceCode> {
719        self.device_code
720    }
721
722    /// Returns the message without the kind.
723    pub fn message(&self) -> &str {
724        &self.message
725    }
726
727    /// Returns data the device sent with the error.
728    pub fn data(&self) -> Option<&[u8]> {
729        self.data.as_deref()
730    }
731}
732
733impl core::fmt::Display for Error {
734    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
735        if self.message.is_empty() {
736            write!(f, "[{:?}]", self.kind)
737        } else {
738            write!(f, "[{:?}] {}", self.kind, self.message)
739        }
740    }
741}
742
743impl std::error::Error for Error {}
744
745/// The BitBox interpreter using this module's shared command and result types.
746#[cfg(feature = "bitbox")]
747pub type BitBoxInterpreter<'a> = bitbox::BitBoxInterpreter<'a, Command, Transmit, Response, Error>;
748/// The Coldcard interpreter using this module's shared command and result types.
749#[cfg(feature = "coldcard")]
750pub type ColdcardInterpreter<'a> =
751    coldcard::ColdcardInterpreter<'a, Command, Transmit, Response, Error>;
752/// The Jade interpreter using this module's shared command and result types.
753#[cfg(feature = "jade")]
754pub type JadeInterpreter = jade::JadeInterpreter<Command, Transmit, Response, Error>;
755/// The Ledger interpreter using this module's shared command and result types.
756#[cfg(feature = "ledger")]
757pub type LedgerInterpreter = ledger::LedgerInterpreter<Command, Transmit, Response, Error>;
758/// The Trezor interpreter using this module's shared command and result types.
759#[cfg(feature = "trezor")]
760pub type TrezorInterpreter = trezor::TrezorInterpreter<Command, Transmit, Response, Error>;
761/// The KeepKey interpreter using this module's shared command and result types.
762#[cfg(feature = "keepkey")]
763pub type KeepKeyInterpreter = keepkey::KeepKeyInterpreter<Command, Transmit, Response, Error>;
764/// The Specter-DIY interpreter using this module's shared command and result types.
765#[cfg(feature = "specter")]
766pub type SpecterInterpreter = specter::SpecterInterpreter<Command, Transmit, Response, Error>;
767
768impl From<Vec<u8>> for Transmit {
769    fn from(payload: Vec<u8>) -> Transmit {
770        Transmit {
771            recipient: Recipient::Device,
772            payload,
773            encrypted: false,
774        }
775    }
776}
777
778impl From<HostRequest> for Transmit {
779    fn from(request: HostRequest) -> Self {
780        Self {
781            recipient: Recipient::Host(request),
782            payload: Vec::new(),
783            encrypted: false,
784        }
785    }
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791    use crate::Interpreter;
792
793    fn assert_interpreter<I>()
794    where
795        I: Interpreter<
796                Command = super::Command,
797                Transmit = super::Transmit,
798                Response = super::Response,
799                Error = super::Error,
800            >,
801    {
802    }
803
804    #[test]
805    fn common_interpreters_are_satisfied() {
806        #[cfg(feature = "bitbox")]
807        assert_interpreter::<BitBoxInterpreter<'static>>();
808        #[cfg(feature = "coldcard")]
809        assert_interpreter::<ColdcardInterpreter<'static>>();
810        #[cfg(feature = "jade")]
811        assert_interpreter::<JadeInterpreter>();
812        #[cfg(feature = "keepkey")]
813        assert_interpreter::<KeepKeyInterpreter>();
814        #[cfg(feature = "specter")]
815        assert_interpreter::<SpecterInterpreter>();
816        #[cfg(feature = "ledger")]
817        assert_interpreter::<LedgerInterpreter>();
818        #[cfg(feature = "trezor")]
819        assert_interpreter::<TrezorInterpreter>();
820    }
821
822    #[test]
823    fn host_response_validation_and_encoding() {
824        let pin = HostRequest::PinMatrix {
825            kind: PinMatrixRequestKind::Current,
826        };
827        assert_eq!(
828            HostResponse::PinPositions("123".into())
829                .into_bytes_for(&pin)
830                .unwrap(),
831            b"123"
832        );
833        assert!(matches!(
834            HostResponse::PinPositions("".into()).into_bytes_for(&pin),
835            Err(e) if e.kind() == ErrorKind::InvalidInput
836                && e.message() == "PIN positions must contain ASCII digits"
837        ));
838        assert!(matches!(
839            HostResponse::PinPositions("123".into()).into_bytes_for(&pin),
840            Err(e) if e.kind() == ErrorKind::InvalidInput
841                && e.message() == "PIN positions must contain ASCII digits"
842        ));
843
844        let first = HostRequest::RecoveryCharacter {
845            word_position: 0,
846            character_position: 0,
847        };
848        assert_eq!(
849            HostResponse::RecoveryCharacter('a')
850                .into_bytes_for(&first)
851                .unwrap(),
852            b"a"
853        );
854        assert!(matches!(
855            HostResponse::RecoveryCharacter('A').into_bytes_for(&first),
856            Err(e) if e.kind() == ErrorKind::InvalidInput
857                && e.message()
858                    == "recovery cipher response must be one lowercase ASCII character"
859        ));
860        assert!(matches!(
861            HostResponse::RecoveryDelete.into_bytes_for(&first),
862            Err(e) if e.kind() == ErrorKind::InvalidInput
863                && e.message() == "recovery action is not valid at this position"
864        ));
865        for request in [
866            HostRequest::RecoveryCharacter {
867                word_position: 0,
868                character_position: 1,
869            },
870            HostRequest::RecoveryCharacter {
871                word_position: 1,
872                character_position: 0,
873            },
874        ] {
875            assert_eq!(
876                HostResponse::RecoveryDelete
877                    .into_bytes_for(&request)
878                    .unwrap(),
879                [0x08]
880            );
881        }
882
883        let middle = HostRequest::RecoveryCharacter {
884            word_position: 1,
885            character_position: 3,
886        };
887        assert_eq!(
888            HostResponse::RecoveryDelete
889                .into_bytes_for(&middle)
890                .unwrap(),
891            [0x08]
892        );
893        assert_eq!(
894            HostResponse::RecoveryNextWord
895                .into_bytes_for(&middle)
896                .unwrap(),
897            b" "
898        );
899        assert!(HostResponse::RecoveryDone.into_bytes_for(&middle).is_err());
900        let too_early = HostRequest::RecoveryCharacter {
901            word_position: 1,
902            character_position: 2,
903        };
904        assert!(matches!(
905            HostResponse::RecoveryNextWord.into_bytes_for(&too_early),
906            Err(e) if e.kind() == ErrorKind::InvalidInput
907                && e.message() == "recovery action is not valid at this position"
908        ));
909
910        let last = HostRequest::RecoveryCharacter {
911            word_position: 11,
912            character_position: 3,
913        };
914        assert_eq!(
915            HostResponse::RecoveryDone.into_bytes_for(&last).unwrap(),
916            b"\n"
917        );
918        assert!(matches!(
919            HostResponse::PinPositions("1".into()).into_bytes_for(&last),
920            Err(e) if e.kind() == ErrorKind::InvalidInput
921                && e.message() == "host response does not match request"
922        ));
923    }
924
925    #[test]
926    fn host_response_debug_is_redacted() {
927        assert_eq!(
928            format!("{:?}", HostResponse::PinPositions("8675309".into())),
929            "PinPositions(<redacted>)"
930        );
931        assert_eq!(
932            format!("{:?}", HostResponse::RecoveryCharacter('q')),
933            "RecoveryCharacter(<redacted>)"
934        );
935    }
936}