Skip to main content

bhwi_cli/
hwi.rs

1//! Python HWI-compatible argument parsing, device operations, and JSON responses.
2//!
3//! [`parse_args`] and [`process_request`] return values without printing a
4//! response. [`run_cli`] handles command-line streams and prints responses to
5//! stdout; device prompts, help text, and usage text go to stderr. On Unix,
6//! `--stdinpass` prompts for the password on the controlling terminal when available,
7//! falling back to a stderr prompt and stdin input if terminal password reading fails.
8
9use bhwi::bitcoin::psbt::Psbt;
10#[cfg(feature = "keepkey")]
11use bhwi::keepkey::{DEFAULT_KEEPKEY_EMULATOR, KEEPKEY_LOCKED};
12#[cfg(any(feature = "trezor", feature = "keepkey"))]
13use bhwi::passphrase::HostPassphrase;
14use bhwi_async::display_address::{
15    DisplayAddressError, multisig_display_address_from_descriptor,
16    singlesig_display_address_from_descriptor,
17};
18#[cfg(feature = "ledger")]
19use bhwi_async::psbt::{merge_psbt_signatures, strip_legacy_witness_utxos};
20#[cfg(feature = "ledger")]
21use bhwi_async::signing::ledger::{
22    LedgerAddressType, LedgerSigningError, ledger_multisig_display_address, ledger_signing_contexts,
23};
24use bhwi_async::signing::message_signature;
25use bhwi_async::{DeviceBackup, DisplayAddress, RestoreOptions, SetupOptions};
26use bitcoin::{
27    Network, NetworkKind,
28    base64::prelude::{BASE64_STANDARD, Engine as _},
29    bip32::{ChildNumber, DerivationPath, Fingerprint, Xpub},
30};
31use chrono::{Datelike, Local, Timelike};
32use clap::{ArgAction, ArgGroup, CommandFactory, Parser, Subcommand, ValueEnum, error::ErrorKind};
33use miniscript::{
34    Descriptor, DescriptorPublicKey,
35    descriptor::{DescriptorType, checksum},
36};
37use serde::{Serialize, Serializer};
38use std::{
39    ffi::OsString,
40    fs,
41    io::{self, BufRead},
42    path::PathBuf,
43    process::ExitCode,
44    str::FromStr,
45};
46
47#[cfg(feature = "bitbox")]
48use crate::management::{bitbox_restore_context, bitbox_setup_context};
49#[cfg(feature = "keepkey")]
50use crate::management::{keepkey_restore_context, keepkey_setup_context};
51#[cfg(feature = "trezor")]
52use crate::management::{trezor_restore_context, trezor_setup_context};
53#[cfg(target_os = "linux")]
54use crate::udev::{UdevRuleSelection, install_udev_rules};
55use crate::{
56    Device, DeviceManager, DeviceSelector, DeviceType, can_sign_taproot,
57    get_descriptors::{GetDescriptorOptions, get_descriptor},
58    python_hwi_device_manager, reports_device_info,
59};
60
61type HwiResult<T> = std::result::Result<T, HwiError>;
62
63/// The Clap parser for Python HWI-compatible global options and subcommands.
64///
65/// Use [`parse_args`] to obtain an executable [`HwiRequest`], or [`run_cli`] for
66/// stdin handling and compatible stdout, stderr, and exit-status behavior.
67#[derive(Debug, Clone, Parser)]
68#[command(author, version, about = "Python HWI compatible interface")]
69pub struct HwiCli {
70    #[command(subcommand)]
71    command: HwiCliCommand,
72    #[arg(long = "device-type", short = 't')]
73    device_type: Option<String>,
74    #[arg(long = "device-path", short = 'd')]
75    device_path: Option<String>,
76    #[arg(long, short = 'f')]
77    fingerprint: Option<Fingerprint>,
78    #[arg(long, short = 'p')]
79    password: Option<String>,
80    #[arg(long, default_value = "main")]
81    chain: String,
82    #[arg(long)]
83    debug: bool,
84    #[arg(long)]
85    emulators: bool,
86    #[arg(long)]
87    stdin: bool,
88    #[arg(long, short = 'i')]
89    interactive: bool,
90    #[arg(long)]
91    expert: bool,
92    #[arg(long, hide = true)]
93    stdinpass: bool,
94}
95
96/// Subcommands accepted by the Python HWI-compatible argument parser.
97#[derive(Debug, Clone, Subcommand)]
98pub enum HwiCliCommand {
99    /// Enumerates devices and their readiness or connection errors.
100    Enumerate,
101    /// Retrieves the account extended public key for an address type.
102    Getmasterxpub {
103        /// The address type used to select the BIP44-style purpose.
104        #[arg(long = "addr-type", value_enum, default_value = "wit")]
105        addr_type: HwiAddressType,
106        /// The account number, defaulting to zero.
107        #[arg(long, default_value_t = 0)]
108        account: u32,
109    },
110    /// Signs a base64-encoded PSBT.
111    Signtx {
112        /// The base64-encoded PSBT to sign.
113        psbt: String,
114    },
115    /// Signs a UTF-8 message with a key at a derivation path.
116    Signmessage {
117        /// The message text to sign.
118        message: String,
119        /// The BIP32 derivation path of the signing key.
120        path: String,
121    },
122    /// Displays an address selected by exactly one path or descriptor.
123    #[command(group(
124        ArgGroup::new("address_target")
125            .required(true)
126            .args(["path", "desc"])
127    ))]
128    Displayaddress {
129        /// The BIP32 derivation path, mutually exclusive with `desc`.
130        #[arg(long, conflicts_with = "desc")]
131        path: Option<String>,
132        /// The output descriptor, mutually exclusive with `path`.
133        #[arg(long, conflicts_with = "path")]
134        desc: Option<String>,
135        /// The address type for a path request; ignored for a descriptor request.
136        #[arg(long = "addr-type", value_enum, default_value = "wit")]
137        addr_type: HwiAddressType,
138    },
139    /// Retrieves an extended public key at a BIP32 path.
140    Getxpub {
141        /// The derivation path of the extended public key.
142        path: String,
143    },
144    /// Retrieves receive and change descriptors for an account.
145    Getdescriptors {
146        /// The account number, defaulting to zero.
147        #[arg(long, default_value_t = 0)]
148        account: u32,
149    },
150    /// Retrieves ranged descriptors for Bitcoin Core keypool import.
151    Getkeypool {
152        /// The inclusive first child index.
153        start: u32,
154        /// The inclusive last child index.
155        end: u32,
156        /// A compatibility flag; keypool import is enabled unless `nokeypool` is set.
157        #[arg(long, action = ArgAction::SetTrue, conflicts_with = "nokeypool")]
158        keypool: bool,
159        /// Whether to disable keypool and active flags in the returned records.
160        #[arg(long, action = ArgAction::SetTrue)]
161        nokeypool: bool,
162        /// Whether to return only change descriptors; otherwise both branches are used without `path`.
163        #[arg(long, action = ArgAction::SetTrue)]
164        internal: bool,
165        /// The address type, defaulting to native SegWit and mutually exclusive with `all`.
166        #[arg(long = "addr-type", value_enum, conflicts_with = "all")]
167        addr_type: Option<HwiAddressType>,
168        /// Whether to return every address type supported by the selected device.
169        #[arg(long, action = ArgAction::SetTrue)]
170        all: bool,
171        /// The account number, ignored when `path` is supplied.
172        #[arg(long, default_value_t = 0)]
173        account: u32,
174        /// An explicit derivation path starting with `m/` and ending in `/*`.
175        #[arg(long)]
176        path: Option<String>,
177    },
178    /// Initializes a supported device in interactive mode.
179    Setup {
180        /// The requested device label.
181        #[arg(long, short = 'l', default_value = "")]
182        label: String,
183        /// A compatibility option rejected when nonempty for BitBox02 and unused by Trezor and KeepKey.
184        #[arg(long = "backup_passphrase", short = 'b', default_value = "")]
185        backup_passphrase: String,
186    },
187    /// Erases a supported device's wallet.
188    Wipe,
189    /// Restores a supported device from a mnemonic in interactive mode.
190    Restore {
191        /// The requested mnemonic word count.
192        #[arg(long = "word_count", short = 'w', default_value_t = 24)]
193        word_count: i32,
194        /// The requested device label.
195        #[arg(long, short = 'l', default_value = "")]
196        label: String,
197    },
198    /// Requests a BitBox02 mnemonic display or a Coldcard backup file.
199    Backup {
200        /// A compatibility label, required to be empty for BitBox02 and unused by Coldcard.
201        #[arg(long, short = 'l', default_value = "")]
202        label: String,
203        /// A compatibility backup password, required to be empty for BitBox02 and unused by Coldcard.
204        #[arg(long = "backup_passphrase", short = 'b', default_value = "")]
205        backup_passphrase: String,
206    },
207    /// Prompts a supported device to request host-entered PIN positions.
208    Promptpin,
209    /// Sends scrambled keypad positions to a device waiting for a PIN.
210    Sendpin {
211        /// A nonempty ASCII-digit position string, not literal PIN digits.
212        pin: String,
213    },
214    /// Toggles passphrase protection on a supported device.
215    Togglepassphrase,
216    /// Installs all device udev rules and updates permissions on Linux.
217    #[cfg(target_os = "linux")]
218    Installudevrules {
219        /// The directory in which to write the udev rules.
220        #[arg(long, default_value = "/etc/udev/rules.d/")]
221        location: PathBuf,
222    },
223    /// Captures an unrecognized subcommand for compatibility error handling.
224    #[command(external_subcommand)]
225    External(
226        /// The unrecognized command name followed by its arguments.
227        Vec<OsString>,
228    ),
229}
230
231/// A device selector and an operation ready for [`process_request`].
232#[derive(Debug, Clone)]
233pub struct HwiRequest {
234    /// The network, device, and host-passphrase selection.
235    pub selector: HwiSelector,
236    /// The requested operation.
237    pub command: HwiCommand,
238}
239
240/// Device selection as parsed from argv. The device type stays a raw string
241/// until device lookup, matching upstream where an unknown type is a lookup
242/// failure (`-3`), or `-4` only when `get_client` is reached via `-d`.
243#[derive(Debug, Clone)]
244pub struct HwiSelector {
245    /// The Bitcoin network used for device operations and account paths.
246    pub network: Network,
247    /// The master fingerprint to match after opening a device, if supplied.
248    pub fingerprint: Option<Fingerprint>,
249    /// The raw HWI device-family or model name, resolved during device lookup.
250    pub device_type: Option<String>,
251    /// An explicit native transport path, if supplied.
252    pub device_path: Option<String>,
253    /// Whether discovery should include emulators.
254    pub include_emulators: bool,
255    /// The normalized host passphrase for Trezor or KeepKey, if supplied.
256    #[cfg(any(feature = "trezor", feature = "keepkey"))]
257    pub passphrase: Option<HostPassphrase>,
258    /// Whether a password argument was supplied, including an empty password.
259    pub password_given: bool,
260}
261
262impl HwiSelector {
263    fn device_selector(&self, device_type: Option<DeviceType>) -> DeviceSelector {
264        // Whether `passphrase` exists follows bhwi-async's features, which another
265        // crate in the build can widen past this one's.
266        let selector = DeviceSelector {
267            network: self.network,
268            fingerprint: self.fingerprint,
269            device_type,
270            device_path: self.device_path.clone(),
271            include_emulators: self.include_emulators,
272            ..DeviceSelector::default()
273        };
274        #[cfg(any(feature = "trezor", feature = "keepkey"))]
275        let selector = DeviceSelector {
276            passphrase: self.passphrase.clone(),
277            ..selector
278        };
279        selector
280    }
281}
282
283fn hwi_selector_matches_device(
284    raw: Option<&str>,
285    device_type: DeviceType,
286    model: &str,
287    is_emulated: bool,
288) -> bool {
289    let Some(raw) = raw else {
290        return true;
291    };
292    let Ok(selected_type) = parse_device_type(raw) else {
293        return false;
294    };
295    selected_type == device_type
296        && (!raw.eq_ignore_ascii_case("keepkey_simulator")
297            || (model == "keepkey_simulator" && is_emulated))
298}
299
300/// Resolves the raw device type, reproducing upstream's error precedence:
301/// `-1` without type/fingerprint, `-4` for an unknown type with a path, then
302/// `-3` when an unknown type cannot match an enumerated device.
303fn hwi_device_manager(selector: &HwiSelector) -> Result<DeviceManager, HwiError> {
304    if selector.device_type.is_none() && selector.fingerprint.is_none() {
305        return Err(HwiError::new(
306            HwiErrorCode::NoDeviceType,
307            "You must specify a device type or fingerprint for all commands except enumerate",
308        ));
309    }
310    let device_type = match selector.device_type.as_deref() {
311        None => None,
312        Some(raw) => match parse_device_type(raw) {
313            Ok(device_type) => Some(device_type),
314            Err(err) if selector.device_path.is_some() && selector.fingerprint.is_none() => {
315                return Err(err);
316            }
317            Err(_) => {
318                return Err(HwiError::new(
319                    HwiErrorCode::DeviceConnectionError,
320                    "Could not find device with specified fingerprint or type",
321                ));
322            }
323        },
324    };
325    Ok(python_hwi_device_manager(
326        selector.device_selector(device_type),
327        selector.password_given,
328    ))
329}
330
331async fn find_hwi_device(selector: &HwiSelector) -> Result<(DeviceManager, Device), HwiError> {
332    let manager = hwi_device_manager(selector)?;
333    match manager.get_device_with_fingerprint().await {
334        Ok(Some(device))
335            if hwi_selector_matches_device(
336                selector.device_type.as_deref(),
337                device.device_type(),
338                device.model(),
339                device.is_emulated(),
340            ) =>
341        {
342            manager
343                .refuse_password(device.device_type())
344                .map_err(|err| classify_device_error(&err))?;
345            Ok((manager, device))
346        }
347        Ok(Some(_)) | Ok(None) => Err(HwiError::new(
348            HwiErrorCode::DeviceConnectionError,
349            "Could not find device with specified fingerprint or type",
350        )),
351        Err(err) => Err(classify_select_error(
352            selector
353                .device_type
354                .as_deref()
355                .and_then(|raw| parse_device_type(raw).ok()),
356            &err,
357        )),
358    }
359}
360
361/// A failed selection carries the skipped devices only as a list, out of the
362/// `source()` chain; one waiting for its host-entered PIN keeps upstream's -12.
363fn classify_select_error<E: std::error::Error + 'static>(
364    device_type: Option<DeviceType>,
365    err: &bhwi_async::device::SelectError<E>,
366) -> HwiError {
367    if let bhwi_async::device::SelectError::NoUsableDevice(no_usable) = err
368        && let Some(locked) = no_usable.skipped.iter().find(|skipped| {
369            skipped.error.kind() == bhwi::common::ErrorKind::Locked
370                && skipped.error.device_code().is_none()
371        })
372    {
373        return HwiError::new(HwiErrorCode::DeviceNotReady, hwi_message(&locked.error));
374    }
375    match device_type {
376        Some(device_type) => classify_device_error_for(device_type, err),
377        None => classify_device_error(err),
378    }
379}
380
381/// An executable Python HWI-compatible operation.
382///
383/// Device support varies; unsupported operations return an [`HwiError`] in
384/// [`HwiResponse::Error`] rather than succeeding without performing the operation.
385#[derive(Debug, Clone, Eq, PartialEq)]
386pub enum HwiCommand {
387    /// Enumerates devices, including per-device errors and readiness information.
388    Enumerate,
389    /// Retrieves the extended public key for an account and address type.
390    GetMasterXpub {
391        /// The address type used to select the account's derivation purpose.
392        addr_type: HwiAddressType,
393        /// The BIP44-style account number.
394        account: u32,
395    },
396    /// Signs a PSBT, preparing Ledger policy contexts when required.
397    SignTx {
398        /// The base64-encoded PSBT.
399        psbt: String,
400    },
401    /// Signs a UTF-8 message at a BIP32 derivation path.
402    SignMessage {
403        /// The message text.
404        message: String,
405        /// The derivation path of the signing key.
406        path: String,
407    },
408    /// Retrieves an address and requests display on the device.
409    DisplayAddress(
410        /// The path or descriptor identifying the address.
411        HwiDisplayAddressRequest,
412    ),
413    /// Retrieves an extended public key at a BIP32 path.
414    GetXpub {
415        /// The derivation path of the extended public key.
416        path: String,
417        /// Whether to include the decoded extended-key fields in the response.
418        expert: bool,
419    },
420    /// Retrieves receive and change descriptors for supported address types.
421    GetDescriptors {
422        /// The BIP44-style account number.
423        account: u32,
424    },
425    /// Retrieves ranged Bitcoin Core descriptor-import records.
426    GetKeypool {
427        /// The inclusive first child index.
428        start: u32,
429        /// The inclusive last child index, which must not precede `start`.
430        end: u32,
431        /// Whether to select only change; otherwise both branches are returned without `path`.
432        internal: bool,
433        /// The value for both `keypool` and `active` in the returned records.
434        keypool: bool,
435        /// The BIP44-style account number, ignored when `path` is supplied.
436        account: u32,
437        /// The address type to use unless `all` is true.
438        addr_type: HwiAddressType,
439        /// Whether to return every address type supported by the selected device.
440        all: bool,
441        /// An optional derivation path starting with `m/` and ending in `/*`.
442        path: Option<String>,
443    },
444    /// Requests a BitBox02 mnemonic display or writes a Coldcard backup to a local file.
445    Backup {
446        /// A compatibility label, required to be empty for BitBox02 and unused by Coldcard.
447        label: String,
448        /// A compatibility password, required to be empty for BitBox02 and unused by Coldcard.
449        backup_passphrase: String,
450    },
451    /// Initializes a BitBox02, Trezor, or KeepKey.
452    Setup {
453        /// Whether interactive setup is allowed; false is rejected.
454        interactive: bool,
455        /// The requested device label.
456        label: String,
457        /// A compatibility option rejected when nonempty for BitBox02 and unused by Trezor and KeepKey.
458        backup_passphrase: String,
459    },
460    /// Erases a supported device's wallet.
461    Wipe,
462    /// Restores a BitBox02, Trezor, or KeepKey from a mnemonic.
463    Restore {
464        /// Whether interactive recovery is allowed; false is rejected.
465        interactive: bool,
466        /// The requested mnemonic word count.
467        word_count: i32,
468        /// The requested device label.
469        label: String,
470    },
471    /// Toggles passphrase protection on a supported device.
472    TogglePassphrase,
473    /// Requests PIN entry and prints keypad instructions to stderr.
474    PromptPin,
475    /// Sends scrambled keypad positions to a supported device waiting for a PIN.
476    SendPin {
477        /// A nonempty ASCII-digit position string, with no 1–9 range check.
478        pin: String,
479    },
480    /// Produces a device-specific unsupported-action error after selecting a device.
481    UnsupportedDeviceAction(
482        /// The management action used to choose the error message.
483        HwiUnsupportedDeviceAction,
484    ),
485    /// Installs all udev rules, updates the USB group, and reloads rules on Linux.
486    #[cfg(target_os = "linux")]
487    InstallUdevRules {
488        /// The directory in which to write the udev rules.
489        location: PathBuf,
490    },
491    /// Reports an unsupported HWI command without selecting a device.
492    Unsupported(
493        /// The unrecognized command name.
494        String,
495    ),
496}
497
498/// A management action used to choose a device-specific unsupported-command error.
499#[derive(Debug, Clone, Eq, PartialEq)]
500pub enum HwiUnsupportedDeviceAction {
501    /// A setup request.
502    Setup {
503        /// Whether interactive setup was requested.
504        interactive: bool,
505        /// The requested device label.
506        label: String,
507        /// The supplied compatibility backup password.
508        backup_passphrase: String,
509    },
510    /// A wallet-erasure request.
511    Wipe,
512    /// A mnemonic-recovery request.
513    Restore {
514        /// Whether interactive recovery was requested.
515        interactive: bool,
516        /// The requested mnemonic word count.
517        word_count: i32,
518        /// The requested device label.
519        label: String,
520    },
521    /// A backup request.
522    Backup {
523        /// The supplied compatibility label.
524        label: String,
525        /// The supplied compatibility backup password.
526        backup_passphrase: String,
527    },
528    /// A request for host-entered PIN positions.
529    PromptPin,
530    /// A request to submit PIN positions.
531    SendPin {
532        /// The scrambled keypad positions, not literal PIN digits.
533        pin: String,
534    },
535    /// A passphrase-protection toggle request.
536    TogglePassphrase,
537}
538
539/// A path or descriptor identifying an address to display on a device.
540#[derive(Debug, Clone, Eq, PartialEq)]
541pub enum HwiDisplayAddressRequest {
542    /// An address selected by a BIP32 derivation path and address type.
543    Path {
544        /// The BIP32 derivation path to parse.
545        path: String,
546        /// The address encoding to request.
547        addr_type: HwiAddressType,
548    },
549    /// An address selected by an output descriptor.
550    ///
551    /// Supported single-key descriptors are converted to path requests.
552    /// Supported multisig descriptors use backend-specific requests; Ledger
553    /// policy preparation may prompt for registration.
554    Descriptor {
555        /// The output descriptor to parse.
556        descriptor: String,
557    },
558}
559
560/// An HWI address type used for derivation purposes and output descriptors.
561#[derive(Debug, Clone, Copy, Eq, PartialEq, ValueEnum)]
562pub enum HwiAddressType {
563    /// Legacy P2PKH, named `legacy`.
564    #[value(name = "legacy")]
565    Legacy,
566    /// P2SH-wrapped P2WPKH, named `sh_wit`.
567    #[value(name = "sh_wit")]
568    ShWit,
569    /// Native SegWit P2WPKH, named `wit`.
570    #[value(name = "wit")]
571    Wit,
572    /// Taproot P2TR, named `tap`.
573    #[value(name = "tap")]
574    Tap,
575}
576
577/// A Python HWI-compatible JSON error object.
578#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
579pub struct HwiError {
580    /// The human-readable error message, serialized under `error`.
581    pub error: String,
582    /// The negative HWI compatibility code, not a process exit status.
583    pub code: i32,
584}
585
586/// Error categories mapped to Python HWI's negative integer codes.
587///
588/// Classification depends on the operation and backend. For example, Ledger
589/// signing or address-display cancellation is reported as device failure
590/// (`-13`), whereas other backends use action cancellation (`-14`).
591#[derive(Debug, Clone, Copy, Eq, PartialEq)]
592pub enum HwiErrorCode {
593    /// Neither a device type nor a fingerprint was supplied (`-1`).
594    NoDeviceType,
595    /// Required arguments are missing or command-line usage is invalid (`-2`).
596    MissingArguments,
597    /// An explicitly addressed device type is unknown (`-4`).
598    UnknownDevice,
599    /// An argument or device-refused input is invalid (`-7`).
600    BadArgument,
601    /// The command, requested mode, or device operation is unsupported (`-9`).
602    UnsupportedCommand,
603    /// A device operation failed (`-13`).
604    DeviceFailure,
605    /// Device selection, connection, or an unclassified device operation failed (`-3`).
606    DeviceConnectionError,
607    /// Installing udev rules requires root privileges (`-16`).
608    NeedToBeRoot,
609    /// Setup was requested for an initialized device (`-10`).
610    DeviceAlreadyInitialized,
611    /// PIN entry was requested for an unlocked device (`-11`).
612    DeviceAlreadyUnlocked,
613    /// A device requires a PIN or passphrase before it can proceed (`-12`).
614    DeviceNotReady,
615    /// The device has no initialized wallet (`-18`).
616    DeviceNotInitialized,
617    /// Command-line help was requested (`-17`).
618    HelpText,
619    /// The user canceled or refused an operation (`-14`).
620    ActionCanceled,
621    /// The supplied PSBT cannot be parsed (`-5`).
622    InvalidTx,
623}
624
625impl HwiErrorCode {
626    fn code(self) -> i32 {
627        match self {
628            HwiErrorCode::NoDeviceType => -1,
629            HwiErrorCode::MissingArguments => -2,
630            HwiErrorCode::UnknownDevice => -4,
631            HwiErrorCode::BadArgument => -7,
632            HwiErrorCode::UnsupportedCommand => -9,
633            HwiErrorCode::DeviceFailure => -13,
634            HwiErrorCode::DeviceConnectionError => -3,
635            HwiErrorCode::NeedToBeRoot => -16,
636            HwiErrorCode::DeviceAlreadyInitialized => -10,
637            HwiErrorCode::DeviceAlreadyUnlocked => -11,
638            HwiErrorCode::DeviceNotReady => -12,
639            HwiErrorCode::DeviceNotInitialized => -18,
640            HwiErrorCode::HelpText => -17,
641            HwiErrorCode::ActionCanceled => -14,
642            HwiErrorCode::InvalidTx => -5,
643        }
644    }
645}
646
647impl HwiError {
648    fn new(code: HwiErrorCode, error: impl Into<String>) -> Self {
649        Self {
650            error: error.into(),
651            code: code.code(),
652        }
653    }
654}
655
656/// A device record in the Python HWI enumeration response.
657///
658/// Optional fields are omitted when absent, except `label`: its outer `None`
659/// omits the key and `Some(None)` writes JSON `null`. Enumeration includes the
660/// label key for Coldcard, KeepKey, Ledger, and Trezor; other families and
661/// candidates that could not be opened omit it.
662/// Unreported PIN and passphrase requirements are represented as false.
663#[derive(Debug, Serialize)]
664pub struct HwiEnumeratedDevice {
665    /// The device-family name, serialized under the JSON key `type`.
666    #[serde(rename = "type")]
667    pub device_type: String,
668    /// The HWI-compatible model name, including emulator model names.
669    pub model: String,
670    /// The HWI-compatible native transport path.
671    pub path: String,
672    /// The optional label, with an outer option controlling key omission.
673    #[serde(skip_serializing_if = "Option::is_none")]
674    pub label: Option<Option<String>>,
675    /// The master fingerprint as a hexadecimal string, omitted when unavailable.
676    #[serde(
677        default,
678        serialize_with = "option_fingerprint",
679        skip_serializing_if = "Option::is_none"
680    )]
681    pub fingerprint: Option<Fingerprint>,
682    /// Whether the device reports that host PIN positions are required.
683    pub needs_pin_sent: bool,
684    /// The reported host-passphrase requirement, cleared after retrieving the fingerprint.
685    pub needs_passphrase_sent: bool,
686    /// Warning groups, omitted from JSON when empty.
687    #[serde(skip_serializing_if = "Vec::is_empty")]
688    pub warnings: Vec<Vec<String>>,
689    /// A per-device error message, omitted when no error occurred.
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub error: Option<String>,
692    /// The per-device HWI error code, omitted when no error occurred.
693    #[serde(skip_serializing_if = "Option::is_none")]
694    pub code: Option<i32>,
695}
696
697/// An untagged Python HWI-compatible JSON response.
698///
699/// Serialization writes the contained object or array directly, without an enum
700/// name or discriminator. [`HwiEnumeratedDevice`] uses `type` for its device family,
701/// not as a response-variant tag.
702#[derive(Debug, Serialize)]
703#[serde(untagged)]
704pub enum HwiResponse {
705    /// Device enumeration records, serialized as an array.
706    Enumerate(
707        /// The discovered devices and per-device errors.
708        Vec<HwiEnumeratedDevice>,
709    ),
710    /// An extended public key response.
711    GetXpub(
712        /// The extended key and any expert-mode fields.
713        HwiGetXpubResponse,
714    ),
715    /// Receive and change output descriptors.
716    GetDescriptors(
717        /// The account's descriptor lists.
718        HwiGetDescriptorsResponse,
719    ),
720    /// Bitcoin Core descriptor-import records, serialized as an array.
721    GetKeypool(
722        /// The ranged keypool records.
723        Vec<HwiGetKeypoolEntry>,
724    ),
725    /// A PSBT signing result.
726    SignTx(
727        /// The resulting PSBT and whether its serialization changed.
728        HwiSignTxResponse,
729    ),
730    /// A message-signing result.
731    SignMessage(
732        /// The encoded message signature.
733        HwiSignMessageResponse,
734    ),
735    /// A device-displayed address.
736    DisplayAddress(
737        /// The returned Bitcoin address.
738        HwiDisplayAddressResponse,
739    ),
740    /// A management-operation result.
741    Success(
742        /// The operation's reported success flag.
743        HwiSuccessResponse,
744    ),
745    /// An operation or argument error.
746    Error(
747        /// The error message and HWI compatibility code.
748        HwiError,
749    ),
750}
751
752/// A management operation's success response.
753#[derive(Debug, Serialize)]
754pub struct HwiSuccessResponse {
755    /// Whether the device operation reported success.
756    pub success: bool,
757}
758
759/// A PSBT signing response.
760#[derive(Debug, Serialize)]
761pub struct HwiSignTxResponse {
762    /// The resulting PSBT encoded as base64.
763    pub psbt: String,
764    /// Whether signing changed the serialized PSBT, not whether all inputs are signed.
765    pub signed: bool,
766}
767
768/// A Bitcoin message-signing response.
769#[derive(Debug, Serialize)]
770pub struct HwiSignMessageResponse {
771    /// The base64-encoded compact signature with its HWI-normalized header.
772    pub signature: String,
773}
774
775/// The address returned after a device-display request.
776#[derive(Debug, Serialize)]
777pub struct HwiDisplayAddressResponse {
778    /// The Bitcoin address string.
779    pub address: String,
780}
781
782/// An extended public key and optional expert-mode details.
783///
784/// Responses produced by this module omit every field except `xpub` unless
785/// expert mode is requested.
786#[derive(Debug, Serialize)]
787pub struct HwiGetXpubResponse {
788    /// The Base58Check-encoded extended public key.
789    pub xpub: String,
790    /// Whether the extended key uses test-network versions; present only in expert mode.
791    #[serde(skip_serializing_if = "Option::is_none")]
792    pub testnet: Option<bool>,
793    /// Whether this is a private key; always false in generated expert responses.
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub private: Option<bool>,
796    /// The key's BIP32 derivation depth; present only in expert mode.
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub depth: Option<u8>,
799    /// The parent fingerprint as hexadecimal; present only in expert mode.
800    #[serde(
801        default,
802        serialize_with = "option_fingerprint",
803        skip_serializing_if = "Option::is_none"
804    )]
805    pub parent_fingerprint: Option<Fingerprint>,
806    /// The child number with its hardened bit, if any; present only in expert mode.
807    #[serde(skip_serializing_if = "Option::is_none")]
808    pub child_num: Option<u32>,
809    /// The 32-byte chain code as hexadecimal; present only in expert mode.
810    #[serde(skip_serializing_if = "Option::is_none")]
811    pub chaincode: Option<String>,
812    /// The compressed public key as hexadecimal; present only in expert mode.
813    #[serde(skip_serializing_if = "Option::is_none")]
814    pub pubkey: Option<String>,
815}
816
817/// Receive and change descriptors for a device account.
818#[derive(Debug, Serialize)]
819pub struct HwiGetDescriptorsResponse {
820    /// Ranged receive descriptors with checksums and `h`-notation hardened paths.
821    pub receive: Vec<String>,
822    /// Ranged change descriptors with checksums and `h`-notation hardened paths.
823    pub internal: Vec<String>,
824}
825
826/// A ranged descriptor-import record for Bitcoin Core.
827#[derive(Debug, Serialize)]
828pub struct HwiGetKeypoolEntry {
829    /// The wildcard descriptor with checksum and `h`-notation hardened paths.
830    pub desc: String,
831    /// The inclusive first and last child indices; `desc` remains ranged.
832    pub range: [u32; 2],
833    /// The import timestamp marker, set to `"now"` in generated responses.
834    pub timestamp: &'static str,
835    /// Whether this describes the internal change branch.
836    pub internal: bool,
837    /// Whether to add the descriptor to the keypool.
838    pub keypool: bool,
839    /// Whether to activate the descriptor, matching `keypool` in generated responses.
840    pub active: bool,
841    /// Whether the import is watch-only, always true in generated responses.
842    pub watchonly: bool,
843}
844
845/// Parses command-line arguments, including the program name, into an HWI request.
846///
847/// Performs no device I/O, stdin reading, or response printing. Use [`run_cli`]
848/// for `--stdin`, password prompting, and compatible help and usage handling.
849///
850/// # Errors
851///
852/// Returns [`HwiError`] with code `-7` for Clap parsing failures, including help
853/// requests, or the conversion error for invalid global options such as a
854/// network name (`-2`). Raw device types are resolved later during processing.
855pub fn parse_args<I, T>(args: I) -> HwiResult<HwiRequest>
856where
857    I: IntoIterator<Item = T>,
858    T: Into<OsString> + Clone,
859{
860    let cli = HwiCli::try_parse_from(args)
861        .map_err(|err| HwiError::new(HwiErrorCode::BadArgument, err.to_string()))?;
862    request_from_cli(cli)
863}
864
865/// Executes an HWI request and returns its serializable response.
866///
867/// Does not print the response, but device operations may perform I/O and emit
868/// prompts on stderr. Backup files are written to the current directory as
869/// `backup-YYYYMMDD-HHMM.7z`; udev installation also changes system permissions.
870///
871/// Device lookup generally precedes operation-argument validation. Commands
872/// requiring a device report `-1` without a type or fingerprint. Unknown types
873/// report `-4` with an explicit path and no fingerprint, otherwise `-3`;
874/// enumeration ignores unknown type filters. Keypool bounds and some unsupported
875/// address modes are checked before device lookup.
876pub async fn process_request(request: HwiRequest) -> HwiResponse {
877    match request.command {
878        HwiCommand::Enumerate => enumerate(request.selector).await,
879        HwiCommand::GetMasterXpub { addr_type, account } => {
880            get_master_xpub(request.selector, addr_type, account).await
881        }
882        HwiCommand::SignTx { psbt } => sign_tx(request.selector, psbt).await,
883        HwiCommand::SignMessage { message, path } => {
884            sign_message(request.selector, message, path).await
885        }
886        HwiCommand::DisplayAddress(address) => display_address(request.selector, address).await,
887        HwiCommand::GetXpub { path, expert } => get_xpub(request.selector, path, expert).await,
888        HwiCommand::GetDescriptors { account } => get_descriptors(request.selector, account).await,
889        HwiCommand::GetKeypool {
890            start,
891            end,
892            internal,
893            keypool,
894            account,
895            addr_type,
896            all,
897            path,
898        } => {
899            get_keypool(
900                request.selector,
901                HwiGetKeypoolRequest {
902                    start,
903                    end,
904                    internal,
905                    keypool,
906                    account,
907                    addr_type,
908                    all,
909                    path,
910                },
911            )
912            .await
913        }
914        HwiCommand::Backup {
915            label,
916            backup_passphrase,
917        } => backup_device(request.selector, label, backup_passphrase).await,
918        HwiCommand::Setup {
919            interactive,
920            label,
921            backup_passphrase,
922        } => setup_device(request.selector, interactive, label, backup_passphrase).await,
923        HwiCommand::Wipe => wipe_device(request.selector).await,
924        HwiCommand::Restore {
925            interactive,
926            word_count,
927            label,
928        } => restore_device(request.selector, interactive, word_count, label).await,
929        HwiCommand::TogglePassphrase => toggle_passphrase_device(request.selector).await,
930        HwiCommand::PromptPin => prompt_pin_device(request.selector).await,
931        HwiCommand::SendPin { pin } => send_pin_device(request.selector, pin).await,
932        HwiCommand::UnsupportedDeviceAction(action) => {
933            unsupported_device_action(request.selector, action).await
934        }
935        #[cfg(target_os = "linux")]
936        HwiCommand::InstallUdevRules { location } => install_udev_rules_hwi(location),
937        HwiCommand::Unsupported(command) => HwiResponse::Error(HwiError::new(
938            HwiErrorCode::UnsupportedCommand,
939            format!("Unsupported HWI command: {command}"),
940        )),
941    }
942}
943
944/// Runs the Python HWI-compatible CLI and returns its process exit status.
945///
946/// `args` includes the program name. With `--stdin`, whitespace-separated
947/// arguments are appended from input lines until an empty line or EOF.
948/// Responses are printed as untagged JSON to stdout, except version output.
949/// Help prints a `-17` JSON error to stdout and help text to stderr. Usage errors
950/// print a `-2` JSON error and usage text, and return status 2; help, version,
951/// successful operations, and runtime error responses return status 0.
952pub async fn run_cli<I, T>(args: I) -> ExitCode
953where
954    I: IntoIterator<Item = T>,
955    T: Into<OsString> + Clone,
956{
957    let args = match args_from_stdin(args) {
958        Ok(args) => args,
959        Err(err) => {
960            return print_response(HwiResponse::Error(HwiError::new(
961                HwiErrorCode::BadArgument,
962                err.to_string(),
963            )));
964        }
965    };
966    let outcome = cli_outcome(args);
967    let status = ExitCode::from(exit_status(&outcome));
968    match outcome {
969        CliOutcome::Stdout(text) => {
970            print!("{text}");
971            status
972        }
973        CliOutcome::Help(help) => {
974            // Python HWI 3.2.0 prints a `-17` JSON object on stdout and the
975            // help text on stderr (hwilib/_cli.py HWIHelpAction).
976            println!(
977                "{}",
978                serde_json::to_string(&HwiError::new(
979                    HwiErrorCode::HelpText,
980                    "Help text requested",
981                ))
982                .expect("serialize HWI help error")
983            );
984            eprint!("{help}");
985            status
986        }
987        CliOutcome::Usage(usage) => {
988            println!(
989                "{}",
990                serde_json::to_string(&HwiError {
991                    error: usage.message,
992                    code: HwiErrorCode::MissingArguments.code(),
993                })
994                .expect("serialize HWI usage error")
995            );
996            eprintln!("{}", usage.usage);
997            status
998        }
999        CliOutcome::Response(response) => print_response(response),
1000        CliOutcome::Request(request) => print_response(process_request(*request).await),
1001    }
1002}
1003
1004/// Argparse-style usage failure: `message` goes to stdout as HWI JSON, `usage` to stderr.
1005#[derive(Debug, Clone, Eq, PartialEq)]
1006struct UsageError {
1007    message: String,
1008    usage: String,
1009}
1010
1011#[derive(Debug)]
1012enum CliOutcome {
1013    Stdout(String),
1014    Help(String),
1015    Usage(UsageError),
1016    Response(HwiResponse),
1017    Request(Box<HwiRequest>),
1018}
1019
1020fn exit_status(outcome: &CliOutcome) -> u8 {
1021    match outcome {
1022        CliOutcome::Usage(_) => 2,
1023        _ => 0,
1024    }
1025}
1026
1027fn cli_outcome(args: Vec<OsString>) -> CliOutcome {
1028    let prog = program_name(&args);
1029    let mut cli = match HwiCli::try_parse_from(args) {
1030        Ok(cli) => cli,
1031        Err(err) => return clap_outcome(&prog, err),
1032    };
1033    if let Err(err) = read_stdin_password(&mut cli) {
1034        return CliOutcome::Response(HwiResponse::Error(err));
1035    }
1036    match request_from_cli(cli) {
1037        Ok(request) => {
1038            if let HwiCommand::Unsupported(command) = &request.command {
1039                CliOutcome::Usage(UsageError {
1040                    message: format!(
1041                        "{prog}: error: argument command: invalid choice: '{command}'"
1042                    ),
1043                    usage: top_level_usage(&prog),
1044                })
1045            } else {
1046                CliOutcome::Request(Box::new(request))
1047            }
1048        }
1049        Err(err) if err.code == HwiErrorCode::MissingArguments.code() => {
1050            CliOutcome::Usage(UsageError {
1051                message: format!("{prog}: error: {}", err.error),
1052                usage: top_level_usage(&prog),
1053            })
1054        }
1055        Err(err) => CliOutcome::Response(HwiResponse::Error(err)),
1056    }
1057}
1058
1059fn clap_outcome(prog: &str, err: clap::Error) -> CliOutcome {
1060    match err.kind() {
1061        // Upstream shares one HWIArgumentParser across subparsers, so every
1062        // `--help` takes the `-17` JSON path; `--version` stays plain stdout.
1063        ErrorKind::DisplayHelp => CliOutcome::Help(err.to_string()),
1064        ErrorKind::DisplayVersion => CliOutcome::Stdout(err.to_string()),
1065        ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand | ErrorKind::MissingSubcommand => {
1066            CliOutcome::Usage(UsageError {
1067                message: format!("{prog}: error: the following arguments are required: command"),
1068                usage: top_level_usage(prog),
1069            })
1070        }
1071        ErrorKind::MissingRequiredArgument
1072        | ErrorKind::UnknownArgument
1073        | ErrorKind::InvalidValue
1074        | ErrorKind::ValueValidation
1075        | ErrorKind::InvalidSubcommand
1076        | ErrorKind::ArgumentConflict
1077        | ErrorKind::NoEquals
1078        | ErrorKind::WrongNumberOfValues
1079        | ErrorKind::TooManyValues
1080        | ErrorKind::TooFewValues => CliOutcome::Usage(clap_usage_error(prog, &err)),
1081        // Every other kind keeps the pre-existing runtime error JSON.
1082        _ => CliOutcome::Response(HwiResponse::Error(HwiError::new(
1083            HwiErrorCode::BadArgument,
1084            err.to_string(),
1085        ))),
1086    }
1087}
1088
1089fn clap_usage_error(prog: &str, err: &clap::Error) -> UsageError {
1090    let rendered = err.render().to_string();
1091    let mut message = String::new();
1092    let mut usage = String::new();
1093    let mut in_usage = false;
1094    for line in rendered.lines() {
1095        if line.starts_with("Usage:") {
1096            in_usage = true;
1097        }
1098        if line.starts_with("For more information, try") {
1099            continue;
1100        }
1101        if in_usage {
1102            if !usage.is_empty() {
1103                usage.push('\n');
1104            }
1105            usage.push_str(line);
1106        } else {
1107            for word in line.split_whitespace() {
1108                if !message.is_empty() {
1109                    message.push(' ');
1110                }
1111                message.push_str(word);
1112            }
1113        }
1114    }
1115    let message = message
1116        .strip_prefix("error: ")
1117        .unwrap_or(&message)
1118        .to_owned();
1119    let usage = usage.trim();
1120    UsageError {
1121        message: format!("{prog}: error: {message}"),
1122        usage: if usage.is_empty() {
1123            top_level_usage(prog)
1124        } else {
1125            usage.to_owned()
1126        },
1127    }
1128}
1129
1130fn program_name(args: &[OsString]) -> String {
1131    args.first()
1132        .map(PathBuf::from)
1133        .and_then(|path| {
1134            path.file_name()
1135                .map(|name| name.to_string_lossy().into_owned())
1136        })
1137        .filter(|name| !name.is_empty())
1138        .unwrap_or_else(|| "hwi".to_owned())
1139}
1140
1141fn top_level_usage(prog: &str) -> String {
1142    let mut command = HwiCli::command().bin_name(prog);
1143    command.render_usage().to_string()
1144}
1145
1146fn args_from_stdin<I, T>(args: I) -> io::Result<Vec<OsString>>
1147where
1148    I: IntoIterator<Item = T>,
1149    T: Into<OsString>,
1150{
1151    let mut args = args.into_iter().map(Into::into).collect::<Vec<_>>();
1152    if !args.iter().any(|arg| arg == "--stdin") {
1153        return Ok(args);
1154    }
1155
1156    for line in io::stdin().lock().lines() {
1157        let line = line?;
1158        if line.is_empty() {
1159            break;
1160        }
1161        args.extend(line.split_whitespace().map(OsString::from));
1162    }
1163
1164    Ok(args)
1165}
1166
1167const EMPTY_PASSPHRASE_WARNING: &str = "Passphrase protection enabled but passphrase was not provided. Using default passphrase of the empty string (\"\")";
1168const KEEPKEY_PASSPHRASE_REQUIRED: &str =
1169    "Passphrase needs to be specified before the fingerprint information can be retrieved";
1170
1171async fn enumerate(selector: HwiSelector) -> HwiResponse {
1172    // Upstream enumerate ignores an unrecognized -t entirely; drop the filter
1173    // rather than failing so the exit stays 0 with a JSON array.
1174    let device_type = selector
1175        .device_type
1176        .as_deref()
1177        .and_then(|raw| parse_device_type(raw).ok());
1178    let raw_device_type = device_type.and(selector.device_type.as_deref());
1179    let manager = python_hwi_device_manager(
1180        selector.device_selector(device_type),
1181        selector.password_given,
1182    );
1183    let scan = match manager.enumerate().await {
1184        Ok(scan) => scan,
1185        Err(err) => {
1186            return HwiResponse::Error(device_error(&err));
1187        }
1188    };
1189    let mut response = Vec::with_capacity(scan.devices.len() + scan.skipped.len());
1190    for mut device in scan.devices {
1191        if !hwi_selector_matches_device(
1192            raw_device_type,
1193            device.device_type(),
1194            device.model(),
1195            device.is_emulated(),
1196        ) {
1197            continue;
1198        }
1199        let mut error = None;
1200        let mut code = None;
1201        let mut info = None;
1202        let mut fingerprint = None;
1203        let mut needs_pin_sent = false;
1204        let mut needs_passphrase_sent = false;
1205        let mut warnings = Vec::new();
1206        match device.device().unlock(manager.selector.network).await {
1207            Ok(()) => {
1208                // A device waiting for a PIN stops answering every other request, and the
1209                // fingerprint request is what makes it start waiting.
1210                if reports_device_info(device.device_type()) {
1211                    match device.info().await {
1212                        Ok(device_info) => info = Some(device_info),
1213                        Err(err) => {
1214                            error = Some(hwi_error_text(&err));
1215                            code = Some(HwiErrorCode::DeviceConnectionError.code());
1216                        }
1217                    }
1218                }
1219                needs_pin_sent = info
1220                    .as_ref()
1221                    .and_then(|info| info.needs_pin_sent)
1222                    .unwrap_or(false);
1223                needs_passphrase_sent = info
1224                    .as_ref()
1225                    .and_then(|info| info.needs_passphrase_sent)
1226                    .unwrap_or(false);
1227                if needs_pin_sent {
1228                    #[cfg(feature = "keepkey")]
1229                    if device.device_type() == DeviceType::KeepKey {
1230                        error = Some(KEEPKEY_LOCKED.to_owned());
1231                    }
1232                    #[cfg(feature = "trezor")]
1233                    if device.device_type() == DeviceType::Trezor {
1234                        error = Some(bhwi::trezor::TrezorError::LOCKED.to_owned());
1235                    }
1236                    code = Some(HwiErrorCode::DeviceNotReady.code());
1237                } else if error.is_none() {
1238                    if needs_passphrase_sent && !manager.password_given() {
1239                        if device.device_type() == DeviceType::KeepKey {
1240                            error = Some(KEEPKEY_PASSPHRASE_REQUIRED.to_owned());
1241                            code = Some(HwiErrorCode::DeviceNotReady.code());
1242                        } else {
1243                            warnings.push(vec![EMPTY_PASSPHRASE_WARNING.to_owned()]);
1244                        }
1245                    }
1246                    if error.is_none()
1247                        && info.as_ref().and_then(|info| info.initialized) == Some(false)
1248                    {
1249                        error = Some("Not initialized".to_owned());
1250                        code = Some(HwiErrorCode::DeviceNotInitialized.code());
1251                    }
1252                    if error.is_none() {
1253                        match device.fingerprint().await {
1254                            Ok(device_fingerprint) => {
1255                                fingerprint = Some(device_fingerprint);
1256                                // Deriving it required the passphrase, so it has been sent.
1257                                needs_passphrase_sent = false;
1258                            }
1259                            Err(err)
1260                                if is_uninitialized_bitbox_error(device.device_type(), &err) =>
1261                            {
1262                                error = Some("Not initialized".to_owned());
1263                                code = Some(HwiErrorCode::DeviceNotInitialized.code());
1264                            }
1265                            Err(err) => {
1266                                let classified = classify_device_error(&err);
1267                                error = Some(classified.error);
1268                                code = Some(classified.code);
1269                            }
1270                        }
1271                    }
1272                }
1273            }
1274            Err(err) if is_uninitialized_bitbox_error(device.device_type(), &err) => {
1275                error = Some("Not initialized".to_owned());
1276                code = Some(HwiErrorCode::DeviceNotInitialized.code());
1277            }
1278            Err(err) => {
1279                let classified = classify_device_error(&err);
1280                error = Some(classified.error);
1281                code = Some(classified.code);
1282            }
1283        }
1284        let label = info.as_ref().and_then(|info| info.label.clone());
1285        let firmware = info.as_ref().and_then(|info| info.firmware.clone());
1286        response.push(HwiEnumeratedDevice {
1287            device_type: device.device_type().to_string(),
1288            model: hwi_enumerate_model(
1289                device.device_type(),
1290                device.model(),
1291                device.is_emulated(),
1292                firmware.as_deref(),
1293            ),
1294            path: hwi_enumerate_path(device.device_type(), device.path(), device.is_emulated()),
1295            label: label_for(device.device_type(), label),
1296            fingerprint,
1297            needs_pin_sent,
1298            needs_passphrase_sent,
1299            warnings,
1300            error,
1301            code,
1302        });
1303    }
1304    // Python HWI lists unopenable devices with an error rather than omitting them.
1305    for skipped in scan.skipped {
1306        response.push(HwiEnumeratedDevice {
1307            device_type: skipped.device_type.to_string(),
1308            model: skipped.model,
1309            path: skipped.path,
1310            label: None,
1311            fingerprint: None,
1312            needs_pin_sent: false,
1313            needs_passphrase_sent: false,
1314            warnings: Vec::new(),
1315            error: Some(hwi_message(&skipped.error)),
1316            code: Some(HwiErrorCode::DeviceConnectionError.code()),
1317        });
1318    }
1319    HwiResponse::Enumerate(response)
1320}
1321
1322#[cfg(target_os = "linux")]
1323fn install_udev_rules_hwi(location: PathBuf) -> HwiResponse {
1324    match install_udev_rules(&location, UdevRuleSelection::All) {
1325        Ok(()) => HwiResponse::Success(HwiSuccessResponse { success: true }),
1326        Err(err) if err.needs_root() => HwiResponse::Error(HwiError::new(
1327            HwiErrorCode::NeedToBeRoot,
1328            "installudevrules failed: Need to be root.",
1329        )),
1330        Err(err) => HwiResponse::Error(HwiError::new(
1331            HwiErrorCode::DeviceFailure,
1332            format!("installudevrules failed: {err}"),
1333        )),
1334    }
1335}
1336
1337async fn unsupported_device_action(
1338    selector: HwiSelector,
1339    action: HwiUnsupportedDeviceAction,
1340) -> HwiResponse {
1341    let (_manager, device) = match find_hwi_device(&selector).await {
1342        Ok(found) => found,
1343        Err(err) => return HwiResponse::Error(err),
1344    };
1345
1346    let error = match action {
1347        HwiUnsupportedDeviceAction::Setup { interactive, .. } if !interactive => {
1348            "setup requires interactive mode".to_owned()
1349        }
1350        HwiUnsupportedDeviceAction::Restore { interactive, .. } if !interactive => {
1351            "restore requires interactive mode".to_owned()
1352        }
1353        action => hwi_unavailable_action_message(device.device_type(), &action),
1354    };
1355
1356    HwiResponse::Error(HwiError::new(HwiErrorCode::UnsupportedCommand, error))
1357}
1358
1359async fn setup_device(
1360    selector: HwiSelector,
1361    interactive: bool,
1362    label: String,
1363    backup_passphrase: String,
1364) -> HwiResponse {
1365    let (_manager, mut device) = match find_hwi_device(&selector).await {
1366        Ok(found) => found,
1367        Err(err) => return HwiResponse::Error(err),
1368    };
1369
1370    if !interactive {
1371        return HwiResponse::Error(HwiError::new(
1372            HwiErrorCode::UnsupportedCommand,
1373            "setup requires interactive mode",
1374        ));
1375    }
1376    if matches!(
1377        device.device_type(),
1378        DeviceType::KeepKey | DeviceType::Trezor
1379    ) && device.info().await.ok().and_then(|info| info.initialized) == Some(true)
1380    {
1381        return HwiResponse::Error(HwiError::new(
1382            HwiErrorCode::DeviceAlreadyInitialized,
1383            "Device is already initialized. Use wipe first and try again",
1384        ));
1385    }
1386    if !matches!(
1387        device.device_type(),
1388        DeviceType::BitBox02 | DeviceType::KeepKey | DeviceType::Trezor
1389    ) {
1390        let action = HwiUnsupportedDeviceAction::Setup {
1391            interactive,
1392            label,
1393            backup_passphrase,
1394        };
1395        return HwiResponse::Error(HwiError::new(
1396            HwiErrorCode::UnsupportedCommand,
1397            hwi_unavailable_action_message(device.device_type(), &action),
1398        ));
1399    }
1400    if device.device_type() == DeviceType::BitBox02 && !backup_passphrase.is_empty() {
1401        return HwiResponse::Error(HwiError::new(
1402            HwiErrorCode::UnsupportedCommand,
1403            "Passphrase not needed when setting up a BitBox02.",
1404        ));
1405    }
1406    if device.device_type() == DeviceType::BitBox02
1407        && device.info().await.ok().and_then(|info| info.initialized) == Some(true)
1408    {
1409        return HwiResponse::Error(HwiError::new(
1410            HwiErrorCode::UnsupportedCommand,
1411            "The BitBox02 must be wiped before setup.",
1412        ));
1413    }
1414
1415    let context = match device.device_type() {
1416        #[cfg(feature = "keepkey")]
1417        DeviceType::KeepKey => keepkey_setup_context(),
1418        #[cfg(feature = "trezor")]
1419        DeviceType::Trezor => trezor_setup_context(),
1420        #[cfg(feature = "bitbox")]
1421        DeviceType::BitBox02 => match bitbox_setup_context(device.is_emulated()) {
1422            Ok(context) => context,
1423            Err(err) => {
1424                return HwiResponse::Error(HwiError::new(
1425                    HwiErrorCode::DeviceFailure,
1426                    err.to_string(),
1427                ));
1428            }
1429        },
1430        #[allow(unreachable_patterns)]
1431        device_type => {
1432            return HwiResponse::Error(HwiError::new(
1433                HwiErrorCode::UnsupportedCommand,
1434                format!("{device_type} support is not compiled into this build"),
1435            ));
1436        }
1437    };
1438    match device
1439        .device()
1440        .setup_device(
1441            SetupOptions {
1442                label,
1443                backup_passphrase,
1444            },
1445            Some(context),
1446        )
1447        .await
1448    {
1449        Ok(success) => HwiResponse::Success(HwiSuccessResponse { success }),
1450        Err(err) => HwiResponse::Error(classify_device_error(&err)),
1451    }
1452}
1453
1454async fn wipe_device(selector: HwiSelector) -> HwiResponse {
1455    let (_manager, mut device) = match find_hwi_device(&selector).await {
1456        Ok(found) => found,
1457        Err(err) => return HwiResponse::Error(err),
1458    };
1459
1460    if !matches!(
1461        device.device_type(),
1462        DeviceType::BitBox02 | DeviceType::KeepKey | DeviceType::Trezor
1463    ) {
1464        return HwiResponse::Error(HwiError::new(
1465            HwiErrorCode::UnsupportedCommand,
1466            hwi_unavailable_action_message(device.device_type(), &HwiUnsupportedDeviceAction::Wipe),
1467        ));
1468    }
1469    if device.device_type() == DeviceType::BitBox02
1470        && device.info().await.ok().and_then(|info| info.initialized) == Some(false)
1471    {
1472        return HwiResponse::Error(HwiError::new(
1473            HwiErrorCode::DeviceNotInitialized,
1474            "The BitBox02 must be initialized first.",
1475        ));
1476    }
1477
1478    match device.device().wipe_device().await {
1479        Ok(success) => HwiResponse::Success(HwiSuccessResponse { success }),
1480        Err(err) => HwiResponse::Error(classify_device_error(&err)),
1481    }
1482}
1483
1484async fn restore_device(
1485    selector: HwiSelector,
1486    interactive: bool,
1487    word_count: i32,
1488    label: String,
1489) -> HwiResponse {
1490    let (_manager, mut device) = match find_hwi_device(&selector).await {
1491        Ok(found) => found,
1492        Err(err) => return HwiResponse::Error(err),
1493    };
1494
1495    if !interactive {
1496        return HwiResponse::Error(HwiError::new(
1497            HwiErrorCode::UnsupportedCommand,
1498            "restore requires interactive mode",
1499        ));
1500    }
1501    if matches!(
1502        device.device_type(),
1503        DeviceType::KeepKey | DeviceType::Trezor
1504    ) && device.info().await.ok().and_then(|info| info.initialized) == Some(true)
1505    {
1506        return HwiResponse::Error(HwiError::new(
1507            HwiErrorCode::DeviceFailure,
1508            "Device already initialized. Call device.wipe() and try again.",
1509        ));
1510    }
1511    if !matches!(
1512        device.device_type(),
1513        DeviceType::BitBox02 | DeviceType::KeepKey | DeviceType::Trezor
1514    ) {
1515        return HwiResponse::Error(HwiError::new(
1516            HwiErrorCode::UnsupportedCommand,
1517            hwi_unavailable_action_message(
1518                device.device_type(),
1519                &HwiUnsupportedDeviceAction::Restore {
1520                    interactive,
1521                    word_count,
1522                    label,
1523                },
1524            ),
1525        ));
1526    }
1527    let context = match device.device_type() {
1528        #[cfg(feature = "bitbox")]
1529        DeviceType::BitBox02 => {
1530            if device.info().await.ok().and_then(|info| info.initialized) == Some(true) {
1531                return HwiResponse::Error(HwiError::new(
1532                    HwiErrorCode::UnsupportedCommand,
1533                    "The BitBox02 must be wiped before setup.",
1534                ));
1535            }
1536            match bitbox_restore_context() {
1537                Ok(context) => Some(context),
1538                Err(err) => {
1539                    return HwiResponse::Error(HwiError::new(
1540                        HwiErrorCode::DeviceFailure,
1541                        err.to_string(),
1542                    ));
1543                }
1544            }
1545        }
1546        #[cfg(feature = "keepkey")]
1547        DeviceType::KeepKey => match keepkey_restore_context() {
1548            Ok(context) => Some(context),
1549            Err(err) => {
1550                return HwiResponse::Error(HwiError::new(
1551                    HwiErrorCode::DeviceFailure,
1552                    err.to_string(),
1553                ));
1554            }
1555        },
1556        #[cfg(feature = "trezor")]
1557        DeviceType::Trezor => match trezor_restore_context() {
1558            Ok(context) => Some(context),
1559            Err(err) => {
1560                return HwiResponse::Error(HwiError::new(
1561                    HwiErrorCode::DeviceFailure,
1562                    err.to_string(),
1563                ));
1564            }
1565        },
1566        #[allow(unreachable_patterns)]
1567        device_type => {
1568            return HwiResponse::Error(HwiError::new(
1569                HwiErrorCode::UnsupportedCommand,
1570                format!("{device_type} support is not compiled into this build"),
1571            ));
1572        }
1573    };
1574    match device
1575        .device()
1576        .restore_device(RestoreOptions { label, word_count }, context)
1577        .await
1578    {
1579        Ok(success) => HwiResponse::Success(HwiSuccessResponse { success }),
1580        Err(err) => HwiResponse::Error(classify_device_error(&err)),
1581    }
1582}
1583
1584async fn toggle_passphrase_device(selector: HwiSelector) -> HwiResponse {
1585    let (_manager, mut device) = match find_hwi_device(&selector).await {
1586        Ok(found) => found,
1587        Err(err) => return HwiResponse::Error(err),
1588    };
1589    let needs_pin_sent = device.device_type() == DeviceType::KeepKey
1590        && device
1591            .info()
1592            .await
1593            .ok()
1594            .and_then(|info| info.needs_pin_sent)
1595            == Some(true);
1596
1597    if !matches!(
1598        device.device_type(),
1599        DeviceType::BitBox02 | DeviceType::KeepKey | DeviceType::Trezor
1600    ) {
1601        return HwiResponse::Error(HwiError::new(
1602            HwiErrorCode::UnsupportedCommand,
1603            hwi_unavailable_action_message(
1604                device.device_type(),
1605                &HwiUnsupportedDeviceAction::TogglePassphrase,
1606            ),
1607        ));
1608    }
1609    if device.device_type() == DeviceType::BitBox02
1610        && device.info().await.ok().and_then(|info| info.initialized) == Some(false)
1611    {
1612        return HwiResponse::Error(HwiError::new(
1613            HwiErrorCode::DeviceNotInitialized,
1614            "The BitBox02 must be initialized first.",
1615        ));
1616    }
1617
1618    match device.device().toggle_passphrase().await {
1619        Ok(success) => {
1620            if success && needs_pin_sent {
1621                eprintln!("Confirm the action by entering your PIN");
1622                eprintln!("{SEND_PIN_INSTRUCTION}");
1623                eprintln!("{PIN_MATRIX_DESCRIPTION}");
1624            }
1625            HwiResponse::Success(HwiSuccessResponse { success })
1626        }
1627        Err(err) => HwiResponse::Error(classify_device_error(&err)),
1628    }
1629}
1630
1631/// Instructions for mapping scrambled device PIN positions to the host numeric keypad.
1632pub const PIN_MATRIX_DESCRIPTION: &str =
1633    "Use the numeric keypad to describe number positions. The layout is:
1634    7 8 9
1635    4 5 6
1636    1 2 3";
1637/// Instructions for submitting PIN keypad positions through the `sendpin` command.
1638pub const SEND_PIN_INSTRUCTION: &str = "Use 'sendpin' to provide the number positions for the PIN as displayed on your device's screen";
1639
1640async fn device_for_pin_command(
1641    selector: HwiSelector,
1642    action: HwiUnsupportedDeviceAction,
1643    contact_device: bool,
1644) -> Result<crate::Device, HwiError> {
1645    if selector.device_type.is_none() && selector.fingerprint.is_none() {
1646        return Err(HwiError::new(
1647            HwiErrorCode::NoDeviceType,
1648            "You must specify a device type or fingerprint for all commands except enumerate",
1649        ));
1650    }
1651    // Reading a fingerprint means unlocking first, which is what the PIN is for.
1652    if !contact_device && selector.fingerprint.is_some() {
1653        return Err(HwiError::new(
1654            HwiErrorCode::BadArgument,
1655            "A locked device cannot be matched by fingerprint; use --device-type or --device-path",
1656        ));
1657    }
1658
1659    let manager = hwi_device_manager(&selector)?;
1660    let found = if contact_device {
1661        manager.get_device_with_fingerprint().await
1662    } else {
1663        manager.get_device_without_contacting().await
1664    };
1665    let device = match found {
1666        Ok(Some(device)) => device,
1667        Ok(None) => {
1668            return Err(HwiError::new(
1669                HwiErrorCode::DeviceConnectionError,
1670                "Could not find device with specified fingerprint or type",
1671            ));
1672        }
1673        Err(err) => {
1674            return Err(classify_select_error(
1675                selector
1676                    .device_type
1677                    .as_deref()
1678                    .and_then(|raw| parse_device_type(raw).ok()),
1679                &err,
1680            ));
1681        }
1682    };
1683    if contact_device {
1684        manager
1685            .refuse_password(device.device_type())
1686            .map_err(|err| classify_device_error(&err))?;
1687    }
1688
1689    if !matches!(
1690        device.device_type(),
1691        DeviceType::KeepKey | DeviceType::Trezor
1692    ) {
1693        return Err(HwiError::new(
1694            HwiErrorCode::UnsupportedCommand,
1695            hwi_unavailable_action_message(device.device_type(), &action),
1696        ));
1697    }
1698    Ok(device)
1699}
1700
1701/// Only a Trezor or KeepKey waiting for a host-entered PIN is locked without a
1702/// device code; a locked Ledger or Jade keeps upstream's -3.
1703fn locked_device_error(err: &(dyn std::error::Error + 'static)) -> Option<HwiError> {
1704    let error = common_device_error(err)?;
1705    (error.kind() == bhwi::common::ErrorKind::Locked && error.device_code().is_none())
1706        .then(|| HwiError::new(HwiErrorCode::DeviceNotReady, hwi_message(error)))
1707}
1708
1709/// A device waiting for its PIN reports being locked rather than failing to connect.
1710fn device_error(err: &(dyn std::error::Error + 'static)) -> HwiError {
1711    locked_device_error(err)
1712        .unwrap_or_else(|| HwiError::new(HwiErrorCode::DeviceConnectionError, hwi_error_text(err)))
1713}
1714
1715/// Reports the bare device message rather than the wrapped transport error.
1716fn pin_error(err: &(dyn std::error::Error + 'static)) -> HwiError {
1717    let message = hwi_error_text(err);
1718    if let Some(error) = common_device_error(err)
1719        && error.kind() == bhwi::common::ErrorKind::AlreadyUnlocked
1720    {
1721        return HwiError::new(HwiErrorCode::DeviceAlreadyUnlocked, hwi_message(error));
1722    }
1723    if let Some(error) = common_device_error(err)
1724        && matches!(
1725            error.device_code(),
1726            Some(bhwi::common::DeviceCode::Trezor(7) | bhwi::common::DeviceCode::KeepKey(7))
1727        )
1728    {
1729        return HwiError::new(HwiErrorCode::BadArgument, hwi_message(error));
1730    }
1731    HwiError::new(HwiErrorCode::DeviceConnectionError, message)
1732}
1733
1734fn send_pin_error_response(err: &(dyn std::error::Error + 'static)) -> HwiResponse {
1735    let mut source = Some(err);
1736    while let Some(current) = source {
1737        #[cfg(feature = "trezor")]
1738        let action_cancelled = matches!(
1739            current.downcast_ref::<bhwi::trezor::TrezorError>(),
1740            Some(bhwi::trezor::TrezorError::ActionCancelled(..))
1741        );
1742        #[cfg(not(feature = "trezor"))]
1743        let action_cancelled = false;
1744        // The common KeepKey adapter converts the same protocol error before
1745        // the async HWI boundary sees it.
1746        let converted_action_cancelled =
1747            current
1748                .downcast_ref::<bhwi::common::Error>()
1749                .is_some_and(|error| match error.kind() {
1750                    bhwi::common::ErrorKind::AuthenticationRefused => true,
1751                    bhwi::common::ErrorKind::UserCancelled => is_trezor_family_code(error),
1752                    _ => false,
1753                });
1754        if action_cancelled || converted_action_cancelled {
1755            return HwiResponse::Success(HwiSuccessResponse { success: false });
1756        }
1757        source = current.source();
1758    }
1759    HwiResponse::Error(pin_error(err))
1760}
1761
1762async fn prompt_pin_device(selector: HwiSelector) -> HwiResponse {
1763    let mut device =
1764        match device_for_pin_command(selector, HwiUnsupportedDeviceAction::PromptPin, true).await {
1765            Ok(device) => device,
1766            Err(error) => return HwiResponse::Error(error),
1767        };
1768
1769    eprintln!("{SEND_PIN_INSTRUCTION}");
1770    eprintln!("{PIN_MATRIX_DESCRIPTION}");
1771
1772    match device.device().prompt_pin().await {
1773        Ok(success) => HwiResponse::Success(HwiSuccessResponse { success }),
1774        Err(err) => HwiResponse::Error(pin_error(&err)),
1775    }
1776}
1777
1778#[cfg(not(any(feature = "trezor", feature = "keepkey")))]
1779async fn send_pin_device(_selector: HwiSelector, _pin: String) -> HwiResponse {
1780    HwiResponse::Error(HwiError::new(
1781        HwiErrorCode::UnsupportedCommand,
1782        "no host-PIN device support is compiled into this build",
1783    ))
1784}
1785
1786#[cfg(any(feature = "trezor", feature = "keepkey"))]
1787async fn send_pin_device(selector: HwiSelector, pin: String) -> HwiResponse {
1788    let mut device = match device_for_pin_command(
1789        selector,
1790        HwiUnsupportedDeviceAction::SendPin { pin: String::new() },
1791        false,
1792    )
1793    .await
1794    {
1795        Ok(device) => device,
1796        Err(error) => return HwiResponse::Error(error),
1797    };
1798
1799    let context = match device.device_type() {
1800        #[cfg(feature = "keepkey")]
1801        DeviceType::KeepKey => bhwi_async::management::keepkey_pin_context_from_positions(pin),
1802        #[cfg(feature = "trezor")]
1803        DeviceType::Trezor => bhwi_async::management::trezor_pin_context_from_positions(pin),
1804        #[allow(unreachable_patterns)]
1805        device_type => {
1806            let _ = pin;
1807            return HwiResponse::Error(HwiError::new(
1808                HwiErrorCode::UnsupportedCommand,
1809                format!("{device_type} support is not compiled into this build"),
1810            ));
1811        }
1812    };
1813    let context = match context {
1814        Ok(context) => context,
1815        Err(err) => {
1816            return HwiResponse::Error(HwiError::new(HwiErrorCode::BadArgument, err.to_string()));
1817        }
1818    };
1819    send_pin_response(device.device().send_pin(Some(context)).await)
1820}
1821
1822fn send_pin_response(result: Result<bool, bhwi_async::HWIDeviceError>) -> HwiResponse {
1823    match result {
1824        Ok(success) => HwiResponse::Success(HwiSuccessResponse { success }),
1825        // Upstream's `send_pin` returns False for any `Failure` the PIN draws,
1826        // after ruling out an already unlocked device.
1827        Err(err) if common_device_error(&err).is_some_and(is_trezor_family_code) => {
1828            HwiResponse::Success(HwiSuccessResponse { success: false })
1829        }
1830        Err(err) => send_pin_error_response(&err),
1831    }
1832}
1833
1834async fn backup_device(
1835    selector: HwiSelector,
1836    label: String,
1837    backup_passphrase: String,
1838) -> HwiResponse {
1839    let (_manager, mut device) = match find_hwi_device(&selector).await {
1840        Ok(found) => found,
1841        Err(err) => return HwiResponse::Error(err),
1842    };
1843
1844    let device_type = device.device_type();
1845    match device_type {
1846        DeviceType::BitBox02 => {
1847            if !label.is_empty() || !backup_passphrase.is_empty() {
1848                return HwiResponse::Error(HwiError::new(
1849                    HwiErrorCode::UnsupportedCommand,
1850                    "Label/passphrase not needed when exporting mnemonic from the BitBox02.",
1851                ));
1852            }
1853        }
1854        DeviceType::Coldcard => {}
1855        DeviceType::KeepKey
1856        | DeviceType::Ledger
1857        | DeviceType::Jade
1858        | DeviceType::Specter
1859        | DeviceType::Trezor => {
1860            let unsupported = HwiUnsupportedDeviceAction::Backup {
1861                label,
1862                backup_passphrase,
1863            };
1864            return HwiResponse::Error(HwiError::new(
1865                HwiErrorCode::UnsupportedCommand,
1866                hwi_unavailable_action_message(device_type, &unsupported),
1867            ));
1868        }
1869    }
1870
1871    let approval =
1872        coldcard_emulator_approval(coldcard_emulator_path(&device), ColdcardApproval::Backup);
1873    let (backup, approval) = tokio::join!(device.device().backup_device(), approval);
1874    if let Err(err) = approval {
1875        return HwiResponse::Error(HwiError::new(HwiErrorCode::DeviceConnectionError, err));
1876    }
1877    match backup {
1878        Ok(DeviceBackup::Complete) => HwiResponse::Success(HwiSuccessResponse { success: true }),
1879        Ok(DeviceBackup::File(bytes)) => match write_hwi_backup_file(&bytes) {
1880            Ok(()) => HwiResponse::Success(HwiSuccessResponse { success: true }),
1881            Err(err) => HwiResponse::Error(HwiError::new(
1882                HwiErrorCode::DeviceFailure,
1883                format!("backup failed: {err}"),
1884            )),
1885        },
1886        Err(err) => HwiResponse::Error(classify_device_error(&err)),
1887    }
1888}
1889
1890fn write_hwi_backup_file(bytes: &[u8]) -> io::Result<()> {
1891    fs::write(hwi_backup_filename(), bytes)
1892}
1893
1894fn hwi_backup_filename() -> String {
1895    format_hwi_backup_filename(Local::now())
1896}
1897
1898fn format_hwi_backup_filename<Tz: chrono::TimeZone>(time: chrono::DateTime<Tz>) -> String {
1899    format!(
1900        "backup-{:04}{:02}{:02}-{:02}{:02}.7z",
1901        time.year(),
1902        time.month(),
1903        time.day(),
1904        time.hour(),
1905        time.minute()
1906    )
1907}
1908
1909async fn get_master_xpub(
1910    selector: HwiSelector,
1911    addr_type: HwiAddressType,
1912    account: u32,
1913) -> HwiResponse {
1914    let path = match master_xpub_path(addr_type, selector.network, account) {
1915        Ok(path) => path,
1916        Err(err) => {
1917            return HwiResponse::Error(HwiError::new(HwiErrorCode::BadArgument, err.to_string()));
1918        }
1919    };
1920    get_xpub(selector, format!("m/{path}"), false).await
1921}
1922
1923async fn sign_tx(selector: HwiSelector, psbt: String) -> HwiResponse {
1924    let network = selector.network;
1925    let (_manager, mut device) = match find_hwi_device(&selector).await {
1926        Ok(found) => found,
1927        Err(err) => return HwiResponse::Error(err),
1928    };
1929
1930    // Upstream parses the PSBT only once a client exists, then reports an
1931    // invalid transaction rather than a generic bad argument.
1932    let parsed = match Psbt::from_str(psbt.trim()) {
1933        Ok(psbt) => psbt,
1934        Err(err) => {
1935            return HwiResponse::Error(HwiError::new(HwiErrorCode::InvalidTx, err.to_string()));
1936        }
1937    };
1938
1939    let original = parsed.to_string();
1940    #[cfg(feature = "ledger")]
1941    if device.device_type() == DeviceType::Ledger {
1942        let contexts = match ledger_signing_contexts(&mut device, &parsed, network).await {
1943            Ok(contexts) if contexts.is_empty() => {
1944                return HwiResponse::SignTx(HwiSignTxResponse {
1945                    psbt: original,
1946                    signed: false,
1947                });
1948            }
1949            Ok(contexts) => contexts,
1950            Err(LedgerSigningError::BadArgument(err)) => {
1951                return HwiResponse::Error(HwiError::new(HwiErrorCode::BadArgument, err));
1952            }
1953            Err(LedgerSigningError::Device(err)) => {
1954                return HwiResponse::Error(classify_device_error_for(DeviceType::Ledger, &err));
1955            }
1956            Err(err @ (LedgerSigningError::MissingHmac | LedgerSigningError::HmacLength(_))) => {
1957                return HwiResponse::Error(HwiError::new(
1958                    HwiErrorCode::DeviceConnectionError,
1959                    err.to_string(),
1960                ));
1961            }
1962        };
1963
1964        let mut signed_psbt = parsed;
1965        for signing in contexts {
1966            let mut signing_psbt = signed_psbt.clone();
1967            if signing.address_type == LedgerAddressType::Legacy {
1968                strip_legacy_witness_utxos(&mut signing_psbt);
1969            }
1970            let result = match device
1971                .device()
1972                .sign_tx(signing_psbt, Some(signing.context))
1973                .await
1974            {
1975                Ok(psbt) => psbt,
1976                Err(err) => {
1977                    return HwiResponse::Error(classify_device_error_for(DeviceType::Ledger, &err));
1978                }
1979            };
1980            merge_psbt_signatures(&mut signed_psbt, result);
1981        }
1982        let signed = signed_psbt.to_string();
1983        return HwiResponse::SignTx(HwiSignTxResponse {
1984            signed: signed != original,
1985            psbt: signed,
1986        });
1987    }
1988
1989    let device_type = device.device_type();
1990    match device.device().sign_tx(parsed, None).await {
1991        Ok(signed_psbt) => {
1992            let signed = signed_psbt.to_string();
1993            HwiResponse::SignTx(HwiSignTxResponse {
1994                signed: signed != original,
1995                psbt: signed,
1996            })
1997        }
1998        Err(err) => HwiResponse::Error(classify_device_error_for(device_type, &err)),
1999    }
2000}
2001
2002async fn sign_message(selector: HwiSelector, message: String, path: String) -> HwiResponse {
2003    let (_manager, mut device) = match find_hwi_device(&selector).await {
2004        Ok(found) => found,
2005        Err(err) => return HwiResponse::Error(err),
2006    };
2007
2008    // Argument validation happens after device lookup, like upstream.
2009    let path = match DerivationPath::from_str(&path) {
2010        Ok(path) => path,
2011        Err(err) => {
2012            return HwiResponse::Error(HwiError::new(HwiErrorCode::BadArgument, err.to_string()));
2013        }
2014    };
2015
2016    let device_type = device.device_type();
2017    let approval = coldcard_emulator_approval(
2018        coldcard_emulator_path(&device),
2019        coldcard_emulator_action(ColdcardApproval::Once),
2020    );
2021    let (signature, approval) = tokio::join!(
2022        device.device().sign_message(message.as_bytes(), path),
2023        approval
2024    );
2025    if let Err(err) = approval {
2026        return HwiResponse::Error(HwiError::new(HwiErrorCode::DeviceConnectionError, err));
2027    }
2028    match signature {
2029        Ok((header, signature)) => HwiResponse::SignMessage(HwiSignMessageResponse {
2030            signature: BASE64_STANDARD.encode(message_signature(device_type, header, &signature)),
2031        }),
2032        Err(err) => HwiResponse::Error(classify_device_error_for(device_type, &err)),
2033    }
2034}
2035
2036async fn display_address(selector: HwiSelector, request: HwiDisplayAddressRequest) -> HwiResponse {
2037    if matches!(
2038        &request,
2039        HwiDisplayAddressRequest::Path {
2040            addr_type: HwiAddressType::Tap,
2041            ..
2042        }
2043    ) && selector.device_type.as_deref().is_some_and(|raw| {
2044        raw.eq_ignore_ascii_case("keepkey") || raw.eq_ignore_ascii_case("keepkey_simulator")
2045    }) {
2046        return HwiResponse::Error(HwiError::new(
2047            HwiErrorCode::UnsupportedCommand,
2048            "This device does not support displaying Taproot addresses",
2049        ));
2050    }
2051
2052    let (_manager, mut device) = match find_hwi_device(&selector).await {
2053        Ok(found) => found,
2054        Err(err) => return HwiResponse::Error(err),
2055    };
2056
2057    let display = match request {
2058        HwiDisplayAddressRequest::Path { path, addr_type } => {
2059            let path = match DerivationPath::from_str(&path) {
2060                Ok(path) => path,
2061                Err(err) => {
2062                    return HwiResponse::Error(HwiError::new(
2063                        HwiErrorCode::BadArgument,
2064                        err.to_string(),
2065                    ));
2066                }
2067            };
2068            if device.device_type() == DeviceType::Coldcard && addr_type == HwiAddressType::Tap {
2069                return HwiResponse::Error(HwiError::new(
2070                    HwiErrorCode::UnsupportedCommand,
2071                    "Coldcard does not support displaying Taproot addresses yet",
2072                ));
2073            }
2074            if device.device_type() == DeviceType::Jade && addr_type == HwiAddressType::Tap {
2075                return HwiResponse::Error(HwiError::new(HwiErrorCode::DeviceFailure, "tap"));
2076            }
2077            if device.device_type() == DeviceType::Ledger
2078                && !is_standard_path(&path, addr_type, selector.network)
2079            {
2080                return HwiResponse::Error(HwiError::new(
2081                    HwiErrorCode::BadArgument,
2082                    "Ledger requires BIP 44 standard paths",
2083                ));
2084            }
2085            Ok((
2086                DisplayAddress::ByPath {
2087                    path,
2088                    display: true,
2089                    address_format: Some(address_type_for(addr_type)),
2090                },
2091                None,
2092            ))
2093        }
2094        HwiDisplayAddressRequest::Descriptor { descriptor } => {
2095            match singlesig_display_address_from_descriptor(&mut device, &descriptor)
2096                .await
2097                .map_err(display_address_error)
2098            {
2099                Ok(address) => Ok((address, None)),
2100                Err(single_sig_error) => {
2101                    if device.device_type() == DeviceType::Ledger {
2102                        #[cfg(not(feature = "ledger"))]
2103                        {
2104                            return HwiResponse::Error(single_sig_error);
2105                        }
2106                        #[cfg(feature = "ledger")]
2107                        if multisig_display_address_from_descriptor(&descriptor).is_err() {
2108                            return HwiResponse::Error(single_sig_error);
2109                        }
2110                        #[cfg(feature = "ledger")]
2111                        match ledger_multisig_display_address(&mut device, &descriptor).await {
2112                            Ok((address, context)) => Ok((address, Some(context))),
2113                            Err(LedgerSigningError::BadArgument(err)) => {
2114                                return HwiResponse::Error(HwiError::new(
2115                                    HwiErrorCode::BadArgument,
2116                                    err,
2117                                ));
2118                            }
2119                            Err(LedgerSigningError::Device(err)) => {
2120                                return HwiResponse::Error(classify_device_error_for(
2121                                    DeviceType::Ledger,
2122                                    &err,
2123                                ));
2124                            }
2125                            Err(err) => {
2126                                return HwiResponse::Error(HwiError::new(
2127                                    HwiErrorCode::DeviceConnectionError,
2128                                    err.to_string(),
2129                                ));
2130                            }
2131                        }
2132                    } else if matches!(
2133                        device.device_type(),
2134                        DeviceType::Coldcard
2135                            | DeviceType::Jade
2136                            | DeviceType::KeepKey
2137                            | DeviceType::Trezor
2138                    ) {
2139                        match multisig_display_address_from_descriptor(&descriptor) {
2140                            Ok(address) => Ok((DisplayAddress::ByMultisig(address), None)),
2141                            Err(_) => return HwiResponse::Error(single_sig_error),
2142                        }
2143                    } else {
2144                        return HwiResponse::Error(single_sig_error);
2145                    }
2146                }
2147            }
2148        }
2149    };
2150
2151    let (display, context) = match display {
2152        Ok(display) => display,
2153        Err(error) => return HwiResponse::Error(error),
2154    };
2155
2156    let probes_multisig_paths = matches!(display, DisplayAddress::ByMultisig(_))
2157        && matches!(
2158            device.device_type(),
2159            DeviceType::KeepKey | DeviceType::Trezor
2160        );
2161    let approval = coldcard_emulator_approval(
2162        coldcard_emulator_path(&device),
2163        coldcard_emulator_action(ColdcardApproval::Once),
2164    );
2165    let (address, approval) =
2166        tokio::join!(device.device().display_address(display, context), approval);
2167    if let Err(err) = approval {
2168        return HwiResponse::Error(HwiError::new(HwiErrorCode::DeviceConnectionError, err));
2169    }
2170    match address {
2171        Ok(address) => HwiResponse::DisplayAddress(HwiDisplayAddressResponse { address }),
2172        Err(err) => HwiResponse::Error(multisig_probe_error(
2173            probes_multisig_paths,
2174            device.device_type(),
2175            &err,
2176        )),
2177    }
2178}
2179
2180/// Upstream swallows every device failure while probing multisig paths and
2181/// reports one bad argument; a refusal, a lock or a host-side error keeps its code.
2182fn multisig_probe_error(
2183    probes_paths: bool,
2184    device_type: DeviceType,
2185    err: &(dyn std::error::Error + 'static),
2186) -> HwiError {
2187    use bhwi::common::{DeviceCode, ErrorKind};
2188    const NO_PATH: &str = "No path supplied matched device keys";
2189    let failed_while_probing = common_device_error(err).is_some_and(|error| {
2190        matches!(
2191            error.device_code(),
2192            Some(DeviceCode::Trezor(_) | DeviceCode::KeepKey(_))
2193        ) && !matches!(
2194            error.kind(),
2195            ErrorKind::UserCancelled | ErrorKind::AuthenticationRefused | ErrorKind::Locked
2196        )
2197    });
2198    if probes_paths && failed_while_probing {
2199        return HwiError::new(HwiErrorCode::BadArgument, NO_PATH);
2200    }
2201    classify_device_error_for(device_type, err)
2202}
2203
2204#[derive(Clone, Copy)]
2205enum ColdcardApproval {
2206    Once,
2207    Backup,
2208    Refuse,
2209}
2210
2211/// Undocumented test hook: `HWI_COLDCARD_EMULATOR_REFUSE` swaps the built-in
2212/// emulator auto-approval for a refusal so cancel parity cases can exercise
2213/// the device's `refu` path through the CLI.
2214fn coldcard_emulator_action(default: ColdcardApproval) -> ColdcardApproval {
2215    if std::env::var_os("HWI_COLDCARD_EMULATOR_REFUSE").is_some() {
2216        ColdcardApproval::Refuse
2217    } else {
2218        default
2219    }
2220}
2221
2222fn coldcard_emulator_path(device: &Device) -> Option<String> {
2223    (device.device_type() == DeviceType::Coldcard && device.is_emulated())
2224        .then(|| device.path().to_string())
2225}
2226
2227async fn coldcard_emulator_approval(
2228    socket_path: Option<String>,
2229    approval: ColdcardApproval,
2230) -> Result<(), String> {
2231    match socket_path {
2232        Some(socket_path) => coldcard_emulator_keypresses(&socket_path, approval).await,
2233        None => Ok(()),
2234    }
2235}
2236
2237#[cfg(unix)]
2238async fn coldcard_emulator_keypresses(
2239    socket_path: &str,
2240    approval: ColdcardApproval,
2241) -> Result<(), String> {
2242    use tokio::net::UnixDatagram;
2243
2244    let client_path = format!(
2245        "/tmp/bhwi-hwi-approval-{}-{}.sock",
2246        std::process::id(),
2247        generate_hwi_socket_id()
2248    );
2249    let _ = fs::remove_file(&client_path);
2250    let socket = UnixDatagram::bind(&client_path).map_err(|err| err.to_string())?;
2251    socket.connect(socket_path).map_err(|err| err.to_string())?;
2252    let key = match approval {
2253        ColdcardApproval::Refuse => b'x',
2254        ColdcardApproval::Once | ColdcardApproval::Backup => b'y',
2255    };
2256    send_coldcard_simulator_keypress(&socket, key).await?;
2257    if matches!(approval, ColdcardApproval::Backup) {
2258        for _ in 0..20 {
2259            tokio::time::sleep(std::time::Duration::from_millis(250)).await;
2260            send_coldcard_simulator_keypress(&socket, b'1').await?;
2261        }
2262    }
2263    drop(socket);
2264    let _ = fs::remove_file(client_path);
2265    Ok(())
2266}
2267
2268#[cfg(unix)]
2269async fn send_coldcard_simulator_keypress(
2270    socket: &tokio::net::UnixDatagram,
2271    key: u8,
2272) -> Result<(), String> {
2273    let mut packet = [0u8; 64];
2274    packet[0] = 0x80 | 5;
2275    packet[1..5].copy_from_slice(b"XKEY");
2276    packet[5] = key;
2277    socket.send(&packet).await.map_err(|err| err.to_string())?;
2278    Ok(())
2279}
2280
2281#[cfg(not(unix))]
2282async fn coldcard_emulator_keypresses(
2283    _socket_path: &str,
2284    _approval: ColdcardApproval,
2285) -> Result<(), String> {
2286    Ok(())
2287}
2288
2289fn generate_hwi_socket_id() -> usize {
2290    use std::sync::atomic::{AtomicUsize, Ordering};
2291    static SOCKET_COUNTER: AtomicUsize = AtomicUsize::new(0);
2292    SOCKET_COUNTER.fetch_add(1, Ordering::Relaxed)
2293}
2294
2295async fn get_xpub(selector: HwiSelector, path: String, expert: bool) -> HwiResponse {
2296    let (_manager, mut device) = match find_hwi_device(&selector).await {
2297        Ok(found) => found,
2298        Err(err) => return HwiResponse::Error(err),
2299    };
2300
2301    // Argument validation happens after device lookup, like upstream. The
2302    // BitBox backend's get_pubkey_at_path is undecorated upstream, so its
2303    // invalid-path failure surfaces as -13 instead of -7.
2304    let path = match DerivationPath::from_str(&path) {
2305        Ok(path) => path,
2306        Err(err) => {
2307            let code = if device.device_type() == DeviceType::BitBox02 {
2308                HwiErrorCode::DeviceFailure
2309            } else {
2310                HwiErrorCode::BadArgument
2311            };
2312            return HwiResponse::Error(HwiError::new(code, err.to_string()));
2313        }
2314    };
2315
2316    match device.device().get_extended_pubkey(path, false).await {
2317        Ok(xpub) => HwiResponse::GetXpub(get_xpub_response(xpub, expert)),
2318        Err(err) => HwiResponse::Error(classify_anyhow_device_error(&anyhow::Error::new(err))),
2319    }
2320}
2321
2322async fn get_descriptors(selector: HwiSelector, account: u32) -> HwiResponse {
2323    let (manager, mut device) = match find_hwi_device(&selector).await {
2324        Ok(found) => found,
2325        Err(err) => return HwiResponse::Error(err),
2326    };
2327
2328    let fingerprint = match device.fingerprint().await {
2329        Ok(fingerprint) => fingerprint,
2330        Err(err) => {
2331            return HwiResponse::Error(classify_device_error(&err));
2332        }
2333    };
2334    let device_type = device.device_type();
2335    let model = device.model().to_owned();
2336    let network = manager.selector.network;
2337    let mut response = HwiGetDescriptorsResponse {
2338        receive: Vec::new(),
2339        internal: Vec::new(),
2340    };
2341
2342    for internal in [false, true] {
2343        for addr_type in hwi_descriptor_addr_types(device_type, &model) {
2344            let descriptor_type = descriptor_type_for(addr_type);
2345            let options = GetDescriptorOptions::with_account(
2346                fingerprint,
2347                account,
2348                internal,
2349                descriptor_type,
2350                network,
2351            );
2352            let descriptor = match get_descriptor(device.device().as_mut(), options).await {
2353                Ok(descriptor) => descriptor,
2354                Err(err) => {
2355                    return HwiResponse::Error(classify_device_error(&err));
2356                }
2357            };
2358            let descriptor = match hwi_descriptor_string(&descriptor) {
2359                Ok(descriptor) => descriptor,
2360                Err(err) => {
2361                    return HwiResponse::Error(HwiError::new(
2362                        HwiErrorCode::BadArgument,
2363                        err.to_string(),
2364                    ));
2365                }
2366            };
2367            if internal {
2368                response.internal.push(descriptor);
2369            } else {
2370                response.receive.push(descriptor);
2371            }
2372        }
2373    }
2374
2375    HwiResponse::GetDescriptors(response)
2376}
2377
2378struct HwiGetKeypoolRequest {
2379    start: u32,
2380    end: u32,
2381    internal: bool,
2382    keypool: bool,
2383    account: u32,
2384    addr_type: HwiAddressType,
2385    all: bool,
2386    path: Option<String>,
2387}
2388
2389async fn get_keypool(selector: HwiSelector, request: HwiGetKeypoolRequest) -> HwiResponse {
2390    if selector.device_type.is_none() && selector.fingerprint.is_none() {
2391        return HwiResponse::Error(HwiError::new(
2392            HwiErrorCode::NoDeviceType,
2393            "You must specify a device type or fingerprint for all commands except enumerate",
2394        ));
2395    }
2396    if request.start > request.end {
2397        return HwiResponse::Error(HwiError::new(
2398            HwiErrorCode::BadArgument,
2399            "keypool start index must be less than or equal to end index",
2400        ));
2401    }
2402    if !request.all
2403        && request.addr_type == HwiAddressType::Tap
2404        && selector.device_type.as_deref().is_some_and(|raw| {
2405            raw.eq_ignore_ascii_case("keepkey") || raw.eq_ignore_ascii_case("keepkey_simulator")
2406        })
2407    {
2408        return HwiResponse::Error(HwiError::new(
2409            HwiErrorCode::UnsupportedCommand,
2410            "Device does not support Taproot",
2411        ));
2412    }
2413
2414    let (manager, mut device) = match find_hwi_device(&selector).await {
2415        Ok(found) => found,
2416        Err(err) => return HwiResponse::Error(err),
2417    };
2418
2419    let fingerprint = match device.fingerprint().await {
2420        Ok(fingerprint) => fingerprint,
2421        Err(err) => {
2422            return HwiResponse::Error(classify_device_error(&err));
2423        }
2424    };
2425    let device_type = device.device_type();
2426    let model = device.model().to_owned();
2427    let network = manager.selector.network;
2428    let addr_types = if request.all {
2429        hwi_descriptor_addr_types(device_type, &model)
2430    } else if request.addr_type == HwiAddressType::Tap && !can_sign_taproot(device_type, &model) {
2431        return HwiResponse::Error(HwiError::new(
2432            HwiErrorCode::UnsupportedCommand,
2433            "Device does not support Taproot",
2434        ));
2435    } else {
2436        vec![request.addr_type]
2437    };
2438
2439    let branches = if request.path.is_none() && !request.internal {
2440        vec![false, true]
2441    } else {
2442        vec![request.internal]
2443    };
2444
2445    let mut entries = Vec::new();
2446    for addr_type in addr_types {
2447        for internal in branches.iter().copied() {
2448            let descriptor_type = descriptor_type_for(addr_type);
2449            let options = match request.path.as_deref() {
2450                Some(path) => match keypool_path_descriptor_options(
2451                    fingerprint,
2452                    path,
2453                    internal,
2454                    descriptor_type,
2455                    network,
2456                ) {
2457                    Ok(options) => options,
2458                    Err(error) => return HwiResponse::Error(error),
2459                },
2460                None => GetDescriptorOptions::with_account(
2461                    fingerprint,
2462                    request.account,
2463                    internal,
2464                    descriptor_type,
2465                    network,
2466                ),
2467            };
2468            let descriptor = match get_descriptor(device.device().as_mut(), options).await {
2469                Ok(descriptor) => descriptor,
2470                Err(err) => {
2471                    return HwiResponse::Error(classify_device_error(&err));
2472                }
2473            };
2474            let desc = match hwi_descriptor_string(&descriptor) {
2475                Ok(descriptor) => descriptor,
2476                Err(err) => {
2477                    return HwiResponse::Error(HwiError::new(
2478                        HwiErrorCode::BadArgument,
2479                        err.to_string(),
2480                    ));
2481                }
2482            };
2483            entries.push(HwiGetKeypoolEntry {
2484                desc,
2485                range: [request.start, request.end],
2486                timestamp: "now",
2487                internal,
2488                keypool: request.keypool,
2489                active: request.keypool,
2490                watchonly: true,
2491            });
2492        }
2493    }
2494
2495    HwiResponse::GetKeypool(entries)
2496}
2497
2498fn master_xpub_path(
2499    addr_type: HwiAddressType,
2500    network: Network,
2501    account: u32,
2502) -> Result<DerivationPath, bitcoin::bip32::Error> {
2503    Ok([
2504        ChildNumber::from_hardened_idx(bip44_purpose(addr_type))?,
2505        ChildNumber::from_hardened_idx(bip44_chain(network))?,
2506        ChildNumber::from_hardened_idx(account)?,
2507    ]
2508    .as_ref()
2509    .into())
2510}
2511
2512fn bip44_purpose(addr_type: HwiAddressType) -> u32 {
2513    match addr_type {
2514        HwiAddressType::Legacy => 44,
2515        HwiAddressType::ShWit => 49,
2516        HwiAddressType::Wit => 84,
2517        HwiAddressType::Tap => 86,
2518    }
2519}
2520
2521fn bip44_chain(network: Network) -> u32 {
2522    if network == Network::Bitcoin { 0 } else { 1 }
2523}
2524
2525/// Upstream's `is_standard_path`, which its Ledger backend checks before asking the device.
2526fn is_standard_path(path: &DerivationPath, addr_type: HwiAddressType, network: Network) -> bool {
2527    let [purpose, coin, account, change, index] = path.as_ref() else {
2528        return false;
2529    };
2530    account.is_hardened()
2531        && index.is_normal()
2532        && *purpose
2533            == ChildNumber::Hardened {
2534                index: bip44_purpose(addr_type),
2535            }
2536        && *coin
2537            == ChildNumber::Hardened {
2538                index: bip44_chain(network),
2539            }
2540        && matches!(change, ChildNumber::Normal { index: 0 | 1 })
2541}
2542
2543fn descriptor_type_for(addr_type: HwiAddressType) -> DescriptorType {
2544    match addr_type {
2545        HwiAddressType::Legacy => DescriptorType::Pkh,
2546        HwiAddressType::ShWit => DescriptorType::ShWpkh,
2547        HwiAddressType::Wit => DescriptorType::Wpkh,
2548        HwiAddressType::Tap => DescriptorType::Tr,
2549    }
2550}
2551
2552fn address_type_for(addr_type: HwiAddressType) -> bitcoin::address::AddressType {
2553    match addr_type {
2554        HwiAddressType::Legacy => bitcoin::address::AddressType::P2pkh,
2555        HwiAddressType::ShWit => bitcoin::address::AddressType::P2sh,
2556        HwiAddressType::Wit => bitcoin::address::AddressType::P2wpkh,
2557        HwiAddressType::Tap => bitcoin::address::AddressType::P2tr,
2558    }
2559}
2560
2561fn display_address_error(err: DisplayAddressError) -> HwiError {
2562    match err {
2563        DisplayAddressError::Device(err) => classify_device_error(&err),
2564        err => HwiError::new(HwiErrorCode::BadArgument, err.to_string()),
2565    }
2566}
2567
2568/// Maps typed device errors from the async stack onto Python HWI's error
2569/// codes. Walks the `source()` chain for a `bhwi::common::Error` so cancels
2570/// and argument refusals stop flattening to `-3`; unclassified failures keep
2571/// the full message with `-3`.
2572fn common_device_error<'a>(
2573    err: &'a (dyn std::error::Error + 'static),
2574) -> Option<&'a bhwi::common::Error> {
2575    let mut source = Some(err);
2576    while let Some(current) = source {
2577        if let Some(error) = current.downcast_ref::<bhwi::common::Error>() {
2578            return Some(error);
2579        }
2580        source = current.source();
2581    }
2582    None
2583}
2584
2585/// Upstream HWI's own tests compare `error` text, so the kind is not printed.
2586fn hwi_message(error: &bhwi::common::Error) -> String {
2587    if !error.message().is_empty() {
2588        return error.message().to_owned();
2589    }
2590    match error.kind() {
2591        bhwi::common::ErrorKind::UserCancelled => "action canceled by the user".to_owned(),
2592        bhwi::common::ErrorKind::AuthenticationRefused => "authentication refused".to_owned(),
2593        kind => format!("{kind:?}"),
2594    }
2595}
2596
2597fn hwi_error_text(err: &(dyn std::error::Error + 'static)) -> String {
2598    let text = err.to_string();
2599    match common_device_error(err) {
2600        Some(error) => text.replace(&error.to_string(), &hwi_message(error)),
2601        None => text,
2602    }
2603}
2604
2605fn classify_anyhow_device_error(err: &anyhow::Error) -> HwiError {
2606    let source: &(dyn std::error::Error + 'static) = err.as_ref();
2607    classify_device_error(source)
2608}
2609
2610/// Upstream `ledger_bitcoin`'s `DeviceException.exc`, less 0xE000, which is not an error.
2611const LEDGER_BITCOIN_STATUS_WORDS: [u16; 11] = [
2612    0x6985, 0x6982, 0x6A80, 0x6A82, 0x6A86, 0x6A87, 0x6D00, 0x6E00, 0xB000, 0xB007, 0xB008,
2613];
2614
2615fn classify_device_error(err: &(dyn std::error::Error + 'static)) -> HwiError {
2616    if let Some(error) = locked_device_error(err) {
2617        return error;
2618    }
2619
2620    if let Some(error) = common_device_error(err) {
2621        use bhwi::common::ErrorKind;
2622        // Upstream `bitbox02_exception`: 101 and 103 raise UnavailableActionError,
2623        // any other device code is re-raised as an unknown error. Upstream's `init()`
2624        // rejects an uninitialized device first, which here answers 105.
2625        if let Some(bhwi::common::DeviceCode::BitBox(code)) = error.device_code()
2626            && error.kind() != ErrorKind::UserCancelled
2627        {
2628            let code = match code {
2629                105 => HwiErrorCode::DeviceNotInitialized,
2630                101 | 103 => HwiErrorCode::UnsupportedCommand,
2631                _ => HwiErrorCode::DeviceFailure,
2632            };
2633            return HwiError::new(code, hwi_message(error));
2634        }
2635        // Upstream `jade_exception`, code for code.
2636        if let Some(bhwi::common::DeviceCode::Jade(code)) = error.device_code()
2637            && error.kind() != ErrorKind::UserCancelled
2638        {
2639            let code = match code {
2640                -32602 => HwiErrorCode::BadArgument,
2641                -32002 | -32003 | -32600 | -32601 | -32001 => HwiErrorCode::DeviceConnectionError,
2642                _ => HwiErrorCode::DeviceFailure,
2643            };
2644            return HwiError::new(code, hwi_message(error));
2645        }
2646        // Upstream's `ledger_bitcoin` raises these from inside a command, past
2647        // `ledger_exception`, as unknown errors. Opening the app has no upstream
2648        // counterpart and a refusal keeps its own path; every other code keeps -3.
2649        if let Some(bhwi::common::DeviceCode::Ledger(status)) = error.device_code()
2650            && !matches!(error.kind(), ErrorKind::UserCancelled | ErrorKind::NotReady)
2651        {
2652            return if LEDGER_BITCOIN_STATUS_WORDS.contains(&status) {
2653                HwiError::new(HwiErrorCode::DeviceFailure, hwi_message(error))
2654            } else {
2655                device_error(err)
2656            };
2657        }
2658        let code = match error.kind() {
2659            ErrorKind::AuthenticationRefused | ErrorKind::UserCancelled => {
2660                HwiErrorCode::ActionCanceled
2661            }
2662            ErrorKind::Unsupported
2663            | ErrorKind::MissingContext
2664            | ErrorKind::UnsupportedDisplayAddress => HwiErrorCode::UnsupportedCommand,
2665            // Device-refused input keeps upstream's bad-argument class,
2666            // e.g. `Coldcard Error: ...` for an unknown multisig wallet.
2667            ErrorKind::InvalidInput | ErrorKind::Rejected | ErrorKind::WrongNetwork => {
2668                HwiErrorCode::BadArgument
2669            }
2670            ErrorKind::AlreadyInitialized => HwiErrorCode::DeviceAlreadyInitialized,
2671            // Upstream's `trezor_exception` lets any other device failure through as unknown.
2672            kind if is_trezor_family_code(error) && kind != ErrorKind::Locked => {
2673                HwiErrorCode::DeviceFailure
2674            }
2675            _ => return device_error(err),
2676        };
2677        return HwiError::new(code, hwi_message(error));
2678    }
2679    device_error(err)
2680}
2681
2682fn is_trezor_family_code(error: &bhwi::common::Error) -> bool {
2683    matches!(
2684        error.device_code(),
2685        Some(bhwi::common::DeviceCode::Trezor(_) | bhwi::common::DeviceCode::KeepKey(_))
2686    )
2687}
2688
2689/// Device-aware classification. Upstream's Ledger backend uses the new
2690/// `ledger_bitcoin` client whose `DenyError` bypasses the `ledger_exception`
2691/// cancel mapping, so Ledger cancellations surface as `-13` there instead of
2692/// `-14`. Mirror that for parity; every other device keeps `-14`.
2693fn classify_device_error_for(
2694    device_type: DeviceType,
2695    err: &(dyn std::error::Error + 'static),
2696) -> HwiError {
2697    let classified = classify_device_error(err);
2698    if device_type == DeviceType::Ledger && classified.code == HwiErrorCode::ActionCanceled.code() {
2699        return HwiError::new(HwiErrorCode::DeviceFailure, classified.error);
2700    }
2701    // A rejected BitBox02 pairing carries no device code; upstream reports it as -13.
2702    if device_type == DeviceType::BitBox02
2703        && classified.code == HwiErrorCode::ActionCanceled.code()
2704        && common_device_error(err).is_some_and(|error| error.device_code().is_none())
2705    {
2706        return HwiError::new(HwiErrorCode::DeviceFailure, classified.error);
2707    }
2708    // Upstream raises "must be wiped before setup" as UnavailableActionError.
2709    if device_type == DeviceType::BitBox02
2710        && classified.code == HwiErrorCode::DeviceAlreadyInitialized.code()
2711    {
2712        return HwiError::new(HwiErrorCode::UnsupportedCommand, classified.error);
2713    }
2714    classified
2715}
2716
2717fn hwi_descriptor_addr_types(device_type: DeviceType, model: &str) -> Vec<HwiAddressType> {
2718    let mut types = vec![
2719        HwiAddressType::Legacy,
2720        HwiAddressType::Wit,
2721        HwiAddressType::ShWit,
2722    ];
2723    if can_sign_taproot(device_type, model) {
2724        types.push(HwiAddressType::Tap);
2725    }
2726    types
2727}
2728
2729fn hwi_descriptor_string(
2730    descriptor: &Descriptor<DescriptorPublicKey>,
2731) -> Result<String, checksum::Error> {
2732    let descriptor = format!("{descriptor:#}").replace('\'', "h");
2733    let mut checksum = checksum::Engine::new();
2734    checksum.input(&descriptor)?;
2735    Ok(format!("{descriptor}#{}", checksum.checksum()))
2736}
2737
2738fn keypool_path_descriptor_options(
2739    master_fingerprint: Fingerprint,
2740    path: &str,
2741    internal: bool,
2742    descriptor_type: DescriptorType,
2743    network: Network,
2744) -> Result<GetDescriptorOptions, HwiError> {
2745    if !path.starts_with("m/") {
2746        return Err(HwiError::new(
2747            HwiErrorCode::BadArgument,
2748            "Path must start with m/",
2749        ));
2750    }
2751    let Some(path) = path.strip_suffix("/*") else {
2752        return Err(HwiError::new(
2753            HwiErrorCode::BadArgument,
2754            "Path must end with /*",
2755        ));
2756    };
2757    let path = DerivationPath::from_str(path)
2758        .map_err(|err| HwiError::new(HwiErrorCode::BadArgument, err.to_string()))?;
2759    Ok(GetDescriptorOptions::with_path(
2760        master_fingerprint,
2761        path,
2762        internal,
2763        descriptor_type,
2764        network,
2765    ))
2766}
2767
2768fn get_xpub_response(xpub: Xpub, expert: bool) -> HwiGetXpubResponse {
2769    if !expert {
2770        return HwiGetXpubResponse {
2771            xpub: xpub.to_string(),
2772            testnet: None,
2773            private: None,
2774            depth: None,
2775            parent_fingerprint: None,
2776            child_num: None,
2777            chaincode: None,
2778            pubkey: None,
2779        };
2780    }
2781
2782    HwiGetXpubResponse {
2783        xpub: xpub.to_string(),
2784        testnet: Some(xpub.network == NetworkKind::Test),
2785        private: Some(false),
2786        depth: Some(xpub.depth),
2787        parent_fingerprint: Some(xpub.parent_fingerprint),
2788        child_num: Some(u32::from(xpub.child_number)),
2789        chaincode: Some(hex::encode(xpub.chain_code)),
2790        pubkey: Some(hex::encode(xpub.public_key.serialize())),
2791    }
2792}
2793
2794fn is_uninitialized_bitbox_error(
2795    device_type: DeviceType,
2796    err: &(dyn std::error::Error + 'static),
2797) -> bool {
2798    device_type == DeviceType::BitBox02
2799        && common_device_error(err)
2800            .is_some_and(|error| error.device_code() == Some(bhwi::common::DeviceCode::BitBox(105)))
2801}
2802
2803fn label_for(device_type: DeviceType, label: Option<String>) -> Option<Option<String>> {
2804    match device_type {
2805        DeviceType::Coldcard | DeviceType::KeepKey | DeviceType::Ledger | DeviceType::Trezor => {
2806            Some(label)
2807        }
2808        DeviceType::BitBox02 | DeviceType::Jade | DeviceType::Specter => None,
2809    }
2810}
2811
2812fn hwi_enumerate_model(
2813    device_type: DeviceType,
2814    model: &str,
2815    is_emulated: bool,
2816    firmware: Option<&str>,
2817) -> String {
2818    match (device_type, is_emulated) {
2819        (DeviceType::BitBox02, true) => "bitbox02_nova_multi".to_owned(),
2820        (DeviceType::KeepKey, true) => "keepkey_simulator".to_owned(),
2821        (DeviceType::KeepKey, false) => "keepkey".to_owned(),
2822        (DeviceType::Trezor, emulated) => match firmware {
2823            Some(reported) => {
2824                let suffix = if emulated { "_simulator" } else { "" };
2825                format!("trezor_{}{suffix}", reported.to_lowercase())
2826            }
2827            None => model.to_owned(),
2828        },
2829        _ => model.to_owned(),
2830    }
2831}
2832
2833fn hwi_enumerate_path(device_type: DeviceType, path: &str, is_emulated: bool) -> String {
2834    match (device_type, is_emulated) {
2835        (DeviceType::BitBox02, true) => path.strip_prefix("tcp:").unwrap_or(path).to_owned(),
2836        _ => path.to_owned(),
2837    }
2838}
2839
2840fn hwi_unavailable_action_message(
2841    device_type: DeviceType,
2842    action: &HwiUnsupportedDeviceAction,
2843) -> String {
2844    match (device_type, action) {
2845        (DeviceType::Ledger, HwiUnsupportedDeviceAction::Setup { .. }) => {
2846            "The Ledger Nano S and X do not support software setup"
2847        }
2848        (DeviceType::Ledger, HwiUnsupportedDeviceAction::Wipe) => {
2849            "The Ledger Nano S and X do not support wiping via software"
2850        }
2851        (DeviceType::Ledger, HwiUnsupportedDeviceAction::Restore { .. }) => {
2852            "The Ledger Nano S and X do not support restoring via software"
2853        }
2854        (DeviceType::Ledger, HwiUnsupportedDeviceAction::Backup { .. }) => {
2855            "The Ledger Nano S and X do not support creating a backup via software"
2856        }
2857        (DeviceType::Ledger, HwiUnsupportedDeviceAction::PromptPin) => {
2858            "The Ledger Nano S and X do not need a PIN sent from the host"
2859        }
2860        (DeviceType::Ledger, HwiUnsupportedDeviceAction::SendPin { .. }) => {
2861            "The Ledger Nano S and X do not need a PIN sent from the host"
2862        }
2863        (DeviceType::Ledger, HwiUnsupportedDeviceAction::TogglePassphrase) => {
2864            "The Ledger Nano S and X do not support toggling passphrase from the host"
2865        }
2866        (DeviceType::Trezor, HwiUnsupportedDeviceAction::Setup { .. }) => {
2867            "Trezor setup is not yet supported"
2868        }
2869        (DeviceType::Trezor, HwiUnsupportedDeviceAction::Wipe) => {
2870            "Trezor wipe is not yet supported"
2871        }
2872        (DeviceType::Trezor, HwiUnsupportedDeviceAction::Restore { .. }) => {
2873            "Trezor restore is not yet supported"
2874        }
2875        (DeviceType::Trezor, HwiUnsupportedDeviceAction::Backup { .. }) => {
2876            "The Trezor does not support creating a backup via software"
2877        }
2878        (DeviceType::Trezor, HwiUnsupportedDeviceAction::PromptPin) => {
2879            "Trezor PIN entry is not yet supported"
2880        }
2881        (DeviceType::Trezor, HwiUnsupportedDeviceAction::SendPin { .. }) => {
2882            "Trezor PIN entry is not yet supported"
2883        }
2884        (DeviceType::Trezor, HwiUnsupportedDeviceAction::TogglePassphrase) => {
2885            "Trezor passphrase toggling is not yet supported"
2886        }
2887        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::Setup { .. }) => {
2888            "KeepKey setup is unavailable"
2889        }
2890        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::Wipe) => "KeepKey wipe is unavailable",
2891        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::Restore { .. }) => {
2892            "KeepKey restore is unavailable"
2893        }
2894        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::Backup { .. }) => {
2895            "The Keepkey does not support creating a backup via software"
2896        }
2897        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::PromptPin) => {
2898            "KeepKey PIN entry is unavailable"
2899        }
2900        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::SendPin { .. }) => {
2901            "KeepKey PIN entry is unavailable"
2902        }
2903        (DeviceType::KeepKey, HwiUnsupportedDeviceAction::TogglePassphrase) => {
2904            "KeepKey passphrase toggling is unavailable"
2905        }
2906        (DeviceType::Jade, HwiUnsupportedDeviceAction::Setup { .. }) => {
2907            "Blockstream Jade does not support software setup"
2908        }
2909        (DeviceType::Jade, HwiUnsupportedDeviceAction::Wipe) => {
2910            "Blockstream Jade does not support wiping via software"
2911        }
2912        (DeviceType::Jade, HwiUnsupportedDeviceAction::Restore { .. }) => {
2913            "Blockstream Jade does not support restoring via software"
2914        }
2915        (DeviceType::Jade, HwiUnsupportedDeviceAction::Backup { .. }) => {
2916            "Blockstream Jade does not support creating a backup via software"
2917        }
2918        (DeviceType::Jade, HwiUnsupportedDeviceAction::PromptPin) => {
2919            "Blockstream Jade does not need a PIN sent from the host"
2920        }
2921        (DeviceType::Jade, HwiUnsupportedDeviceAction::SendPin { .. }) => {
2922            "Blockstream Jade does not need a PIN sent from the host"
2923        }
2924        (DeviceType::Jade, HwiUnsupportedDeviceAction::TogglePassphrase) => {
2925            "Blockstream Jade does not support toggling passphrase from the host"
2926        }
2927        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::Setup { .. }) => {
2928            "The Coldcard does not support software setup"
2929        }
2930        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::Wipe) => {
2931            "The Coldcard does not support wiping via software"
2932        }
2933        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::Restore { .. }) => {
2934            "The Coldcard does not support restoring via software"
2935        }
2936        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::Backup { .. }) => {
2937            "The Coldcard does not support creating a backup via software"
2938        }
2939        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::PromptPin) => {
2940            "The Coldcard does not need a PIN sent from the host"
2941        }
2942        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::SendPin { .. }) => {
2943            "The Coldcard does not need a PIN sent from the host"
2944        }
2945        (DeviceType::Coldcard, HwiUnsupportedDeviceAction::TogglePassphrase) => {
2946            "The Coldcard does not support toggling passphrase from the host"
2947        }
2948        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::Setup { .. }) => {
2949            "BitBox02 software setup is not implemented"
2950        }
2951        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::Wipe) => {
2952            "BitBox02 software wiping is not implemented"
2953        }
2954        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::Restore { .. }) => {
2955            "BitBox02 software restore is not implemented"
2956        }
2957        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::Backup { .. }) => {
2958            "BitBox02 software backup is not implemented"
2959        }
2960        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::PromptPin) => {
2961            "BitBox02 does not need a PIN sent from the host"
2962        }
2963        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::SendPin { .. }) => {
2964            "BitBox02 does not need a PIN sent from the host"
2965        }
2966        (DeviceType::BitBox02, HwiUnsupportedDeviceAction::TogglePassphrase) => {
2967            "BitBox02 passphrase toggling is not implemented"
2968        }
2969        (DeviceType::Specter, _) => "Specter-DIY does not support this operation from the host",
2970    }
2971    .to_owned()
2972}
2973
2974fn print_response(response: HwiResponse) -> ExitCode {
2975    println!(
2976        "{}",
2977        serde_json::to_string(&response).expect("serialize HWI response")
2978    );
2979    // Runtime error JSON exits 0 to match Python HWI 3.2.0 (hwilib/_cli.py main).
2980    ExitCode::SUCCESS
2981}
2982
2983fn parse_device_type(value: &str) -> HwiResult<DeviceType> {
2984    let unknown = || HwiError::new(HwiErrorCode::UnknownDevice, "Unknown device type specified");
2985    let family = value.split('_').next().unwrap_or(value);
2986    if family.eq_ignore_ascii_case("keepkey")
2987        && !value.eq_ignore_ascii_case("keepkey")
2988        && !value.eq_ignore_ascii_case("keepkey_simulator")
2989    {
2990        return Err(unknown());
2991    }
2992    [
2993        ("bitbox02", DeviceType::BitBox02),
2994        ("coldcard", DeviceType::Coldcard),
2995        ("jade", DeviceType::Jade),
2996        ("ledger", DeviceType::Ledger),
2997        ("keepkey", DeviceType::KeepKey),
2998        ("trezor", DeviceType::Trezor),
2999    ]
3000    .into_iter()
3001    .find_map(|(name, device_type)| family.eq_ignore_ascii_case(name).then_some(device_type))
3002    .ok_or_else(unknown)
3003}
3004
3005fn is_known_emulator_path(device_type: Option<DeviceType>, path: Option<&str>) -> bool {
3006    matches!(
3007        (device_type, path),
3008        (
3009            Some(DeviceType::BitBox02),
3010            Some("127.0.0.1:15423" | "tcp:127.0.0.1:15423")
3011        ) | (Some(DeviceType::Coldcard), Some("/tmp/ckcc-simulator.sock"))
3012            | (
3013                Some(DeviceType::Jade),
3014                Some("127.0.0.1:30121" | "tcp:127.0.0.1:30121")
3015            )
3016            | (
3017                Some(DeviceType::Ledger),
3018                Some("127.0.0.1:9999" | "tcp:127.0.0.1:9999")
3019            )
3020            | (
3021                Some(DeviceType::KeepKey),
3022                Some("127.0.0.1:11044" | "udp:127.0.0.1:11044")
3023            )
3024            | (
3025                Some(DeviceType::Trezor),
3026                Some("127.0.0.1:21324" | "udp:127.0.0.1:21324")
3027            )
3028    )
3029}
3030
3031fn read_stdin_password(args: &mut HwiCli) -> HwiResult<()> {
3032    if args.stdinpass {
3033        let password = match rpassword::prompt_password("Enter your device password: ") {
3034            Ok(password) => password,
3035            Err(_) => {
3036                eprintln!("Warning: Password input may be echoed.");
3037                eprint!("Enter your device password: ");
3038                let mut line = String::new();
3039                std::io::stdin()
3040                    .read_line(&mut line)
3041                    .map_err(|err| HwiError::new(HwiErrorCode::BadArgument, err.to_string()))?;
3042                line.trim_end_matches(['\r', '\n']).to_owned()
3043            }
3044        };
3045        args.password = Some(password);
3046    }
3047    Ok(())
3048}
3049
3050fn request_from_cli(args: HwiCli) -> HwiResult<HwiRequest> {
3051    let _accepted_python_hwi_globals = (args.debug, args.stdin, args.interactive, args.stdinpass);
3052    let password_given = args.password.is_some();
3053    #[cfg(any(feature = "trezor", feature = "keepkey"))]
3054    let passphrase = args.password.map(HostPassphrase::new);
3055    let expert = args.expert;
3056    // The raw device type stays unparsed until device lookup; upstream only
3057    // rejects an unknown type once `get_client` is reached.
3058    let device_type = args.device_type;
3059    let mut device_path = args.device_path;
3060    #[cfg(feature = "keepkey")]
3061    if matches!(&args.command, HwiCliCommand::Enumerate)
3062        && device_path.is_none()
3063        && device_type
3064            .as_deref()
3065            .is_some_and(|raw| raw.eq_ignore_ascii_case("keepkey_simulator"))
3066    {
3067        device_path = Some(DEFAULT_KEEPKEY_EMULATOR.to_owned());
3068    }
3069    let include_emulators = args.emulators
3070        || is_known_emulator_path(
3071            device_type
3072                .as_deref()
3073                .and_then(|raw| parse_device_type(raw).ok()),
3074            device_path.as_deref(),
3075        );
3076    let network = parse_chain(&args.chain)?;
3077    let command = match args.command {
3078        HwiCliCommand::Enumerate => HwiCommand::Enumerate,
3079        HwiCliCommand::Getmasterxpub { addr_type, account } => {
3080            HwiCommand::GetMasterXpub { addr_type, account }
3081        }
3082        HwiCliCommand::Signtx { psbt } => HwiCommand::SignTx { psbt },
3083        HwiCliCommand::Signmessage { message, path } => HwiCommand::SignMessage { message, path },
3084        HwiCliCommand::Displayaddress {
3085            path,
3086            desc,
3087            addr_type,
3088        } => match (path, desc) {
3089            (Some(path), None) => {
3090                HwiCommand::DisplayAddress(HwiDisplayAddressRequest::Path { path, addr_type })
3091            }
3092            (None, Some(descriptor)) => {
3093                HwiCommand::DisplayAddress(HwiDisplayAddressRequest::Descriptor { descriptor })
3094            }
3095            _ => {
3096                return Err(HwiError::new(
3097                    HwiErrorCode::BadArgument,
3098                    "displayaddress requires exactly one of --path or --desc",
3099                ));
3100            }
3101        },
3102        HwiCliCommand::Getxpub { path } => HwiCommand::GetXpub { path, expert },
3103        HwiCliCommand::Getdescriptors { account } => HwiCommand::GetDescriptors { account },
3104        HwiCliCommand::Getkeypool {
3105            start,
3106            end,
3107            keypool: _keypool,
3108            nokeypool,
3109            internal,
3110            addr_type,
3111            all,
3112            account,
3113            path,
3114        } => HwiCommand::GetKeypool {
3115            start,
3116            end,
3117            internal,
3118            keypool: !nokeypool,
3119            account,
3120            addr_type: addr_type.unwrap_or(HwiAddressType::Wit),
3121            all,
3122            path,
3123        },
3124        HwiCliCommand::Setup {
3125            label,
3126            backup_passphrase,
3127        } => HwiCommand::Setup {
3128            interactive: args.interactive,
3129            label,
3130            backup_passphrase,
3131        },
3132        HwiCliCommand::Wipe => HwiCommand::Wipe,
3133        HwiCliCommand::Restore { word_count, label } => HwiCommand::Restore {
3134            interactive: args.interactive,
3135            word_count,
3136            label,
3137        },
3138        HwiCliCommand::Backup {
3139            label,
3140            backup_passphrase,
3141        } => HwiCommand::Backup {
3142            label,
3143            backup_passphrase,
3144        },
3145        HwiCliCommand::Promptpin => HwiCommand::PromptPin,
3146        HwiCliCommand::Sendpin { pin } => HwiCommand::SendPin { pin },
3147        HwiCliCommand::Togglepassphrase => HwiCommand::TogglePassphrase,
3148        #[cfg(target_os = "linux")]
3149        HwiCliCommand::Installudevrules { location } => HwiCommand::InstallUdevRules { location },
3150        HwiCliCommand::External(argv) => {
3151            let command = argv
3152                .first()
3153                .and_then(|arg| arg.to_str())
3154                .unwrap_or("<unknown>")
3155                .to_owned();
3156            HwiCommand::Unsupported(command)
3157        }
3158    };
3159    Ok(HwiRequest {
3160        selector: HwiSelector {
3161            network,
3162            fingerprint: args.fingerprint,
3163            device_type,
3164            device_path,
3165            include_emulators,
3166            #[cfg(any(feature = "trezor", feature = "keepkey"))]
3167            passphrase,
3168            password_given,
3169        },
3170        command,
3171    })
3172}
3173
3174fn parse_chain(value: &str) -> HwiResult<Network> {
3175    match value {
3176        "main" | "mainnet" => Ok(Network::Bitcoin),
3177        "test" | "testnet" => Ok(Network::Testnet),
3178        _ => Network::from_str(value).map_err(|_| {
3179            HwiError::new(
3180                HwiErrorCode::MissingArguments,
3181                format!("argument --chain: invalid choice: '{value}'"),
3182            )
3183        }),
3184    }
3185}
3186
3187fn option_fingerprint<S>(value: &Option<Fingerprint>, ser: S) -> Result<S::Ok, S::Error>
3188where
3189    S: Serializer,
3190{
3191    if let Some(v) = value {
3192        hex::serialize(v, ser)
3193    } else {
3194        ser.serialize_none()
3195    }
3196}
3197
3198#[cfg(test)]
3199mod tests {
3200    use super::*;
3201    use chrono::{FixedOffset, TimeZone};
3202
3203    #[test]
3204    fn display_address_errors_keep_python_hwi_codes() {
3205        let parse = display_address_error(DisplayAddressError::UnsupportedDescriptor(
3206            "wsh(x)".to_owned(),
3207        ));
3208        assert_eq!(parse.code, HwiErrorCode::BadArgument.code());
3209        assert_eq!(parse.error, "Unsupported displayaddress descriptor: wsh(x)");
3210
3211        let device = display_address_error(DisplayAddressError::Device(
3212            bhwi_async::HWIDeviceError::new(std::io::Error::other("boom")),
3213        ));
3214        assert_eq!(device.code, HwiErrorCode::DeviceConnectionError.code());
3215        assert_eq!(device.error, "boom");
3216    }
3217
3218    #[test]
3219    fn device_type_accepts_model_qualified_names() {
3220        for value in ["trezor", "trezor_1", "trezor_t", "trezor_1_simulator"] {
3221            assert_eq!(parse_device_type(value).unwrap(), DeviceType::Trezor);
3222        }
3223        assert_eq!(parse_device_type("bitbox02").unwrap(), DeviceType::BitBox02);
3224        assert_eq!(
3225            parse_device_type("coldcard_simulator").unwrap(),
3226            DeviceType::Coldcard
3227        );
3228        assert!(parse_device_type("notadevice").is_err());
3229    }
3230
3231    #[test]
3232    fn keepkey_selector_accepts_models_and_both_emulator_path_forms() {
3233        for model in ["keepkey", "keepkey_simulator"] {
3234            assert_eq!(parse_device_type(model).unwrap(), DeviceType::KeepKey);
3235        }
3236        assert!(hwi_selector_matches_device(
3237            Some("keepkey"),
3238            DeviceType::KeepKey,
3239            "keepkey",
3240            false,
3241        ));
3242        assert!(hwi_selector_matches_device(
3243            Some("keepkey"),
3244            DeviceType::KeepKey,
3245            "keepkey_simulator",
3246            true,
3247        ));
3248        for path in ["127.0.0.1:11044", "udp:127.0.0.1:11044"] {
3249            let request = parse_args([
3250                "hwi",
3251                "--device-type",
3252                "keepkey_simulator",
3253                "--device-path",
3254                path,
3255                "enumerate",
3256            ])
3257            .unwrap();
3258            assert_eq!(
3259                request.selector.device_type.as_deref(),
3260                Some("keepkey_simulator")
3261            );
3262            assert_eq!(request.selector.device_path.as_deref(), Some(path));
3263            assert!(request.selector.include_emulators);
3264        }
3265        assert!(!is_known_emulator_path(
3266            Some(DeviceType::KeepKey),
3267            Some("tcp:127.0.0.1:11044"),
3268        ));
3269    }
3270
3271    #[test]
3272    fn keepkey_selector_rejects_unknown_model_suffix() {
3273        let raw = "keepkey_not_a_model";
3274        let error = parse_device_type(raw).expect_err("unknown KeepKey model");
3275        assert_eq!(error.code, HwiErrorCode::UnknownDevice.code());
3276        assert_eq!(error.error, "Unknown device type specified");
3277
3278        // Enumerate drops unknown filters, while command lookup rejects them.
3279        let enumerate_filter = parse_device_type(raw).ok().map(|_| raw);
3280        assert!(enumerate_filter.is_none());
3281        assert!(hwi_selector_matches_device(
3282            enumerate_filter,
3283            DeviceType::Trezor,
3284            "trezor_t",
3285            false,
3286        ));
3287
3288        let request = parse_args(["hwi", "-t", raw, "getxpub", "m/44h/1h/0h"]).unwrap();
3289        let response = futures::executor::block_on(process_request(request));
3290        let HwiResponse::Error(error) = response else {
3291            panic!("expected HWI error");
3292        };
3293        assert_eq!(error.code, HwiErrorCode::DeviceConnectionError.code());
3294        assert_eq!(
3295            error.error,
3296            "Could not find device with specified fingerprint or type"
3297        );
3298
3299        let request = parse_args([
3300            "hwi",
3301            "-t",
3302            raw,
3303            "-d",
3304            "/dev/null",
3305            "getxpub",
3306            "m/44h/1h/0h",
3307        ])
3308        .unwrap();
3309        let response = futures::executor::block_on(process_request(request));
3310        let HwiResponse::Error(error) = response else {
3311            panic!("expected HWI error");
3312        };
3313        assert_eq!(error.code, HwiErrorCode::UnknownDevice.code());
3314        assert_eq!(error.error, "Unknown device type specified");
3315    }
3316
3317    #[test]
3318    #[cfg(feature = "keepkey")]
3319    fn keepkey_selector_pathless_simulator_matches_only_emulated_model() {
3320        let request =
3321            parse_args(["hwi", "--device-type", "keepkey_simulator", "enumerate"]).unwrap();
3322        assert_eq!(
3323            request.selector.device_path.as_deref(),
3324            Some(DEFAULT_KEEPKEY_EMULATOR)
3325        );
3326        assert!(request.selector.include_emulators);
3327
3328        let raw = request.selector.device_type.as_deref();
3329        assert!(hwi_selector_matches_device(
3330            raw,
3331            DeviceType::KeepKey,
3332            "keepkey_simulator",
3333            true,
3334        ));
3335        assert!(!hwi_selector_matches_device(
3336            raw,
3337            DeviceType::KeepKey,
3338            "keepkey",
3339            false,
3340        ));
3341        assert!(!hwi_selector_matches_device(
3342            raw,
3343            DeviceType::KeepKey,
3344            "keepkey",
3345            true,
3346        ));
3347        assert!(!hwi_selector_matches_device(
3348            raw,
3349            DeviceType::Trezor,
3350            "keepkey_simulator",
3351            true,
3352        ));
3353    }
3354
3355    #[test]
3356    fn keepkey_simulator_non_enumerate_requires_emulator_flag() {
3357        let request =
3358            parse_args(["hwi", "--device-type", "keepkey_simulator", "getmasterxpub"]).unwrap();
3359        assert_eq!(request.selector.device_path, None);
3360        assert!(!request.selector.include_emulators);
3361
3362        let request = parse_args([
3363            "hwi",
3364            "--device-type",
3365            "keepkey_simulator",
3366            "--emulators",
3367            "getmasterxpub",
3368        ])
3369        .unwrap();
3370        assert!(request.selector.include_emulators);
3371    }
3372
3373    #[test]
3374    fn keepkey_selector_for_destructive_command_cannot_select_physical_device() {
3375        let request = parse_args([
3376            "hwi",
3377            "--device-type",
3378            "keepkey_simulator",
3379            "--device-path",
3380            "hid:physical-keepkey",
3381            "wipe",
3382        ])
3383        .unwrap();
3384        assert_eq!(request.command, HwiCommand::Wipe);
3385        assert!(!request.selector.include_emulators);
3386        assert!(!hwi_selector_matches_device(
3387            request.selector.device_type.as_deref(),
3388            DeviceType::KeepKey,
3389            "keepkey",
3390            false,
3391        ));
3392    }
3393
3394    #[test]
3395    fn backup_filename_matches_python_hwi_local_time_format() {
3396        let time = FixedOffset::east_opt(3 * 60 * 60)
3397            .unwrap()
3398            .with_ymd_and_hms(2026, 7, 21, 9, 5, 0)
3399            .single()
3400            .unwrap();
3401
3402        assert_eq!(format_hwi_backup_filename(time), "backup-20260721-0905.7z");
3403    }
3404
3405    #[test]
3406    fn parses_enumerate_selector() {
3407        let request = parse_args([
3408            "hwi",
3409            "--chain",
3410            "test",
3411            "-f",
3412            "f5acc2fd",
3413            "-t",
3414            "ledger",
3415            "-d",
3416            "tcp:localhost:9999",
3417            "--emulators",
3418            "enumerate",
3419        ])
3420        .expect("request");
3421
3422        assert_eq!(request.selector.network, Network::Testnet);
3423        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3424        assert_eq!(
3425            request.selector.device_path.as_deref(),
3426            Some("tcp:localhost:9999")
3427        );
3428        assert!(request.selector.include_emulators);
3429        assert_eq!(request.command, HwiCommand::Enumerate);
3430    }
3431
3432    #[test]
3433    fn any_given_password_is_marked_in_every_build() {
3434        for (argv, given) in [
3435            (vec!["hwi", "--password", "secret", "enumerate"], true),
3436            (vec!["hwi", "-p", "", "enumerate"], true),
3437            (vec!["hwi", "enumerate"], false),
3438        ] {
3439            let request = parse_args(argv).expect("request");
3440            assert_eq!(request.selector.password_given, given);
3441        }
3442    }
3443
3444    #[test]
3445    fn parses_enumerate_python_hwi_global_flags() {
3446        let request = parse_args([
3447            "hwi",
3448            "--password",
3449            "passphrase",
3450            "--debug",
3451            "--stdin",
3452            "--interactive",
3453            "--expert",
3454            "--stdinpass",
3455            "enumerate",
3456        ])
3457        .expect("request");
3458
3459        assert_eq!(request.command, HwiCommand::Enumerate);
3460    }
3461
3462    #[test]
3463    fn parses_enumerate_python_hwi_short_flags() {
3464        let request = parse_args([
3465            "hwi",
3466            "-p",
3467            "passphrase",
3468            "-i",
3469            "-f",
3470            "f5acc2fd",
3471            "-t",
3472            "ledger",
3473            "-d",
3474            "tcp:localhost:9999",
3475            "enumerate",
3476        ])
3477        .expect("request");
3478
3479        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3480        assert_eq!(
3481            request.selector.device_path.as_deref(),
3482            Some("tcp:localhost:9999")
3483        );
3484        assert_eq!(request.command, HwiCommand::Enumerate);
3485    }
3486
3487    #[test]
3488    fn parses_getxpub_with_expert_flag() {
3489        let request = parse_args([
3490            "hwi",
3491            "--chain",
3492            "test",
3493            "--expert",
3494            "--device-type",
3495            "ledger",
3496            "--emulators",
3497            "getxpub",
3498            "m/44h/1h/0h/0/3",
3499        ])
3500        .expect("request");
3501
3502        assert_eq!(request.selector.network, Network::Testnet);
3503        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3504        assert!(request.selector.include_emulators);
3505        assert_eq!(
3506            request.command,
3507            HwiCommand::GetXpub {
3508                path: "m/44h/1h/0h/0/3".to_owned(),
3509                expert: true,
3510            }
3511        );
3512    }
3513
3514    #[test]
3515    fn parses_getmasterxpub_defaults() {
3516        let request = parse_args([
3517            "hwi",
3518            "--chain",
3519            "test",
3520            "--device-type",
3521            "ledger",
3522            "--emulators",
3523            "getmasterxpub",
3524        ])
3525        .expect("request");
3526
3527        assert_eq!(
3528            request.command,
3529            HwiCommand::GetMasterXpub {
3530                addr_type: HwiAddressType::Wit,
3531                account: 0,
3532            }
3533        );
3534    }
3535
3536    #[test]
3537    fn parses_getmasterxpub_addr_type_and_account() {
3538        let request = parse_args([
3539            "hwi",
3540            "--device-type",
3541            "ledger",
3542            "getmasterxpub",
3543            "--addr-type",
3544            "sh_wit",
3545            "--account",
3546            "7",
3547        ])
3548        .expect("request");
3549
3550        assert_eq!(
3551            request.command,
3552            HwiCommand::GetMasterXpub {
3553                addr_type: HwiAddressType::ShWit,
3554                account: 7,
3555            }
3556        );
3557    }
3558
3559    #[test]
3560    fn parses_signtx_psbt_argument() {
3561        let psbt = "cHNidP8BAHECAAAAAf//////////////////////////////////////////AAAAAAD/////AQAAAAAAAAAAAFYAAAAAAAABAR8AAAAAAAAAAFYA";
3562        let request = parse_args([
3563            "hwi",
3564            "--chain",
3565            "test",
3566            "--device-type",
3567            "ledger",
3568            "signtx",
3569            psbt,
3570        ])
3571        .expect("request");
3572
3573        assert_eq!(request.selector.network, Network::Testnet);
3574        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3575        assert_eq!(
3576            request.command,
3577            HwiCommand::SignTx {
3578                psbt: psbt.to_owned(),
3579            }
3580        );
3581    }
3582
3583    #[test]
3584    fn parses_signmessage_arguments() {
3585        let request = parse_args([
3586            "hwi",
3587            "--chain",
3588            "test",
3589            "--device-type",
3590            "ledger",
3591            "signmessage",
3592            "hello",
3593            "m/44'/1'/0'/0",
3594        ])
3595        .expect("request");
3596
3597        assert_eq!(request.selector.network, Network::Testnet);
3598        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3599        assert_eq!(
3600            request.command,
3601            HwiCommand::SignMessage {
3602                message: "hello".to_owned(),
3603                path: "m/44'/1'/0'/0".to_owned(),
3604            }
3605        );
3606    }
3607
3608    #[test]
3609    fn parses_displayaddress_path() {
3610        let request = parse_args([
3611            "hwi",
3612            "--chain",
3613            "test",
3614            "--device-type",
3615            "ledger",
3616            "displayaddress",
3617            "--addr-type",
3618            "sh_wit",
3619            "--path",
3620            "m/49h/1h/0h/0/0",
3621        ])
3622        .expect("request");
3623
3624        assert_eq!(request.selector.network, Network::Testnet);
3625        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3626        assert_eq!(
3627            request.command,
3628            HwiCommand::DisplayAddress(HwiDisplayAddressRequest::Path {
3629                path: "m/49h/1h/0h/0/0".to_owned(),
3630                addr_type: HwiAddressType::ShWit,
3631            })
3632        );
3633    }
3634
3635    #[test]
3636    fn parses_displayaddress_descriptor() {
3637        let descriptor = "wpkh([f5acc2fd/84h/1h/0h]tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/0/0)";
3638        let request = parse_args([
3639            "hwi",
3640            "--chain",
3641            "test",
3642            "--device-type",
3643            "ledger",
3644            "displayaddress",
3645            "--desc",
3646            descriptor,
3647        ])
3648        .expect("request");
3649
3650        assert_eq!(
3651            request.command,
3652            HwiCommand::DisplayAddress(HwiDisplayAddressRequest::Descriptor {
3653                descriptor: descriptor.to_owned(),
3654            })
3655        );
3656    }
3657
3658    #[test]
3659    fn parses_getdescriptors_account() {
3660        let request = parse_args([
3661            "hwi",
3662            "--chain",
3663            "test",
3664            "--device-type",
3665            "ledger",
3666            "getdescriptors",
3667            "--account",
3668            "3",
3669        ])
3670        .expect("request");
3671
3672        assert_eq!(request.selector.network, Network::Testnet);
3673        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3674        assert_eq!(request.command, HwiCommand::GetDescriptors { account: 3 });
3675    }
3676
3677    #[test]
3678    fn parses_getkeypool_defaults() {
3679        let request = parse_args([
3680            "hwi",
3681            "--chain",
3682            "test",
3683            "--device-type",
3684            "ledger",
3685            "--emulators",
3686            "getkeypool",
3687            "0",
3688            "10",
3689        ])
3690        .expect("request");
3691
3692        assert_eq!(request.selector.network, Network::Testnet);
3693        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3694        assert!(request.selector.include_emulators);
3695        assert_eq!(
3696            request.command,
3697            HwiCommand::GetKeypool {
3698                start: 0,
3699                end: 10,
3700                internal: false,
3701                keypool: true,
3702                account: 0,
3703                addr_type: HwiAddressType::Wit,
3704                all: false,
3705                path: None,
3706            }
3707        );
3708    }
3709
3710    #[test]
3711    fn parses_getkeypool_all_options() {
3712        let request = parse_args([
3713            "hwi",
3714            "--chain",
3715            "test",
3716            "--device-type",
3717            "ledger",
3718            "getkeypool",
3719            "--nokeypool",
3720            "--internal",
3721            "--all",
3722            "--account",
3723            "2",
3724            "--path",
3725            "m/84h/1h/0h/1/*",
3726            "5",
3727            "8",
3728        ])
3729        .expect("request");
3730
3731        assert_eq!(
3732            request.command,
3733            HwiCommand::GetKeypool {
3734                start: 5,
3735                end: 8,
3736                internal: true,
3737                keypool: false,
3738                account: 2,
3739                addr_type: HwiAddressType::Wit,
3740                all: true,
3741                path: Some("m/84h/1h/0h/1/*".to_owned()),
3742            }
3743        );
3744    }
3745
3746    #[test]
3747    fn parses_getkeypool_addr_type() {
3748        let request = parse_args([
3749            "hwi",
3750            "--chain",
3751            "test",
3752            "--device-type",
3753            "ledger",
3754            "getkeypool",
3755            "--addr-type",
3756            "sh_wit",
3757            "--keypool",
3758            "5",
3759            "8",
3760        ])
3761        .expect("request");
3762
3763        assert_eq!(
3764            request.command,
3765            HwiCommand::GetKeypool {
3766                start: 5,
3767                end: 8,
3768                internal: false,
3769                keypool: true,
3770                account: 0,
3771                addr_type: HwiAddressType::ShWit,
3772                all: false,
3773                path: None,
3774            }
3775        );
3776    }
3777
3778    #[test]
3779    fn accepts_python_hwi_version_flag() {
3780        let error = HwiCli::try_parse_from(["hwi", "--version"]).expect_err("version exits");
3781
3782        assert_eq!(error.kind(), ErrorKind::DisplayVersion);
3783    }
3784
3785    #[test]
3786    fn unknown_device_type_without_path_reports_no_device_found() {
3787        // Upstream find_device never matches an unknown type: -3, not -4.
3788        let request = parse_args(["hwi", "-t", "nonexistent", "getxpub", "m/44h/1h/0h"])
3789            .expect("unknown type parses");
3790        let response = futures::executor::block_on(process_request(request));
3791        let HwiResponse::Error(error) = response else {
3792            panic!("expected HWI error");
3793        };
3794        assert_eq!(error.code, HwiErrorCode::DeviceConnectionError.code());
3795        assert_eq!(
3796            error.error,
3797            "Could not find device with specified fingerprint or type"
3798        );
3799    }
3800
3801    #[test]
3802    fn unknown_device_type_with_path_reports_unknown_device() {
3803        // Upstream get_client raises UnknownDeviceError (-4) when -d is given.
3804        let request = parse_args([
3805            "hwi",
3806            "-t",
3807            "nonexistent",
3808            "-d",
3809            "/dev/null",
3810            "getxpub",
3811            "m/44h/1h/0h",
3812        ])
3813        .expect("unknown type parses");
3814        let response = futures::executor::block_on(process_request(request));
3815        let HwiResponse::Error(error) = response else {
3816            panic!("expected HWI error");
3817        };
3818        assert_eq!(error.code, HwiErrorCode::UnknownDevice.code());
3819        assert_eq!(error.error, "Unknown device type specified");
3820    }
3821
3822    #[tokio::test]
3823    async fn keepkey_taproot_display_is_rejected_before_lookup_and_path_parsing() {
3824        let request = parse_args([
3825            "hwi",
3826            "-t",
3827            "keepkey",
3828            "displayaddress",
3829            "--path",
3830            "not_a_derivation_path",
3831            "--addr-type",
3832            "tap",
3833        ])
3834        .unwrap();
3835        let response = process_request(request).await;
3836        let HwiResponse::Error(error) = response else {
3837            panic!("expected HWI error");
3838        };
3839        assert_eq!(error.code, HwiErrorCode::UnsupportedCommand.code());
3840        assert_eq!(
3841            error.error,
3842            "This device does not support displaying Taproot addresses"
3843        );
3844    }
3845
3846    #[tokio::test]
3847    async fn keepkey_taproot_keypool_is_rejected_before_device_lookup() {
3848        let request = parse_args([
3849            "hwi",
3850            "-t",
3851            "keepkey",
3852            "getkeypool",
3853            "0",
3854            "1",
3855            "--addr-type",
3856            "tap",
3857        ])
3858        .unwrap();
3859        let response = process_request(request).await;
3860        let HwiResponse::Error(error) = response else {
3861            panic!("expected HWI error");
3862        };
3863        assert_eq!(error.code, HwiErrorCode::UnsupportedCommand.code());
3864        assert_eq!(error.error, "Device does not support Taproot");
3865    }
3866
3867    #[tokio::test]
3868    async fn invalid_arguments_without_device_report_no_device_found() {
3869        // Argument validation is deferred until after device lookup; with no
3870        // matching device every case collapses to -3 like upstream.
3871        for args in [
3872            ["hwi", "-t", "trezor", "signtx", "notapsbt"].as_slice(),
3873            ["hwi", "-t", "trezor", "getxpub", "not_a_path"].as_slice(),
3874            ["hwi", "-t", "trezor", "signmessage", "hello", "bad/path"].as_slice(),
3875            [
3876                "hwi",
3877                "-t",
3878                "trezor",
3879                "displayaddress",
3880                "--path",
3881                "bad/path",
3882            ]
3883            .as_slice(),
3884        ] {
3885            let request = parse_args(args.iter().copied()).expect("request parses");
3886            let response = process_request(request).await;
3887            let HwiResponse::Error(error) = response else {
3888                panic!("expected HWI error for {args:?}");
3889            };
3890            assert_eq!(
3891                error.code,
3892                HwiErrorCode::DeviceConnectionError.code(),
3893                "{args:?}: {}",
3894                error.error
3895            );
3896        }
3897    }
3898
3899    #[test]
3900    fn accepts_bitbox02_device_type() {
3901        let request =
3902            parse_args(["hwi", "--device-type", "bitbox02", "enumerate"]).expect("bitbox02 parses");
3903
3904        assert_eq!(request.selector.device_type.as_deref(), Some("bitbox02"));
3905    }
3906
3907    #[test]
3908    fn trezor_enumerate_model_follows_python_hwi() {
3909        let model = |firmware, emulated| {
3910            hwi_enumerate_model(DeviceType::Trezor, "trezor_t", emulated, firmware)
3911        };
3912        assert_eq!(model(Some("T"), false), "trezor_t");
3913        assert_eq!(model(Some("1"), false), "trezor_1");
3914        assert_eq!(model(Some("Safe 3"), false), "trezor_safe 3");
3915        assert_eq!(model(Some("1"), true), "trezor_1_simulator");
3916        assert_eq!(model(Some("T"), true), "trezor_t_simulator");
3917        assert_eq!(model(None, false), "trezor_t");
3918    }
3919
3920    #[test]
3921    fn keepkey_enumerate_model_is_canonical() {
3922        assert_eq!(
3923            hwi_enumerate_model(DeviceType::KeepKey, "ignored", false, Some("K1-14AM")),
3924            "keepkey"
3925        );
3926        assert_eq!(
3927            hwi_enumerate_model(DeviceType::KeepKey, "ignored", true, Some("K1-14AM")),
3928            "keepkey_simulator"
3929        );
3930    }
3931
3932    #[test]
3933    fn accepts_trezor_device_type() {
3934        let request =
3935            parse_args(["hwi", "--device-type", "trezor", "enumerate"]).expect("trezor parses");
3936
3937        assert_eq!(request.selector.device_type.as_deref(), Some("trezor"));
3938    }
3939
3940    #[test]
3941    fn parses_setup_action() {
3942        let request = parse_args([
3943            "hwi",
3944            "--chain",
3945            "test",
3946            "--device-type",
3947            "ledger",
3948            "--interactive",
3949            "setup",
3950            "-l",
3951            "HWI Ledger",
3952            "-b",
3953            "backup passphrase",
3954        ])
3955        .expect("setup request");
3956
3957        assert_eq!(request.selector.network, Network::Testnet);
3958        assert_eq!(request.selector.device_type.as_deref(), Some("ledger"));
3959        assert_eq!(
3960            request.command,
3961            HwiCommand::Setup {
3962                interactive: true,
3963                label: "HWI Ledger".to_owned(),
3964                backup_passphrase: "backup passphrase".to_owned(),
3965            }
3966        );
3967    }
3968
3969    #[test]
3970    fn parses_wipe_action() {
3971        let request =
3972            parse_args(["hwi", "--device-type", "bitbox02", "wipe"]).expect("wipe request");
3973
3974        assert_eq!(request.selector.device_type.as_deref(), Some("bitbox02"));
3975        assert_eq!(request.command, HwiCommand::Wipe);
3976    }
3977
3978    #[test]
3979    fn parses_restore_action() {
3980        let request = parse_args([
3981            "hwi",
3982            "--device-type",
3983            "jade",
3984            "--interactive",
3985            "restore",
3986            "--word_count",
3987            "12",
3988            "--label",
3989            "HWI Jade",
3990        ])
3991        .expect("restore request");
3992
3993        assert_eq!(
3994            request.command,
3995            HwiCommand::Restore {
3996                interactive: true,
3997                word_count: 12,
3998                label: "HWI Jade".to_owned(),
3999            }
4000        );
4001    }
4002
4003    #[test]
4004    fn parses_backup_action() {
4005        let request = parse_args([
4006            "hwi",
4007            "--device-type",
4008            "coldcard",
4009            "backup",
4010            "--label",
4011            "HWI Coldcard",
4012            "--backup_passphrase",
4013            "backup passphrase",
4014        ])
4015        .expect("unsupported backup request");
4016
4017        assert_eq!(
4018            request.command,
4019            HwiCommand::Backup {
4020                label: "HWI Coldcard".to_owned(),
4021                backup_passphrase: "backup passphrase".to_owned(),
4022            }
4023        );
4024    }
4025
4026    #[test]
4027    fn parses_pin_actions_and_toggle_passphrase() {
4028        // Device support is decided when the device is found, not while parsing.
4029        let promptpin =
4030            parse_args(["hwi", "--device-type", "ledger", "promptpin"]).expect("promptpin request");
4031        assert_eq!(promptpin.command, HwiCommand::PromptPin);
4032
4033        let sendpin = parse_args(["hwi", "--device-type", "trezor", "sendpin", "1234"])
4034            .expect("sendpin request");
4035        assert_eq!(
4036            sendpin.command,
4037            HwiCommand::SendPin {
4038                pin: "1234".to_owned()
4039            }
4040        );
4041
4042        let togglepassphrase = parse_args(["hwi", "--device-type", "ledger", "togglepassphrase"])
4043            .expect("togglepassphrase request");
4044        assert_eq!(togglepassphrase.command, HwiCommand::TogglePassphrase);
4045    }
4046
4047    #[cfg(target_os = "linux")]
4048    #[test]
4049    fn parses_installudevrules_without_device_selection() {
4050        let request = parse_args(["hwi", "installudevrules", "--location", "/tmp/bhwi-rules.d"])
4051            .expect("installudevrules request");
4052
4053        assert_eq!(request.selector.device_type, None);
4054        assert_eq!(request.selector.fingerprint, None);
4055        assert_eq!(
4056            request.command,
4057            HwiCommand::InstallUdevRules {
4058                location: PathBuf::from("/tmp/bhwi-rules.d"),
4059            }
4060        );
4061    }
4062
4063    #[test]
4064    fn captures_unknown_unsupported_commands() {
4065        let request = parse_args(["hwi", "unknowncommand"]).expect("unsupported command request");
4066
4067        assert_eq!(
4068            request.command,
4069            HwiCommand::Unsupported("unknowncommand".to_owned())
4070        );
4071    }
4072
4073    fn outcome_of(args: &[&str]) -> CliOutcome {
4074        cli_outcome(args.iter().map(OsString::from).collect())
4075    }
4076
4077    fn usage_of(args: &[&str]) -> UsageError {
4078        match outcome_of(args) {
4079            CliOutcome::Usage(usage) => usage,
4080            other => panic!("expected usage error for {args:?}, got {other:?}"),
4081        }
4082    }
4083
4084    #[test]
4085    fn missing_command_is_a_usage_error() {
4086        let args = ["hwi"];
4087        let usage = usage_of(&args);
4088
4089        assert_eq!(
4090            usage.message,
4091            "hwi: error: the following arguments are required: command"
4092        );
4093        assert!(usage.usage.contains("Usage:"), "{}", usage.usage);
4094        assert!(usage.usage.contains("hwi"), "{}", usage.usage);
4095        assert_eq!(exit_status(&outcome_of(&args)), 2);
4096    }
4097
4098    #[test]
4099    fn unknown_subcommand_is_a_usage_error() {
4100        let args = ["hwi", "boguscmd"];
4101        let usage = usage_of(&args);
4102
4103        assert_eq!(
4104            usage.message,
4105            "hwi: error: argument command: invalid choice: 'boguscmd'"
4106        );
4107        assert_eq!(exit_status(&outcome_of(&args)), 2);
4108    }
4109
4110    #[test]
4111    fn missing_required_argument_is_a_usage_error() {
4112        let usage = usage_of(&["hwi", "getxpub"]);
4113
4114        assert!(usage.message.contains("required"), "{}", usage.message);
4115        assert!(usage.message.contains("<PATH>"), "{}", usage.message);
4116        assert!(usage.usage.contains("getxpub"), "{}", usage.usage);
4117    }
4118
4119    #[test]
4120    fn numeric_value_parse_failure_is_a_usage_error() {
4121        let args = ["hwi", "getkeypool", "notanum", "5"];
4122        let error = HwiCli::try_parse_from(args).expect_err("invalid numeric value");
4123
4124        assert_eq!(error.kind(), ErrorKind::ValueValidation);
4125        assert!(!usage_of(&args).usage.is_empty());
4126        assert_eq!(exit_status(&outcome_of(&args)), 2);
4127    }
4128
4129    #[test]
4130    fn invalid_flag_choice_is_a_usage_error() {
4131        let usage = usage_of(&["hwi", "getmasterxpub", "--addr-type", "bogus"]);
4132
4133        assert!(
4134            usage.message.contains("invalid value 'bogus'"),
4135            "{}",
4136            usage.message
4137        );
4138        assert!(usage.usage.contains("Usage:"), "{}", usage.usage);
4139    }
4140
4141    #[test]
4142    fn invalid_chain_choice_is_a_usage_error() {
4143        let usage = usage_of(&["hwi", "--chain", "foo", "enumerate"]);
4144
4145        assert_eq!(
4146            usage.message,
4147            "hwi: error: argument --chain: invalid choice: 'foo'"
4148        );
4149    }
4150
4151    #[test]
4152    fn usage_errors_serialize_python_hwi_json() {
4153        let usage = usage_of(&["hwi", "boguscmd"]);
4154        let json = serde_json::to_string(&HwiError {
4155            error: usage.message,
4156            code: HwiErrorCode::MissingArguments.code(),
4157        })
4158        .expect("serialize usage error");
4159
4160        assert_eq!(
4161            json,
4162            r#"{"error":"hwi: error: argument command: invalid choice: 'boguscmd'","code":-2}"#
4163        );
4164    }
4165
4166    #[test]
4167    fn runtime_errors_keep_their_code_and_exit_zero() {
4168        let outcome = CliOutcome::Response(HwiResponse::Error(HwiError::new(
4169            HwiErrorCode::BadArgument,
4170            "bad argument",
4171        )));
4172
4173        let CliOutcome::Response(HwiResponse::Error(error)) = &outcome else {
4174            panic!("expected runtime error, got {outcome:?}");
4175        };
4176        assert_eq!(error.code, HwiErrorCode::BadArgument.code());
4177        assert_eq!(exit_status(&outcome), 0);
4178    }
4179
4180    #[test]
4181    fn version_prints_to_stdout_and_exits_zero() {
4182        let args = ["hwi", "--version"];
4183        let outcome = outcome_of(&args);
4184        let CliOutcome::Stdout(text) = &outcome else {
4185            panic!("expected stdout outcome for {args:?}, got {outcome:?}");
4186        };
4187        assert!(!text.is_empty());
4188        assert_eq!(exit_status(&outcome), 0);
4189    }
4190
4191    #[test]
4192    fn help_yields_json_error_and_help_text_for_top_level_and_subcommand() {
4193        for args in [
4194            ["hwi", "--help"].as_slice(),
4195            ["hwi", "getxpub", "--help"].as_slice(),
4196        ] {
4197            let outcome = outcome_of(args);
4198            let CliOutcome::Help(help) = &outcome else {
4199                panic!("expected help outcome for {args:?}, got {outcome:?}");
4200            };
4201            assert!(!help.is_empty());
4202            assert_eq!(exit_status(&outcome), 0);
4203        }
4204
4205        let json = serde_json::to_string(&HwiError::new(
4206            HwiErrorCode::HelpText,
4207            "Help text requested",
4208        ))
4209        .expect("serialize help error");
4210        assert_eq!(json, r#"{"error":"Help text requested","code":-17}"#);
4211    }
4212
4213    #[test]
4214    fn successful_parse_yields_a_request_and_exits_zero() {
4215        let args = ["hwi", "--device-type", "ledger", "enumerate"];
4216        let outcome = outcome_of(&args);
4217
4218        assert!(matches!(outcome, CliOutcome::Request(_)));
4219        assert_eq!(exit_status(&outcome), 0);
4220    }
4221
4222    #[test]
4223    fn unsupported_action_without_selector_returns_no_device_type() {
4224        let request = parse_args(["hwi", "wipe"]).expect("unsupported wipe request");
4225        let response = futures::executor::block_on(process_request(request));
4226        let HwiResponse::Error(error) = response else {
4227            panic!("expected HWI error");
4228        };
4229
4230        assert_eq!(error.code, HwiErrorCode::NoDeviceType.code());
4231        assert_eq!(
4232            error.error,
4233            "You must specify a device type or fingerprint for all commands except enumerate"
4234        );
4235    }
4236
4237    #[test]
4238    fn unsupported_action_messages_match_python_hwi() {
4239        assert_eq!(
4240            hwi_unavailable_action_message(DeviceType::Ledger, &HwiUnsupportedDeviceAction::Wipe),
4241            "The Ledger Nano S and X do not support wiping via software"
4242        );
4243        assert_eq!(
4244            hwi_unavailable_action_message(
4245                DeviceType::Jade,
4246                &HwiUnsupportedDeviceAction::TogglePassphrase,
4247            ),
4248            "Blockstream Jade does not support toggling passphrase from the host"
4249        );
4250        assert_eq!(
4251            hwi_unavailable_action_message(
4252                DeviceType::Coldcard,
4253                &HwiUnsupportedDeviceAction::PromptPin,
4254            ),
4255            "The Coldcard does not need a PIN sent from the host"
4256        );
4257    }
4258
4259    #[test]
4260    fn keepkey_backup_error_matches_python_hwi() {
4261        assert_eq!(
4262            hwi_unavailable_action_message(
4263                DeviceType::KeepKey,
4264                &HwiUnsupportedDeviceAction::Backup {
4265                    label: String::new(),
4266                    backup_passphrase: String::new(),
4267                },
4268            ),
4269            "The Keepkey does not support creating a backup via software"
4270        );
4271    }
4272
4273    #[test]
4274    fn ledger_and_coldcard_labels_are_serialized_as_null() {
4275        assert_eq!(
4276            serde_json::to_value(HwiEnumeratedDevice {
4277                device_type: "ledger".to_owned(),
4278                model: "ledger_nano_s".to_owned(),
4279                path: "tcp:localhost:9999".to_owned(),
4280                label: label_for(DeviceType::Ledger, None),
4281                fingerprint: None,
4282                needs_pin_sent: false,
4283                needs_passphrase_sent: false,
4284                warnings: Vec::new(),
4285                error: None,
4286                code: None,
4287            })
4288            .expect("json")["label"],
4289            serde_json::Value::Null
4290        );
4291        assert_eq!(
4292            serde_json::to_value(HwiEnumeratedDevice {
4293                device_type: "coldcard".to_owned(),
4294                model: "coldcard".to_owned(),
4295                path: "/tmp/ckcc-simulator.sock".to_owned(),
4296                label: label_for(DeviceType::Coldcard, None),
4297                fingerprint: None,
4298                needs_pin_sent: false,
4299                needs_passphrase_sent: false,
4300                warnings: Vec::new(),
4301                error: None,
4302                code: None,
4303            })
4304            .expect("json")["label"],
4305            serde_json::Value::Null
4306        );
4307    }
4308
4309    #[test]
4310    fn jade_label_and_missing_fingerprint_are_omitted() {
4311        let json = serde_json::to_value(HwiEnumeratedDevice {
4312            device_type: "jade".to_owned(),
4313            model: "jade".to_owned(),
4314            path: "localhost:30121".to_owned(),
4315            label: label_for(DeviceType::Jade, None),
4316            fingerprint: None,
4317            needs_pin_sent: false,
4318            needs_passphrase_sent: false,
4319            warnings: Vec::new(),
4320            error: Some("connection failed".to_owned()),
4321            code: Some(HwiErrorCode::DeviceConnectionError.code()),
4322        })
4323        .expect("json");
4324
4325        assert!(json.get("label").is_none());
4326        assert!(json.get("fingerprint").is_none());
4327    }
4328
4329    #[test]
4330    fn bitbox_emulator_enumerate_shape_matches_python_hwi() {
4331        let json = serde_json::to_value(HwiEnumeratedDevice {
4332            device_type: "bitbox02".to_owned(),
4333            model: hwi_enumerate_model(DeviceType::BitBox02, "bitbox02_simulator", true, None),
4334            path: hwi_enumerate_path(DeviceType::BitBox02, "tcp:127.0.0.1:15423", true),
4335            label: label_for(DeviceType::BitBox02, None),
4336            fingerprint: None,
4337            needs_pin_sent: false,
4338            needs_passphrase_sent: false,
4339            warnings: Vec::new(),
4340            error: None,
4341            code: None,
4342        })
4343        .expect("json");
4344
4345        assert_eq!(json["model"], "bitbox02_nova_multi");
4346        assert_eq!(json["path"], "127.0.0.1:15423");
4347        assert!(json.get("label").is_none());
4348    }
4349
4350    #[test]
4351    fn keepkey_emulator_enumerate_shape_reports_required_passphrase() {
4352        let json = serde_json::to_value(HwiEnumeratedDevice {
4353            device_type: DeviceType::KeepKey.to_string(),
4354            model: hwi_enumerate_model(DeviceType::KeepKey, "keepkey", true, None),
4355            path: hwi_enumerate_path(DeviceType::KeepKey, "udp:127.0.0.1:11044", true),
4356            label: label_for(DeviceType::KeepKey, None),
4357            fingerprint: None,
4358            needs_pin_sent: false,
4359            needs_passphrase_sent: true,
4360            warnings: Vec::new(),
4361            error: Some(KEEPKEY_PASSPHRASE_REQUIRED.to_owned()),
4362            code: Some(HwiErrorCode::DeviceNotReady.code()),
4363        })
4364        .unwrap();
4365
4366        assert_eq!(
4367            json,
4368            serde_json::json!({
4369                "type": "keepkey",
4370                "model": "keepkey_simulator",
4371                "path": "udp:127.0.0.1:11044",
4372                "label": null,
4373                "needs_pin_sent": false,
4374                "needs_passphrase_sent": true,
4375                "error": KEEPKEY_PASSPHRASE_REQUIRED,
4376                "code": -12,
4377            })
4378        );
4379    }
4380
4381    #[test]
4382    fn getxpub_non_expert_serializes_only_xpub() {
4383        let xpub = sample_xpub();
4384
4385        let json = serde_json::to_value(HwiResponse::GetXpub(get_xpub_response(xpub, false)))
4386            .expect("json");
4387
4388        assert_eq!(json, serde_json::json!({ "xpub": xpub.to_string() }));
4389    }
4390
4391    #[test]
4392    fn signmessage_serializes_only_signature() {
4393        let json = serde_json::to_value(HwiResponse::SignMessage(HwiSignMessageResponse {
4394            signature: "base64-signature".to_owned(),
4395        }))
4396        .expect("json");
4397
4398        assert_eq!(json, serde_json::json!({ "signature": "base64-signature" }));
4399    }
4400
4401    #[test]
4402    fn displayaddress_serializes_only_address() {
4403        let json = serde_json::to_value(HwiResponse::DisplayAddress(HwiDisplayAddressResponse {
4404            address: "tb1qexample".to_owned(),
4405        }))
4406        .expect("json");
4407
4408        assert_eq!(json, serde_json::json!({ "address": "tb1qexample" }));
4409    }
4410
4411    fn wrapped_device_error(error: bhwi::common::Error) -> bhwi_async::HWIDeviceError {
4412        bhwi_async::HWIDeviceError::new(
4413            bhwi_async::Error::<std::io::Error, std::io::Error>::Interpreter(error),
4414        )
4415    }
4416
4417    #[test]
4418    #[cfg(feature = "trezor")]
4419    fn send_pin_action_cancelled_matches_hwi_false_contract() {
4420        use bhwi::common::DeviceCode;
4421        use bhwi::trezor::TrezorError;
4422
4423        let mut errors = vec![wrapped_device_error(bhwi::common::Error::new(
4424            bhwi::common::ErrorKind::AuthenticationRefused,
4425            "authentication refused",
4426        ))];
4427        for code in [4, 6] {
4428            errors.push(bhwi_async::HWIDeviceError::new(
4429                TrezorError::ActionCancelled(code, None),
4430            ));
4431        }
4432        for code in [
4433            DeviceCode::Trezor(4),
4434            DeviceCode::Trezor(6),
4435            DeviceCode::KeepKey(4),
4436            DeviceCode::KeepKey(6),
4437        ] {
4438            errors.push(wrapped_device_error(
4439                bhwi::common::Error::new(bhwi::common::ErrorKind::UserCancelled, "")
4440                    .with_device_code(code),
4441            ));
4442        }
4443        for error in &errors {
4444            assert_eq!(
4445                serde_json::to_value(send_pin_error_response(error)).unwrap(),
4446                serde_json::json!({ "success": false })
4447            );
4448        }
4449    }
4450
4451    #[test]
4452    #[cfg(feature = "trezor")]
4453    fn pin_paths_report_wrong_pin_for_both_brands() {
4454        use bhwi::common::DeviceCode;
4455
4456        for code in [DeviceCode::Trezor(7), DeviceCode::KeepKey(7)] {
4457            let bad_pin = wrapped_device_error(
4458                bhwi::common::Error::new(bhwi::common::ErrorKind::WrongPin, "PIN invalid")
4459                    .with_device_code(code),
4460            );
4461
4462            let error = pin_error(&bad_pin);
4463            assert_eq!(error.code, HwiErrorCode::BadArgument.code(), "{code:?}");
4464            assert_eq!(error.error, "PIN invalid", "{code:?}");
4465
4466            assert_eq!(
4467                serde_json::to_value(send_pin_response(Err(bad_pin))).unwrap(),
4468                serde_json::json!({ "success": false }),
4469                "{code:?}"
4470            );
4471        }
4472    }
4473
4474    #[test]
4475    fn classify_device_error_maps_typed_common_errors() {
4476        use bhwi::common::{DeviceCode, Error as CommonError, ErrorKind};
4477
4478        let classify = |kind, message| {
4479            classify_device_error(&wrapped_device_error(CommonError::new(kind, message)))
4480        };
4481
4482        let cancelled = classify(ErrorKind::UserCancelled, "action canceled by the user");
4483        assert_eq!(cancelled.code, HwiErrorCode::ActionCanceled.code());
4484        assert_eq!(cancelled.error, "action canceled by the user");
4485
4486        let refused = classify(ErrorKind::AuthenticationRefused, "authentication refused");
4487        assert_eq!(refused.code, HwiErrorCode::ActionCanceled.code());
4488
4489        let unavailable = classify(ErrorKind::Unsupported, "unsupported command");
4490        assert_eq!(unavailable.code, HwiErrorCode::UnsupportedCommand.code());
4491
4492        let unsupported = classify(
4493            ErrorKind::UnsupportedDisplayAddress,
4494            "BitBox does not support this address format",
4495        );
4496        assert_eq!(unsupported.code, HwiErrorCode::UnsupportedCommand.code());
4497
4498        let invalid = classify(ErrorKind::InvalidInput, "bad argument");
4499        assert_eq!(invalid.code, HwiErrorCode::BadArgument.code());
4500
4501        let device = classify(
4502            ErrorKind::Rejected,
4503            "Coldcard Error: Unknown multisig wallet",
4504        );
4505        assert_eq!(device.code, HwiErrorCode::BadArgument.code());
4506        assert_eq!(device.error, "Coldcard Error: Unknown multisig wallet");
4507
4508        let fallback = classify(ErrorKind::Serialization, "boom");
4509        assert_eq!(fallback.code, HwiErrorCode::DeviceConnectionError.code());
4510        assert_eq!(fallback.error, "boom");
4511
4512        let trezor_failure = classify_device_error(&wrapped_device_error(
4513            CommonError::new(ErrorKind::Other, "messages.c:232:array overflow")
4514                .with_device_code(DeviceCode::Trezor(3)),
4515        ));
4516        assert_eq!(trezor_failure.code, HwiErrorCode::DeviceFailure.code());
4517    }
4518
4519    #[test]
4520    #[cfg(feature = "keepkey")]
4521    fn keepkey_locked_and_bad_pin_errors_use_python_hwi_codes() {
4522        let locked = classify_device_error(&wrapped_device_error(bhwi::common::Error::from(
4523            bhwi::trezor::TrezorError::Locked(KEEPKEY_LOCKED),
4524        )));
4525        assert_eq!(locked.code, HwiErrorCode::DeviceNotReady.code());
4526        assert_eq!(locked.error, KEEPKEY_LOCKED);
4527
4528        let bad_pin = wrapped_device_error(
4529            bhwi::common::Error::new(bhwi::common::ErrorKind::WrongPin, "bad pin")
4530                .with_device_code(bhwi::common::DeviceCode::KeepKey(7)),
4531        );
4532        let bad_pin = pin_error(&bad_pin);
4533        assert_eq!(bad_pin.code, HwiErrorCode::BadArgument.code());
4534        assert_eq!(bad_pin.error, "bad pin");
4535
4536        for message in [
4537            bhwi::trezor::TrezorError::NO_PIN_NEEDED,
4538            bhwi::trezor::TrezorError::PIN_ALREADY_SENT,
4539        ] {
4540            let error = wrapped_device_error(bhwi::common::Error::new(
4541                bhwi::common::ErrorKind::AlreadyUnlocked,
4542                message,
4543            ));
4544            assert_eq!(
4545                pin_error(&error).code,
4546                HwiErrorCode::DeviceAlreadyUnlocked.code()
4547            );
4548        }
4549    }
4550
4551    #[test]
4552    fn bitbox02_device_codes_follow_upstream_exception_mapping() {
4553        use bhwi::common::{DeviceCode, Error as CommonError, ErrorKind};
4554
4555        for (code, kind, expected) in [
4556            (
4557                101,
4558                ErrorKind::InvalidInput,
4559                HwiErrorCode::UnsupportedCommand,
4560            ),
4561            (
4562                103,
4563                ErrorKind::DeviceFailure,
4564                HwiErrorCode::UnsupportedCommand,
4565            ),
4566            (105, ErrorKind::NotReady, HwiErrorCode::DeviceNotInitialized),
4567            (107, ErrorKind::Duplicate, HwiErrorCode::DeviceFailure),
4568            (104, ErrorKind::UserCancelled, HwiErrorCode::ActionCanceled),
4569        ] {
4570            let err = wrapped_device_error(
4571                CommonError::new(kind, "bitbox device error")
4572                    .with_device_code(DeviceCode::BitBox(code)),
4573            );
4574            assert_eq!(
4575                classify_device_error_for(DeviceType::BitBox02, &err).code,
4576                expected.code(),
4577                "{code}"
4578            );
4579        }
4580
4581        let wiped = wrapped_device_error(CommonError::new(
4582            ErrorKind::AlreadyInitialized,
4583            "The BitBox02 must be wiped before setup.",
4584        ));
4585        assert_eq!(
4586            classify_device_error_for(DeviceType::BitBox02, &wiped).code,
4587            HwiErrorCode::UnsupportedCommand.code()
4588        );
4589
4590        let pairing = wrapped_device_error(CommonError::new(
4591            ErrorKind::AuthenticationRefused,
4592            "authentication refused",
4593        ));
4594        assert_eq!(
4595            classify_device_error_for(DeviceType::BitBox02, &pairing).code,
4596            HwiErrorCode::DeviceFailure.code()
4597        );
4598        assert_eq!(
4599            classify_device_error_for(DeviceType::KeepKey, &pairing).code,
4600            HwiErrorCode::ActionCanceled.code()
4601        );
4602
4603        let refused_while_selecting: bhwi_async::device::SelectError<std::io::Error> =
4604            bhwi_async::device::SelectError::Device(bhwi_async::HWIDeviceError::new(
4605                CommonError::new(ErrorKind::AuthenticationRefused, "authentication refused"),
4606            ));
4607        assert_eq!(
4608            classify_select_error(Some(DeviceType::BitBox02), &refused_while_selecting).code,
4609            HwiErrorCode::DeviceFailure.code()
4610        );
4611    }
4612
4613    #[test]
4614    fn a_locked_device_skipped_during_selection_keeps_the_not_ready_code() {
4615        use bhwi::common::{DeviceCode, Error as CommonError, ErrorKind};
4616        use bhwi_async::device::{NoUsableDevice, SelectError, SkippedDevice};
4617
4618        let locked = CommonError::new(ErrorKind::Locked, "Trezor is locked");
4619        let other = CommonError::new(ErrorKind::Locked, "Ledger device is locked")
4620            .with_device_code(DeviceCode::Ledger(0x5515));
4621        let err: SelectError<std::io::Error> = SelectError::NoUsableDevice(NoUsableDevice {
4622            skipped: vec![
4623                SkippedDevice::new(DeviceType::Ledger, "Ledger", "hid:1", &other),
4624                SkippedDevice::new(DeviceType::Trezor, "Trezor", "hid:2", &locked),
4625            ],
4626        });
4627        let classified = classify_select_error(None, &err);
4628        assert_eq!(classified.code, HwiErrorCode::DeviceNotReady.code());
4629        assert_eq!(classified.error, "Trezor is locked");
4630
4631        let err: SelectError<std::io::Error> = SelectError::NoUsableDevice(NoUsableDevice {
4632            skipped: vec![SkippedDevice::new(
4633                DeviceType::Ledger,
4634                "Ledger",
4635                "hid:1",
4636                &other,
4637            )],
4638        });
4639        assert_eq!(
4640            classify_select_error(None, &err).code,
4641            HwiErrorCode::DeviceConnectionError.code()
4642        );
4643    }
4644
4645    #[test]
4646    fn ledger_cancellations_downgrade_to_unknown_error_code() {
4647        let err = wrapped_device_error(bhwi::common::Error::new(
4648            bhwi::common::ErrorKind::UserCancelled,
4649            "action canceled by the user",
4650        ));
4651        let ledger = classify_device_error_for(DeviceType::Ledger, &err);
4652        assert_eq!(ledger.code, HwiErrorCode::DeviceFailure.code());
4653        let coldcard = classify_device_error_for(DeviceType::Coldcard, &err);
4654        assert_eq!(coldcard.code, HwiErrorCode::ActionCanceled.code());
4655    }
4656
4657    #[test]
4658    fn ledger_status_words_follow_upstream_ledger_bitcoin() {
4659        use bhwi::common::{DeviceCode, Error as CommonError, ErrorKind};
4660
4661        let ledger = |kind, status| {
4662            let err = wrapped_device_error(
4663                CommonError::new(kind, format!("status {status:#06x}"))
4664                    .with_device_code(DeviceCode::Ledger(status)),
4665            );
4666            classify_device_error_for(DeviceType::Ledger, &err).code
4667        };
4668        for (kind, status) in [
4669            (ErrorKind::InvalidInput, 0x6A80),
4670            (ErrorKind::Unsupported, 0x6A82),
4671            (ErrorKind::Protocol, 0x6A86),
4672            (ErrorKind::Protocol, 0x6A87),
4673            (ErrorKind::Unsupported, 0x6D00),
4674            (ErrorKind::Protocol, 0xB007),
4675            (ErrorKind::DeviceFailure, 0xB008),
4676            (ErrorKind::Rejected, 0x6985),
4677            (ErrorKind::Rejected, 0x6982),
4678            (ErrorKind::Other, 0xB000),
4679        ] {
4680            assert_eq!(
4681                ledger(kind, status),
4682                HwiErrorCode::DeviceFailure.code(),
4683                "{status:#06x}"
4684            );
4685        }
4686        for (kind, status) in [
4687            (ErrorKind::NotReady, 0x6A80),
4688            (ErrorKind::NotReady, 0x6E00),
4689            (ErrorKind::Rejected, 0x5501),
4690            (ErrorKind::Rejected, 0x6901),
4691            (ErrorKind::Locked, 0x5515),
4692            (ErrorKind::Other, 0x6FAA),
4693        ] {
4694            assert_eq!(
4695                ledger(kind, status),
4696                HwiErrorCode::DeviceConnectionError.code(),
4697                "{kind:?} {status:#06x}"
4698            );
4699        }
4700        assert_eq!(
4701            ledger(ErrorKind::UserCancelled, 0x6985),
4702            HwiErrorCode::DeviceFailure.code()
4703        );
4704    }
4705
4706    #[test]
4707    fn classify_anyhow_device_error_walks_the_chain() {
4708        let err = anyhow::Error::new(wrapped_device_error(bhwi::common::Error::new(
4709            bhwi::common::ErrorKind::UserCancelled,
4710            "action canceled by the user",
4711        )))
4712        .context("getting master fingerprint");
4713        let classified = classify_anyhow_device_error(&err);
4714        assert_eq!(classified.code, HwiErrorCode::ActionCanceled.code());
4715        assert_eq!(classified.error, "action canceled by the user");
4716    }
4717
4718    #[test]
4719    fn classify_anyhow_device_error_maps_wrapped_missing_command() {
4720        let err = anyhow::Error::new(wrapped_device_error(bhwi::common::Error::new(
4721            bhwi::common::ErrorKind::Unsupported,
4722            "host interaction required",
4723        )))
4724        .context("displaying descriptor address");
4725        let classified = classify_anyhow_device_error(&err);
4726        assert_eq!(classified.code, HwiErrorCode::UnsupportedCommand.code());
4727        assert_eq!(classified.error, "host interaction required");
4728    }
4729
4730    #[test]
4731    fn classify_anyhow_device_error_maps_wrapped_overlong_passphrase() {
4732        let err = anyhow::Error::new(wrapped_device_error(bhwi::common::Error::new(
4733            bhwi::common::ErrorKind::InvalidInput,
4734            "Passphrase too long",
4735        )))
4736        .context("getting master fingerprint");
4737        let classified = classify_anyhow_device_error(&err);
4738        assert_eq!(classified.code, HwiErrorCode::BadArgument.code());
4739        assert_eq!(classified.error, "Passphrase too long");
4740    }
4741
4742    #[test]
4743    #[cfg(feature = "keepkey")]
4744    fn classify_anyhow_device_error_preserves_locked_descriptor_lookup() {
4745        let err = anyhow::Error::new(wrapped_device_error(bhwi::common::Error::from(
4746            bhwi::trezor::TrezorError::Locked(KEEPKEY_LOCKED),
4747        )))
4748        .context("getting descriptor fingerprint");
4749        let classified = classify_anyhow_device_error(&err);
4750        assert_eq!(classified.code, HwiErrorCode::DeviceNotReady.code());
4751        assert_eq!(classified.error, KEEPKEY_LOCKED);
4752    }
4753
4754    #[test]
4755    fn getdescriptors_serializes_receive_and_internal() {
4756        let json = serde_json::to_value(HwiResponse::GetDescriptors(HwiGetDescriptorsResponse {
4757            receive: vec!["wpkh(...)#receive".to_owned()],
4758            internal: vec!["wpkh(...)#internal".to_owned()],
4759        }))
4760        .expect("json");
4761
4762        assert_eq!(
4763            json,
4764            serde_json::json!({
4765                "receive": ["wpkh(...)#receive"],
4766                "internal": ["wpkh(...)#internal"],
4767            })
4768        );
4769    }
4770
4771    #[test]
4772    fn getkeypool_serializes_importdescriptors_shape() {
4773        let json = serde_json::to_value(HwiResponse::GetKeypool(vec![HwiGetKeypoolEntry {
4774            desc: "wpkh(...)#keypool".to_owned(),
4775            range: [0, 10],
4776            timestamp: "now",
4777            internal: false,
4778            keypool: true,
4779            active: true,
4780            watchonly: true,
4781        }]))
4782        .expect("json");
4783
4784        assert_eq!(
4785            json,
4786            serde_json::json!([
4787                {
4788                    "desc": "wpkh(...)#keypool",
4789                    "range": [0, 10],
4790                    "timestamp": "now",
4791                    "internal": false,
4792                    "keypool": true,
4793                    "active": true,
4794                    "watchonly": true,
4795                }
4796            ])
4797        );
4798    }
4799
4800    #[test]
4801    fn hwi_descriptor_string_uses_hardened_h_and_recomputes_checksum() {
4802        let descriptor = Descriptor::<DescriptorPublicKey>::from_str(
4803            "wpkh([f5acc2fd/84'/1'/0']tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT/0/*)",
4804        )
4805        .expect("descriptor");
4806
4807        let descriptor = hwi_descriptor_string(&descriptor).expect("descriptor string");
4808
4809        assert!(descriptor.contains("/84h/1h/0h]"));
4810        assert!(!descriptor.contains('\''));
4811        checksum::verify_checksum(&descriptor).expect("valid checksum");
4812    }
4813
4814    #[test]
4815    fn getkeypool_path_accepts_hwi_ranged_path() {
4816        let fingerprint = Fingerprint::from([0xf5, 0xac, 0xc2, 0xfd]);
4817
4818        let options = keypool_path_descriptor_options(
4819            fingerprint,
4820            "m/84h/1h/0h/0/*",
4821            false,
4822            DescriptorType::Wpkh,
4823            Network::Testnet,
4824        )
4825        .expect("keypool path options");
4826
4827        assert_eq!(options.master_fingerprint, fingerprint);
4828    }
4829
4830    #[test]
4831    fn getkeypool_path_rejects_missing_master_prefix() {
4832        let fingerprint = Fingerprint::from([0xf5, 0xac, 0xc2, 0xfd]);
4833
4834        let err = keypool_path_descriptor_options(
4835            fingerprint,
4836            "84h/1h/0h/0/*",
4837            false,
4838            DescriptorType::Wpkh,
4839            Network::Testnet,
4840        )
4841        .expect_err("missing master prefix");
4842
4843        assert_eq!(err.code, HwiErrorCode::BadArgument.code());
4844        assert_eq!(err.error, "Path must start with m/");
4845    }
4846
4847    #[test]
4848    fn getkeypool_path_rejects_missing_wildcard() {
4849        let fingerprint = Fingerprint::from([0xf5, 0xac, 0xc2, 0xfd]);
4850
4851        let err = keypool_path_descriptor_options(
4852            fingerprint,
4853            "m/84h/1h/0h/0",
4854            false,
4855            DescriptorType::Wpkh,
4856            Network::Testnet,
4857        )
4858        .expect_err("missing wildcard");
4859
4860        assert_eq!(err.code, HwiErrorCode::BadArgument.code());
4861        assert_eq!(err.error, "Path must end with /*");
4862    }
4863
4864    #[test]
4865    fn descriptor_addr_types_match_python_hwi_taproot_capabilities() {
4866        assert_eq!(
4867            hwi_descriptor_addr_types(DeviceType::Ledger, "ledger_nano_s_simulator"),
4868            vec![
4869                HwiAddressType::Legacy,
4870                HwiAddressType::Wit,
4871                HwiAddressType::ShWit,
4872                HwiAddressType::Tap,
4873            ]
4874        );
4875        assert_eq!(
4876            hwi_descriptor_addr_types(DeviceType::Jade, "jade_simulator"),
4877            vec![
4878                HwiAddressType::Legacy,
4879                HwiAddressType::Wit,
4880                HwiAddressType::ShWit,
4881            ]
4882        );
4883        assert_eq!(
4884            hwi_descriptor_addr_types(DeviceType::Coldcard, "coldcard_simulator"),
4885            vec![
4886                HwiAddressType::Legacy,
4887                HwiAddressType::Wit,
4888                HwiAddressType::ShWit,
4889            ]
4890        );
4891        assert_eq!(
4892            hwi_descriptor_addr_types(DeviceType::KeepKey, "keepkey_simulator"),
4893            vec![
4894                HwiAddressType::Legacy,
4895                HwiAddressType::Wit,
4896                HwiAddressType::ShWit,
4897            ]
4898        );
4899        assert!(can_sign_taproot(
4900            DeviceType::Coldcard,
4901            "coldcard_simulator_edge"
4902        ));
4903    }
4904
4905    #[test]
4906    fn getxpub_expert_serializes_python_hwi_field_names() {
4907        let xpub = sample_xpub();
4908
4909        let json =
4910            serde_json::to_value(HwiResponse::GetXpub(get_xpub_response(xpub, true))).unwrap();
4911        let object = json.as_object().expect("expert getxpub object");
4912
4913        assert_eq!(object.len(), 8);
4914        assert_eq!(json["xpub"], xpub.to_string());
4915        assert_eq!(json["testnet"], true);
4916        assert_eq!(json["private"], false);
4917        assert_eq!(json["depth"], xpub.depth);
4918        assert_eq!(
4919            json["parent_fingerprint"],
4920            xpub.parent_fingerprint.to_string()
4921        );
4922        assert_eq!(json["child_num"], u32::from(xpub.child_number));
4923        assert_eq!(json["chaincode"], hex::encode(xpub.chain_code));
4924        assert_eq!(json["pubkey"], hex::encode(xpub.public_key.serialize()));
4925        assert!(!object.contains_key("child_index"));
4926        assert!(!object.contains_key("chain_code"));
4927    }
4928
4929    #[test]
4930    fn master_xpub_path_matches_python_hwi_addr_types() {
4931        for (addr_type, expected) in [
4932            (HwiAddressType::Legacy, "44'/1'/7'"),
4933            (HwiAddressType::ShWit, "49'/1'/7'"),
4934            (HwiAddressType::Wit, "84'/1'/7'"),
4935            (HwiAddressType::Tap, "86'/1'/7'"),
4936        ] {
4937            let path = master_xpub_path(addr_type, Network::Testnet, 7).unwrap();
4938            assert_eq!(path.to_string(), expected);
4939        }
4940    }
4941
4942    #[test]
4943    fn master_xpub_path_uses_mainnet_coin_type_only_for_mainnet() {
4944        assert_eq!(
4945            master_xpub_path(HwiAddressType::Wit, Network::Bitcoin, 0)
4946                .unwrap()
4947                .to_string(),
4948            "84'/0'/0'"
4949        );
4950        for network in [
4951            Network::Testnet,
4952            Network::Testnet4,
4953            Network::Signet,
4954            Network::Regtest,
4955        ] {
4956            assert_eq!(
4957                master_xpub_path(HwiAddressType::Wit, network, 0)
4958                    .unwrap()
4959                    .to_string(),
4960                "84'/1'/0'"
4961            );
4962        }
4963    }
4964
4965    fn sample_xpub() -> Xpub {
4966        Xpub::from_str("tpubDCwYjpDhUdPGP5rS3wgNg13mTrrjBuG8V9VpWbyptX6TRPbNoZVXsoVUSkCjmQ8jJycjuDKBb9eataSymXakTTaGifxR6kmVsfFehH1ZgJT")
4967            .expect("sample xpub")
4968    }
4969}