1use 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#[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#[derive(Debug, Clone, Subcommand)]
98pub enum HwiCliCommand {
99 Enumerate,
101 Getmasterxpub {
103 #[arg(long = "addr-type", value_enum, default_value = "wit")]
105 addr_type: HwiAddressType,
106 #[arg(long, default_value_t = 0)]
108 account: u32,
109 },
110 Signtx {
112 psbt: String,
114 },
115 Signmessage {
117 message: String,
119 path: String,
121 },
122 #[command(group(
124 ArgGroup::new("address_target")
125 .required(true)
126 .args(["path", "desc"])
127 ))]
128 Displayaddress {
129 #[arg(long, conflicts_with = "desc")]
131 path: Option<String>,
132 #[arg(long, conflicts_with = "path")]
134 desc: Option<String>,
135 #[arg(long = "addr-type", value_enum, default_value = "wit")]
137 addr_type: HwiAddressType,
138 },
139 Getxpub {
141 path: String,
143 },
144 Getdescriptors {
146 #[arg(long, default_value_t = 0)]
148 account: u32,
149 },
150 Getkeypool {
152 start: u32,
154 end: u32,
156 #[arg(long, action = ArgAction::SetTrue, conflicts_with = "nokeypool")]
158 keypool: bool,
159 #[arg(long, action = ArgAction::SetTrue)]
161 nokeypool: bool,
162 #[arg(long, action = ArgAction::SetTrue)]
164 internal: bool,
165 #[arg(long = "addr-type", value_enum, conflicts_with = "all")]
167 addr_type: Option<HwiAddressType>,
168 #[arg(long, action = ArgAction::SetTrue)]
170 all: bool,
171 #[arg(long, default_value_t = 0)]
173 account: u32,
174 #[arg(long)]
176 path: Option<String>,
177 },
178 Setup {
180 #[arg(long, short = 'l', default_value = "")]
182 label: String,
183 #[arg(long = "backup_passphrase", short = 'b', default_value = "")]
185 backup_passphrase: String,
186 },
187 Wipe,
189 Restore {
191 #[arg(long = "word_count", short = 'w', default_value_t = 24)]
193 word_count: i32,
194 #[arg(long, short = 'l', default_value = "")]
196 label: String,
197 },
198 Backup {
200 #[arg(long, short = 'l', default_value = "")]
202 label: String,
203 #[arg(long = "backup_passphrase", short = 'b', default_value = "")]
205 backup_passphrase: String,
206 },
207 Promptpin,
209 Sendpin {
211 pin: String,
213 },
214 Togglepassphrase,
216 #[cfg(target_os = "linux")]
218 Installudevrules {
219 #[arg(long, default_value = "/etc/udev/rules.d/")]
221 location: PathBuf,
222 },
223 #[command(external_subcommand)]
225 External(
226 Vec<OsString>,
228 ),
229}
230
231#[derive(Debug, Clone)]
233pub struct HwiRequest {
234 pub selector: HwiSelector,
236 pub command: HwiCommand,
238}
239
240#[derive(Debug, Clone)]
244pub struct HwiSelector {
245 pub network: Network,
247 pub fingerprint: Option<Fingerprint>,
249 pub device_type: Option<String>,
251 pub device_path: Option<String>,
253 pub include_emulators: bool,
255 #[cfg(any(feature = "trezor", feature = "keepkey"))]
257 pub passphrase: Option<HostPassphrase>,
258 pub password_given: bool,
260}
261
262impl HwiSelector {
263 fn device_selector(&self, device_type: Option<DeviceType>) -> DeviceSelector {
264 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
300fn 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
361fn 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#[derive(Debug, Clone, Eq, PartialEq)]
386pub enum HwiCommand {
387 Enumerate,
389 GetMasterXpub {
391 addr_type: HwiAddressType,
393 account: u32,
395 },
396 SignTx {
398 psbt: String,
400 },
401 SignMessage {
403 message: String,
405 path: String,
407 },
408 DisplayAddress(
410 HwiDisplayAddressRequest,
412 ),
413 GetXpub {
415 path: String,
417 expert: bool,
419 },
420 GetDescriptors {
422 account: u32,
424 },
425 GetKeypool {
427 start: u32,
429 end: u32,
431 internal: bool,
433 keypool: bool,
435 account: u32,
437 addr_type: HwiAddressType,
439 all: bool,
441 path: Option<String>,
443 },
444 Backup {
446 label: String,
448 backup_passphrase: String,
450 },
451 Setup {
453 interactive: bool,
455 label: String,
457 backup_passphrase: String,
459 },
460 Wipe,
462 Restore {
464 interactive: bool,
466 word_count: i32,
468 label: String,
470 },
471 TogglePassphrase,
473 PromptPin,
475 SendPin {
477 pin: String,
479 },
480 UnsupportedDeviceAction(
482 HwiUnsupportedDeviceAction,
484 ),
485 #[cfg(target_os = "linux")]
487 InstallUdevRules {
488 location: PathBuf,
490 },
491 Unsupported(
493 String,
495 ),
496}
497
498#[derive(Debug, Clone, Eq, PartialEq)]
500pub enum HwiUnsupportedDeviceAction {
501 Setup {
503 interactive: bool,
505 label: String,
507 backup_passphrase: String,
509 },
510 Wipe,
512 Restore {
514 interactive: bool,
516 word_count: i32,
518 label: String,
520 },
521 Backup {
523 label: String,
525 backup_passphrase: String,
527 },
528 PromptPin,
530 SendPin {
532 pin: String,
534 },
535 TogglePassphrase,
537}
538
539#[derive(Debug, Clone, Eq, PartialEq)]
541pub enum HwiDisplayAddressRequest {
542 Path {
544 path: String,
546 addr_type: HwiAddressType,
548 },
549 Descriptor {
555 descriptor: String,
557 },
558}
559
560#[derive(Debug, Clone, Copy, Eq, PartialEq, ValueEnum)]
562pub enum HwiAddressType {
563 #[value(name = "legacy")]
565 Legacy,
566 #[value(name = "sh_wit")]
568 ShWit,
569 #[value(name = "wit")]
571 Wit,
572 #[value(name = "tap")]
574 Tap,
575}
576
577#[derive(Debug, Clone, Eq, PartialEq, Serialize)]
579pub struct HwiError {
580 pub error: String,
582 pub code: i32,
584}
585
586#[derive(Debug, Clone, Copy, Eq, PartialEq)]
592pub enum HwiErrorCode {
593 NoDeviceType,
595 MissingArguments,
597 UnknownDevice,
599 BadArgument,
601 UnsupportedCommand,
603 DeviceFailure,
605 DeviceConnectionError,
607 NeedToBeRoot,
609 DeviceAlreadyInitialized,
611 DeviceAlreadyUnlocked,
613 DeviceNotReady,
615 DeviceNotInitialized,
617 HelpText,
619 ActionCanceled,
621 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#[derive(Debug, Serialize)]
664pub struct HwiEnumeratedDevice {
665 #[serde(rename = "type")]
667 pub device_type: String,
668 pub model: String,
670 pub path: String,
672 #[serde(skip_serializing_if = "Option::is_none")]
674 pub label: Option<Option<String>>,
675 #[serde(
677 default,
678 serialize_with = "option_fingerprint",
679 skip_serializing_if = "Option::is_none"
680 )]
681 pub fingerprint: Option<Fingerprint>,
682 pub needs_pin_sent: bool,
684 pub needs_passphrase_sent: bool,
686 #[serde(skip_serializing_if = "Vec::is_empty")]
688 pub warnings: Vec<Vec<String>>,
689 #[serde(skip_serializing_if = "Option::is_none")]
691 pub error: Option<String>,
692 #[serde(skip_serializing_if = "Option::is_none")]
694 pub code: Option<i32>,
695}
696
697#[derive(Debug, Serialize)]
703#[serde(untagged)]
704pub enum HwiResponse {
705 Enumerate(
707 Vec<HwiEnumeratedDevice>,
709 ),
710 GetXpub(
712 HwiGetXpubResponse,
714 ),
715 GetDescriptors(
717 HwiGetDescriptorsResponse,
719 ),
720 GetKeypool(
722 Vec<HwiGetKeypoolEntry>,
724 ),
725 SignTx(
727 HwiSignTxResponse,
729 ),
730 SignMessage(
732 HwiSignMessageResponse,
734 ),
735 DisplayAddress(
737 HwiDisplayAddressResponse,
739 ),
740 Success(
742 HwiSuccessResponse,
744 ),
745 Error(
747 HwiError,
749 ),
750}
751
752#[derive(Debug, Serialize)]
754pub struct HwiSuccessResponse {
755 pub success: bool,
757}
758
759#[derive(Debug, Serialize)]
761pub struct HwiSignTxResponse {
762 pub psbt: String,
764 pub signed: bool,
766}
767
768#[derive(Debug, Serialize)]
770pub struct HwiSignMessageResponse {
771 pub signature: String,
773}
774
775#[derive(Debug, Serialize)]
777pub struct HwiDisplayAddressResponse {
778 pub address: String,
780}
781
782#[derive(Debug, Serialize)]
787pub struct HwiGetXpubResponse {
788 pub xpub: String,
790 #[serde(skip_serializing_if = "Option::is_none")]
792 pub testnet: Option<bool>,
793 #[serde(skip_serializing_if = "Option::is_none")]
795 pub private: Option<bool>,
796 #[serde(skip_serializing_if = "Option::is_none")]
798 pub depth: Option<u8>,
799 #[serde(
801 default,
802 serialize_with = "option_fingerprint",
803 skip_serializing_if = "Option::is_none"
804 )]
805 pub parent_fingerprint: Option<Fingerprint>,
806 #[serde(skip_serializing_if = "Option::is_none")]
808 pub child_num: Option<u32>,
809 #[serde(skip_serializing_if = "Option::is_none")]
811 pub chaincode: Option<String>,
812 #[serde(skip_serializing_if = "Option::is_none")]
814 pub pubkey: Option<String>,
815}
816
817#[derive(Debug, Serialize)]
819pub struct HwiGetDescriptorsResponse {
820 pub receive: Vec<String>,
822 pub internal: Vec<String>,
824}
825
826#[derive(Debug, Serialize)]
828pub struct HwiGetKeypoolEntry {
829 pub desc: String,
831 pub range: [u32; 2],
833 pub timestamp: &'static str,
835 pub internal: bool,
837 pub keypool: bool,
839 pub active: bool,
841 pub watchonly: bool,
843}
844
845pub 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
865pub 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
944pub 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 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#[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 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 _ => 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 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 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 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 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
1631pub 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";
1637pub 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 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
1701fn 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
1709fn 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
1715fn 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 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 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 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 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
2180fn 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
2211fn 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 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
2525fn 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
2568fn 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
2585fn 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
2610const 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 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 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 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 ErrorKind::InvalidInput | ErrorKind::Rejected | ErrorKind::WrongNetwork => {
2668 HwiErrorCode::BadArgument
2669 }
2670 ErrorKind::AlreadyInitialized => HwiErrorCode::DeviceAlreadyInitialized,
2671 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
2689fn 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 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 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 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 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 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 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 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 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 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}