1use core::fmt;
2use std::rc::Rc;
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use bhwi::bitcoin::{Network, bip32::Fingerprint};
7use bhwi::common::{self, ErrorKind};
8#[cfg(any(feature = "trezor", feature = "keepkey"))]
9use bhwi::passphrase::HostPassphrase;
10
11use crate::{ErrorKindOf, HWIDevice, HWIDeviceError, Info};
12
13#[derive(Debug, Clone, Copy, Eq, PartialEq)]
15pub enum DeviceType {
16 BitBox02,
18 Coldcard,
20 Jade,
22 KeepKey,
24 Ledger,
26 Specter,
28 Trezor,
30}
31
32impl DeviceType {
33 pub const ALL: [DeviceType; 7] = [
35 DeviceType::BitBox02,
36 DeviceType::Coldcard,
37 DeviceType::Jade,
38 DeviceType::KeepKey,
39 DeviceType::Ledger,
40 DeviceType::Specter,
41 DeviceType::Trezor,
42 ];
43
44 pub fn as_str(self) -> &'static str {
46 match self {
47 DeviceType::BitBox02 => "bitbox02",
48 DeviceType::Coldcard => "coldcard",
49 DeviceType::Jade => "jade",
50 DeviceType::KeepKey => "keepkey",
51 DeviceType::Ledger => "ledger",
52 DeviceType::Specter => "specter",
53 DeviceType::Trezor => "trezor",
54 }
55 }
56}
57
58impl fmt::Display for DeviceType {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str(self.as_str())
61 }
62}
63
64pub fn supports_multisig_display_address(device_type: DeviceType) -> bool {
66 matches!(
67 device_type,
68 DeviceType::Coldcard | DeviceType::Jade | DeviceType::KeepKey | DeviceType::Trezor
69 )
70}
71
72pub fn can_sign_taproot(device_type: DeviceType, model: &str) -> bool {
74 match device_type {
75 DeviceType::BitBox02 => false,
76 DeviceType::Ledger => true,
77 DeviceType::Jade => false,
78 DeviceType::KeepKey => false,
79 DeviceType::Coldcard => model.contains("edge"),
80 DeviceType::Specter => false,
81 DeviceType::Trezor => model != "trezor_one",
82 }
83}
84
85pub fn reports_device_info(device_type: DeviceType) -> bool {
87 matches!(device_type, DeviceType::KeepKey | DeviceType::Trezor)
88}
89
90#[derive(Debug, Clone)]
92pub struct DeviceSelector {
93 pub network: Network,
95 pub fingerprint: Option<Fingerprint>,
97 pub device_type: Option<DeviceType>,
99 pub device_path: Option<String>,
101 pub include_emulators: bool,
103 #[cfg(any(feature = "trezor", feature = "keepkey"))]
105 pub passphrase: Option<HostPassphrase>,
106}
107
108impl Default for DeviceSelector {
109 fn default() -> Self {
110 Self {
111 network: Network::Bitcoin,
112 fingerprint: None,
113 device_type: None,
114 device_path: None,
115 include_emulators: false,
116 #[cfg(any(feature = "trezor", feature = "keepkey"))]
117 passphrase: None,
118 }
119 }
120}
121
122impl DeviceSelector {
123 pub fn matches(&self, device_type: DeviceType, path: &str) -> bool {
127 self.device_type.is_none_or(|target| target == device_type)
128 && self
129 .device_path
130 .as_ref()
131 .is_none_or(|target| target == path)
132 }
133}
134
135pub type PairingCodePrompt = Arc<dyn Fn(&str) + Send + Sync>;
137
138#[derive(Debug, Clone)]
140pub struct DeviceCandidate {
141 pub device_type: DeviceType,
143 pub name: String,
145 pub model: String,
147 pub path: String,
149 pub is_emulated: bool,
151}
152
153pub type HostInteractionFactory = Rc<dyn Fn() -> Box<dyn crate::HostInteraction>>;
155
156#[async_trait(?Send)]
160pub trait DeviceSource {
161 type Error: std::error::Error + ErrorKindOf + 'static;
163
164 async fn list(&self, selector: &DeviceSelector) -> Result<Vec<DeviceCandidate>, Self::Error>;
166
167 async fn open(
171 &self,
172 candidate: &DeviceCandidate,
173 selector: &DeviceSelector,
174 pairing_code: Option<&PairingCodePrompt>,
175 host_interaction: Option<&HostInteractionFactory>,
176 ) -> Result<Device, Self::Error>;
177}
178
179#[derive(Debug, thiserror::Error)]
181pub enum SelectError<E> {
182 #[error(transparent)]
184 Source(
185 E,
187 ),
188
189 #[error(transparent)]
191 Device(
192 #[from]
194 HWIDeviceError,
195 ),
196
197 #[error(transparent)]
199 NoUsableDevice(
200 #[from]
202 NoUsableDevice,
203 ),
204
205 #[cfg(feature = "bitbox")]
207 #[error("{}", crate::bitbox::HOST_PASSPHRASE_REJECTED)]
208 HostPassphraseRejected,
209}
210
211pub struct DeviceManager<S> {
213 pub selector: DeviceSelector,
215 source: S,
216 pairing_code: Option<PairingCodePrompt>,
217 host_interaction: Option<HostInteractionFactory>,
218}
219
220impl<S: DeviceSource> DeviceManager<S> {
221 pub fn new(source: S, selector: DeviceSelector) -> Self {
223 Self {
224 selector,
225 source,
226 pairing_code: None,
227 host_interaction: None,
228 }
229 }
230
231 pub fn with_pairing_code_prompt(mut self, prompt: PairingCodePrompt) -> Self {
233 self.pairing_code = Some(prompt);
234 self
235 }
236
237 pub fn with_host_interaction(mut self, factory: HostInteractionFactory) -> Self {
239 self.host_interaction = Some(factory);
240 self
241 }
242
243 pub async fn list(&self) -> Result<Vec<DeviceCandidate>, S::Error> {
245 self.source.list(&self.selector).await
246 }
247
248 pub async fn open(&self, candidate: &DeviceCandidate) -> Result<Device, S::Error> {
250 self.source
251 .open(
252 candidate,
253 &self.selector,
254 self.pairing_code.as_ref(),
255 self.host_interaction.as_ref(),
256 )
257 .await
258 }
259
260 pub async fn enumerate(&self) -> Result<DeviceScan, S::Error> {
266 let mut scan = DeviceScan::default();
267 for candidate in self.list().await? {
268 match self.open(&candidate).await {
269 Ok(device) => scan.devices.push(device),
270 Err(err) => scan.skipped.push(unopened_candidate(&candidate, &err)),
271 }
272 }
273 Ok(scan)
274 }
275
276 pub async fn select(
285 &self,
286 ) -> Result<(Option<Device>, Vec<SkippedDevice>), SelectError<S::Error>> {
287 let candidates = self.list().await.map_err(SelectError::Source)?;
288 let mut skipped = Vec::new();
289 let mut target_dev = None;
290 for candidate in candidates {
291 let mut d = match self.open(&candidate).await {
292 Ok(device) => device,
293 Err(err) => {
294 skipped.push(unopened_candidate(&candidate, &err));
295 continue;
296 }
297 };
298
299 if let Err(err) = d.device().unlock(self.selector.network).await {
300 if err.kind() == Some(ErrorKind::UserCancelled)
302 || self.selector.fingerprint.is_none()
303 {
304 return Err(err.into());
305 }
306 skipped.push(skipped_candidate(&candidate, &err));
307 continue;
308 }
309
310 let Some(fingerprint) = self.selector.fingerprint else {
311 target_dev = Some(d);
312 break;
313 };
314 match d.fingerprint().await {
315 Ok(found) if found == fingerprint => {
316 target_dev = Some(d);
317 break;
318 }
319 Ok(_) => {}
320 Err(err) => {
321 if err.kind() == Some(ErrorKind::UserCancelled) {
322 return Err(err.into());
323 }
324 skipped.push(skipped_candidate(&candidate, &err));
325 }
326 }
327 }
328 let Some(dev) = target_dev else {
329 return Ok((None, skipped));
330 };
331 #[cfg(all(feature = "bitbox", any(feature = "trezor", feature = "keepkey")))]
332 if self.selector.passphrase.is_some() && dev.device_type() == DeviceType::BitBox02 {
333 return Err(SelectError::HostPassphraseRejected);
334 }
335 Ok((Some(dev), skipped))
336 }
337
338 pub async fn scan(&self) -> Result<DeviceScan, SelectError<S::Error>> {
345 let candidates = self.list().await.map_err(SelectError::Source)?;
346 let mut scan = DeviceScan::default();
347 for candidate in candidates {
348 let mut device = match self.open(&candidate).await {
349 Ok(device) => device,
350 Err(err) => {
351 scan.skipped.push(unopened_candidate(&candidate, &err));
352 continue;
353 }
354 };
355 match probe(&mut device, self.selector.network).await {
356 Ok(()) => scan.devices.push(device),
357 Err(err) if err.kind() == Some(ErrorKind::UserCancelled) => return Err(err.into()),
358 Err(err) => scan.skipped.push(skipped_candidate(&candidate, &err)),
359 }
360 }
361 Ok(scan)
362 }
363
364 pub async fn get_device_with_fingerprint(
369 &self,
370 ) -> Result<Option<Device>, SelectError<S::Error>> {
371 let (device, skipped) = self.select().await?;
372 match device {
373 Some(device) => Ok(Some(device)),
374 None => Ok(no_device(skipped)?),
375 }
376 }
377
378 pub async fn get_device_without_contacting(
384 &self,
385 ) -> Result<Option<Device>, SelectError<S::Error>> {
386 let scan = self.enumerate().await.map_err(SelectError::Source)?;
387 match scan.devices.into_iter().next() {
388 Some(device) => Ok(Some(device)),
389 None => Ok(no_device(scan.skipped)?),
390 }
391 }
392}
393
394#[derive(Debug)]
396pub struct NoUsableDevice {
397 pub skipped: Vec<SkippedDevice>,
399}
400
401impl fmt::Display for NoUsableDevice {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 let reasons = self
404 .skipped
405 .iter()
406 .map(|s| format!("{} at {}: {}", s.device_type, s.path, s.error))
407 .collect::<Vec<_>>()
408 .join("; ");
409 write!(f, "no usable device found, could not open: {reasons}")
410 }
411}
412
413impl std::error::Error for NoUsableDevice {}
414
415fn skipped_candidate(
416 candidate: &DeviceCandidate,
417 error: &(dyn std::error::Error + 'static),
418) -> SkippedDevice {
419 SkippedDevice::new(
420 candidate.device_type,
421 &candidate.model,
422 &candidate.path,
423 error,
424 )
425}
426
427fn unopened_candidate<E>(candidate: &DeviceCandidate, error: &E) -> SkippedDevice
428where
429 E: std::error::Error + ErrorKindOf + 'static,
430{
431 SkippedDevice {
432 device_type: candidate.device_type,
433 model: candidate.model.clone(),
434 path: candidate.path.clone(),
435 error: classify(error, Some(error.error_kind())),
436 }
437}
438
439async fn probe(device: &mut Device, network: Network) -> Result<(), HWIDeviceError> {
440 device.device().unlock(network).await?;
442 let info = device.info().await?;
443 if info.initialized != Some(false) {
444 device.fingerprint().await?;
445 }
446 Ok(())
447}
448
449pub fn no_device(skipped: Vec<SkippedDevice>) -> Result<Option<Device>, NoUsableDevice> {
451 if skipped.is_empty() {
452 return Ok(None);
453 }
454 Err(NoUsableDevice { skipped })
455}
456
457pub fn is_user_cancelled(err: &(dyn std::error::Error + 'static)) -> bool {
459 classify_error(err).kind() == ErrorKind::UserCancelled
460}
461
462pub fn classify_error(err: &(dyn std::error::Error + 'static)) -> common::Error {
464 classify(err, None)
465}
466
467fn classify(err: &(dyn std::error::Error + 'static), kind: Option<ErrorKind>) -> common::Error {
468 let message = err.to_string();
469 let mut kind = kind;
470 let mut source = Some(err);
471 while let Some(current) = source {
472 if let Some(error) = current.downcast_ref::<common::Error>() {
473 return reworded(error, message);
474 }
475 kind = kind.or_else(|| {
476 current
477 .downcast_ref::<HWIDeviceError>()
478 .and_then(HWIDeviceError::kind)
479 });
480 source = current.source();
481 }
482 common::Error::new(kind.unwrap_or(ErrorKind::Other), message)
483}
484
485fn reworded(error: &common::Error, message: String) -> common::Error {
486 if error.to_string() == message {
487 return error.clone();
488 }
489 let inner = error.to_string();
490 let message = if message.contains(&inner) {
491 message
492 .replace(&inner, error.message())
493 .trim_end_matches(": ")
494 .to_owned()
495 } else if message.contains(error.message()) {
496 message
497 } else {
498 format!("{message}: {}", error.message())
499 };
500 let mut outer = common::Error::new(error.kind(), message);
501 if let Some(code) = error.device_code() {
502 outer = outer.with_device_code(code);
503 }
504 if let Some(data) = error.data() {
505 outer = outer.with_data(data.to_vec());
506 }
507 outer
508}
509
510#[derive(Debug, Clone)]
512pub struct SkippedDevice {
513 pub device_type: DeviceType,
515 pub model: String,
517 pub path: String,
519 pub error: common::Error,
521}
522
523pub enum ScanEntry {
525 Found(
527 Device,
529 ),
530 Skipped(
532 SkippedDevice,
534 ),
535}
536
537impl SkippedDevice {
538 pub fn new(
540 device_type: DeviceType,
541 model: impl Into<String>,
542 path: impl Into<String>,
543 error: &(dyn std::error::Error + 'static),
544 ) -> Self {
545 Self {
546 device_type,
547 model: model.into(),
548 path: path.into(),
549 error: classify_error(error),
550 }
551 }
552}
553
554impl ScanEntry {
555 pub fn skipped(
557 device_type: DeviceType,
558 model: impl Into<String>,
559 path: impl Into<String>,
560 error: &(dyn std::error::Error + 'static),
561 ) -> Self {
562 Self::Skipped(SkippedDevice::new(device_type, model, path, error))
563 }
564}
565
566#[derive(Default)]
568pub struct DeviceScan {
569 pub devices: Vec<Device>,
571 pub skipped: Vec<SkippedDevice>,
573}
574
575impl Extend<ScanEntry> for DeviceScan {
576 fn extend<I: IntoIterator<Item = ScanEntry>>(&mut self, iter: I) {
577 for entry in iter {
578 match entry {
579 ScanEntry::Found(device) => self.devices.push(device),
580 ScanEntry::Skipped(skipped) => self.skipped.push(skipped),
581 }
582 }
583 }
584}
585
586#[derive(Debug, Clone)]
588pub struct NetworkMismatch {
589 pub expected: Network,
591 pub reported: Vec<Network>,
593}
594
595pub fn networks_string(networks: &[Network]) -> String {
597 networks
598 .iter()
599 .map(|n| n.to_string())
600 .collect::<Vec<_>>()
601 .join(", ")
602}
603
604pub struct Device {
609 name: String,
610 device_type: DeviceType,
611 path: String,
612 model: String,
613 device: Box<dyn HWIDevice>,
614 is_emulated: bool,
615 fingerprint: Option<Fingerprint>,
616 info: Option<Info>,
617}
618
619impl Device {
620 pub fn new(
622 name: &str,
623 device_type: DeviceType,
624 path: impl Into<String>,
625 model: impl Into<String>,
626 device: Box<dyn HWIDevice>,
627 is_emulated: bool,
628 ) -> Self {
629 Self {
630 name: name.into(),
631 device_type,
632 path: path.into(),
633 model: model.into(),
634 device,
635 is_emulated,
636 fingerprint: None,
637 info: None,
638 }
639 }
640
641 pub fn name(&self) -> &str {
643 &self.name
644 }
645
646 pub fn device_type(&self) -> DeviceType {
648 self.device_type
649 }
650
651 pub fn path(&self) -> &str {
653 &self.path
654 }
655
656 pub fn model(&self) -> &str {
658 &self.model
659 }
660
661 pub fn device(&mut self) -> &mut Box<dyn HWIDevice> {
663 &mut self.device
664 }
665
666 pub fn is_emulated(&self) -> bool {
668 self.is_emulated
669 }
670
671 pub fn cached_fingerprint(&self) -> Option<Fingerprint> {
673 self.fingerprint
674 }
675
676 pub fn cached_info(&self) -> Option<&Info> {
678 self.info.as_ref()
679 }
680
681 pub async fn fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
683 if let Some(fingerprint) = self.fingerprint {
684 Ok(fingerprint)
685 } else {
686 let fingerprint = self.device.get_master_fingerprint().await?;
687 self.fingerprint = Some(fingerprint);
688 Ok(fingerprint)
689 }
690 }
691
692 pub async fn network_mismatch(
696 &mut self,
697 expected: Network,
698 ) -> Result<Option<NetworkMismatch>, HWIDeviceError> {
699 let info = self.info().await?;
700 if info.networks.is_empty() || info.networks.contains(&expected) {
701 return Ok(None);
702 }
703 Ok(Some(NetworkMismatch {
704 expected,
705 reported: info.networks,
706 }))
707 }
708
709 pub async fn info(&mut self) -> Result<Info, HWIDeviceError> {
714 if let Some(ref info) = self.info {
715 Ok(info.clone())
716 } else if self.device_type == DeviceType::Specter {
717 let info = Info {
719 version: "unavailable".into(),
720 ..Info::default()
721 };
722 self.info = Some(info.clone());
723 Ok(info)
724 } else {
725 let info = self.device.get_info().await?;
726 self.info = Some(info.clone());
727 Ok(info)
728 }
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use bhwi::bitcoin::{
735 bip32::{DerivationPath, Xpub},
736 psbt::Psbt,
737 secp256k1::ecdsa::Signature,
738 };
739 use futures::executor::block_on;
740
741 use super::*;
742 use crate::{
743 DeviceBackup, DeviceContext, DisplayAddress, RestoreOptions, SetupOptions,
744 WalletRegistration,
745 };
746
747 struct Wallet {
749 fingerprint: Fingerprint,
750 refuses_unlock: bool,
751 }
752
753 #[async_trait(?Send)]
754 impl HWIDevice for Wallet {
755 async fn backup_device(&mut self) -> Result<DeviceBackup, HWIDeviceError> {
756 unimplemented!()
757 }
758 async fn setup_device(
759 &mut self,
760 _: SetupOptions,
761 _: Option<DeviceContext>,
762 ) -> Result<bool, HWIDeviceError> {
763 unimplemented!()
764 }
765 async fn wipe_device(&mut self) -> Result<bool, HWIDeviceError> {
766 unimplemented!()
767 }
768 async fn restore_device(
769 &mut self,
770 _: RestoreOptions,
771 _: Option<DeviceContext>,
772 ) -> Result<bool, HWIDeviceError> {
773 unimplemented!()
774 }
775 async fn toggle_passphrase(&mut self) -> Result<bool, HWIDeviceError> {
776 unimplemented!()
777 }
778 async fn prompt_pin(&mut self) -> Result<bool, HWIDeviceError> {
779 unimplemented!()
780 }
781 async fn send_pin(&mut self, _: Option<DeviceContext>) -> Result<bool, HWIDeviceError> {
782 unimplemented!()
783 }
784 async fn unlock(&mut self, _: Network) -> Result<(), HWIDeviceError> {
785 if self.refuses_unlock {
786 let refusal: Box<dyn std::error::Error + Send + Sync> =
787 "authentication refused".into();
788 return Err(refusal.into());
789 }
790 Ok(())
791 }
792 async fn get_info(&mut self) -> Result<Info, HWIDeviceError> {
793 unimplemented!()
794 }
795 async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
796 Ok(self.fingerprint)
797 }
798 async fn get_extended_pubkey(
799 &mut self,
800 _: DerivationPath,
801 _: bool,
802 ) -> Result<Xpub, HWIDeviceError> {
803 unimplemented!()
804 }
805 async fn sign_message(
806 &mut self,
807 _: &[u8],
808 _: DerivationPath,
809 ) -> Result<(u8, Signature), HWIDeviceError> {
810 unimplemented!()
811 }
812 async fn display_address(
813 &mut self,
814 _: DisplayAddress,
815 _: Option<DeviceContext>,
816 ) -> Result<String, HWIDeviceError> {
817 unimplemented!()
818 }
819 async fn register_wallet(
820 &mut self,
821 _: &str,
822 _: &str,
823 ) -> Result<WalletRegistration, HWIDeviceError> {
824 unimplemented!()
825 }
826 async fn sign_tx(
827 &mut self,
828 _: Psbt,
829 _: Option<DeviceContext>,
830 ) -> Result<Psbt, HWIDeviceError> {
831 unimplemented!()
832 }
833 }
834
835 const REFUSING: [u8; 4] = [0xaa; 4];
836 const UNLOCKED: [u8; 4] = [0xbb; 4];
837
838 #[derive(Debug, thiserror::Error)]
839 #[error("device went away")]
840 struct Unplugged;
841
842 impl ErrorKindOf for Unplugged {
843 fn error_kind(&self) -> ErrorKind {
844 ErrorKind::Disconnected
845 }
846 }
847
848 struct Wallets(DeviceType);
850
851 #[async_trait(?Send)]
852 impl DeviceSource for Wallets {
853 type Error = Unplugged;
854
855 async fn list(&self, _: &DeviceSelector) -> Result<Vec<DeviceCandidate>, Self::Error> {
856 Ok(["refusing", "unlocked"]
857 .into_iter()
858 .map(|path| DeviceCandidate {
859 device_type: self.0,
860 name: "wallet".to_owned(),
861 model: "wallet".to_owned(),
862 path: path.to_owned(),
863 is_emulated: true,
864 })
865 .collect())
866 }
867
868 async fn open(
869 &self,
870 candidate: &DeviceCandidate,
871 _: &DeviceSelector,
872 _: Option<&PairingCodePrompt>,
873 _: Option<&HostInteractionFactory>,
874 ) -> Result<Device, Self::Error> {
875 let refuses_unlock = candidate.path == "refusing";
876 let wallet = Wallet {
877 fingerprint: Fingerprint::from(if refuses_unlock { REFUSING } else { UNLOCKED }),
878 refuses_unlock,
879 };
880 Ok(Device::new(
881 &candidate.name,
882 candidate.device_type,
883 &candidate.path,
884 &candidate.model,
885 Box::new(wallet),
886 candidate.is_emulated,
887 ))
888 }
889 }
890
891 fn manager(fingerprint: Option<Fingerprint>) -> DeviceManager<Wallets> {
892 let selector = DeviceSelector {
893 fingerprint,
894 ..DeviceSelector::default()
895 };
896 DeviceManager::new(Wallets(DeviceType::Trezor), selector)
897 }
898
899 #[cfg(all(feature = "bitbox", any(feature = "trezor", feature = "keepkey")))]
900 #[test]
901 fn a_bitbox02_refuses_any_supplied_passphrase() {
902 for passphrase in [Some("secret"), Some(""), None] {
903 let selector = DeviceSelector {
904 fingerprint: Some(Fingerprint::from(UNLOCKED)),
905 passphrase: passphrase.map(|p| HostPassphrase::new(p.to_owned())),
906 ..DeviceSelector::default()
907 };
908 let result =
909 block_on(DeviceManager::new(Wallets(DeviceType::BitBox02), selector).select());
910 assert_eq!(
911 matches!(result, Err(SelectError::HostPassphraseRejected)),
912 passphrase.is_some()
913 );
914 }
915 }
916
917 #[test]
918 fn a_refused_unlock_ends_selection_when_no_fingerprint_was_named() {
919 let err = block_on(manager(None).select())
920 .err()
921 .expect("the refusal is reported");
922 assert!(matches!(err, SelectError::Device(_)));
923 }
924
925 #[test]
926 fn a_refused_unlock_is_skipped_while_looking_for_a_named_fingerprint() {
927 let unlocked = Fingerprint::from(UNLOCKED);
928 let (device, skipped) =
929 block_on(manager(Some(unlocked)).select()).expect("the named wallet is found");
930 assert_eq!(
931 device.expect("a wallet").cached_fingerprint(),
932 Some(unlocked)
933 );
934 assert_eq!(skipped.len(), 1);
935 }
936
937 #[test]
938 fn taproot_support_matches_python_hwi() {
939 assert!(can_sign_taproot(
940 DeviceType::Ledger,
941 "ledger_nano_s_simulator"
942 ));
943 assert!(!can_sign_taproot(
944 DeviceType::BitBox02,
945 "bitbox02_simulator"
946 ));
947 assert!(!can_sign_taproot(DeviceType::Jade, "jade_simulator"));
948 assert!(can_sign_taproot(
949 DeviceType::Coldcard,
950 "coldcard_simulator_edge"
951 ));
952 assert!(!can_sign_taproot(
953 DeviceType::Coldcard,
954 "coldcard_simulator"
955 ));
956 assert!(can_sign_taproot(DeviceType::Trezor, "trezor_t"));
957 assert!(!can_sign_taproot(DeviceType::Trezor, "trezor_one"));
958 }
959
960 #[test]
961 fn an_interpreter_error_reads_as_its_cause() {
962 let wrapped: crate::Error<std::io::Error, std::io::Error> = crate::Error::Interpreter(
963 bhwi::common::Error::new(ErrorKind::InvalidInput, "Passphrase too long"),
964 );
965 assert_eq!(wrapped.to_string(), "[InvalidInput] Passphrase too long");
966 assert_eq!(
967 std::error::Error::source(&wrapped)
968 .expect("the cause is kept")
969 .to_string(),
970 "[InvalidInput] Passphrase too long"
971 );
972
973 let skipped = SkippedDevice::new(DeviceType::KeepKey, "keepkey", "udp:11044", &wrapped);
974 assert_eq!(
975 skipped.error,
976 bhwi::common::Error::new(ErrorKind::InvalidInput, "Passphrase too long")
977 );
978 assert_eq!(
979 skipped.error.to_string(),
980 "[InvalidInput] Passphrase too long"
981 );
982 }
983
984 #[test]
985 fn a_transport_error_keeps_the_layer_that_carries_its_meaning() {
986 let wrapped = HWIDeviceError::with_kind(
987 crate::Error::<std::io::Error, std::io::Error>::Transport {
988 kind: ErrorKind::Transport,
989 error: std::io::Error::other("Broken pipe"),
990 },
991 ErrorKind::Transport,
992 );
993
994 let skipped = SkippedDevice::new(DeviceType::KeepKey, "keepkey", "udp:11044", &wrapped);
995 assert_eq!(skipped.error.kind(), ErrorKind::Transport);
996 assert_eq!(
997 skipped.error.to_string(),
998 "[Transport] transport error: Broken pipe"
999 );
1000 }
1001
1002 #[cfg(feature = "trezor")]
1003 struct Replies(std::collections::VecDeque<Result<Vec<u8>, std::io::Error>>);
1004
1005 #[cfg(feature = "trezor")]
1006 impl Replies {
1007 fn new(replies: impl IntoIterator<Item = Result<Vec<u8>, std::io::Error>>) -> Self {
1008 Self(replies.into_iter().collect())
1009 }
1010 }
1011
1012 #[cfg(feature = "trezor")]
1013 #[async_trait(?Send)]
1014 impl crate::Transport for Replies {
1015 type Error = std::io::Error;
1016
1017 async fn exchange(&mut self, _: &[u8], _: bool) -> Result<Vec<u8>, Self::Error> {
1018 self.0.pop_front().expect("a scripted reply")
1019 }
1020
1021 fn error_kind(&self, error: &Self::Error) -> ErrorKind {
1022 crate::transport::io_error_kind(error)
1023 }
1024 }
1025
1026 #[cfg(feature = "trezor")]
1027 fn raised<F>(error: &HWIDeviceError) -> common::Error
1028 where
1029 F: std::error::Error + 'static,
1030 {
1031 match std::error::Error::source(error)
1032 .and_then(|source| source.downcast_ref::<crate::Error<std::io::Error, F>>())
1033 {
1034 Some(crate::Error::Interpreter(error)) => error.clone(),
1035 other => panic!("expected an interpreter error, got {other:?}"),
1036 }
1037 }
1038
1039 #[cfg(feature = "trezor")]
1040 fn assert_kept_whole(error: &HWIDeviceError, raised: &common::Error) {
1041 assert_eq!(classify_error(error), *raised);
1042 let skipped = SkippedDevice::new(DeviceType::Trezor, "trezor_one", "udp:21324", error);
1043 assert_eq!(skipped.error, *raised);
1044 assert_eq!(skipped.error.to_string(), error.to_string());
1045 }
1046
1047 #[cfg(feature = "trezor")]
1048 fn trezor_reply(
1049 message_type: bhwi::trezor::api::MessageType,
1050 payload: &[u8],
1051 ) -> Result<Vec<u8>, std::io::Error> {
1052 Ok(bhwi::trezor::api::frame(message_type as u16, payload))
1053 }
1054
1055 #[cfg(feature = "trezor")]
1056 #[test]
1057 fn a_trezor_failure_keeps_its_kind_code_and_message() {
1058 use bhwi::trezor::{TrezorError, api::MessageType};
1059
1060 let mut failure = vec![0x08, 0x07, 0x12, 0x0b];
1062 failure.extend_from_slice(b"PIN invalid");
1063 let mut trezor =
1064 crate::Trezor::new(Replies::new([trezor_reply(MessageType::Failure, &failure)]));
1065
1066 let error = block_on(HWIDevice::get_master_fingerprint(&mut trezor))
1067 .expect_err("the device reports a failure");
1068
1069 let raised = raised::<TrezorError>(&error);
1070 assert_eq!(raised.kind(), ErrorKind::WrongPin);
1071 assert_eq!(raised.device_code(), Some(common::DeviceCode::Trezor(7)));
1072 assert_eq!(raised.message(), "PIN invalid");
1073 assert_kept_whole(&error, &raised);
1074
1075 #[derive(Debug, thiserror::Error)]
1076 #[error("getting fingerprint failed")]
1077 struct Context(#[source] crate::HWIDeviceError);
1078 let classified = classify_error(&Context(error));
1079 assert_eq!(classified.kind(), ErrorKind::WrongPin);
1080 assert_eq!(
1081 classified.message(),
1082 "getting fingerprint failed: PIN invalid"
1083 );
1084
1085 #[derive(Debug, thiserror::Error)]
1086 #[error("getting fingerprint failed: {0}")]
1087 struct Quoting(#[source] common::Error);
1088 let quoted = classify_error(&Quoting(common::Error::new(
1089 ErrorKind::WrongPin,
1090 "PIN invalid",
1091 )));
1092 assert_eq!(
1093 quoted.to_string(),
1094 "[WrongPin] getting fingerprint failed: PIN invalid"
1095 );
1096 }
1097
1098 #[cfg(feature = "trezor")]
1099 #[test]
1100 fn a_disconnect_keeps_the_kind_recorded_by_the_device_error() {
1101 let mut trezor = crate::Trezor::new(Replies::new([Err(std::io::Error::new(
1102 std::io::ErrorKind::BrokenPipe,
1103 "device went away",
1104 ))]));
1105
1106 let error = block_on(HWIDevice::get_master_fingerprint(&mut trezor))
1107 .expect_err("the transport fails");
1108
1109 let classified = classify_error(&error);
1110 assert_eq!(classified.kind(), ErrorKind::Disconnected);
1111 assert_eq!(classified.device_code(), None);
1112 assert_eq!(classified.data(), None);
1113 assert_eq!(
1114 classified.to_string(),
1115 "[Disconnected] transport error: device went away"
1116 );
1117 let skipped = SkippedDevice::new(DeviceType::Trezor, "trezor_one", "udp:21324", &error);
1118 assert_eq!(skipped.error, classified);
1119 assert!(!is_user_cancelled(&error));
1120 }
1121
1122 #[cfg(feature = "trezor")]
1123 struct CancellingTrezor;
1124
1125 #[cfg(feature = "trezor")]
1126 #[async_trait(?Send)]
1127 impl DeviceSource for CancellingTrezor {
1128 type Error = Unplugged;
1129
1130 async fn list(&self, _: &DeviceSelector) -> Result<Vec<DeviceCandidate>, Self::Error> {
1131 Ok(vec![DeviceCandidate {
1132 device_type: DeviceType::Trezor,
1133 name: "trezor".to_owned(),
1134 model: "trezor_one".to_owned(),
1135 path: "udp:21324".to_owned(),
1136 is_emulated: true,
1137 }])
1138 }
1139
1140 async fn open(
1141 &self,
1142 candidate: &DeviceCandidate,
1143 _: &DeviceSelector,
1144 _: Option<&PairingCodePrompt>,
1145 _: Option<&HostInteractionFactory>,
1146 ) -> Result<Device, Self::Error> {
1147 use bhwi::trezor::api::MessageType;
1148 let mut failure = vec![0x08, 0x04, 0x12, 0x18];
1150 failure.extend_from_slice(b"Action cancelled by user");
1151 let replies = Replies::new([
1152 trezor_reply(MessageType::Features, &[]),
1153 trezor_reply(MessageType::Failure, &failure),
1154 ]);
1155 Ok(Device::new(
1156 &candidate.name,
1157 candidate.device_type,
1158 &candidate.path,
1159 &candidate.model,
1160 Box::new(crate::Trezor::new(replies)),
1161 candidate.is_emulated,
1162 ))
1163 }
1164 }
1165
1166 #[cfg(feature = "trezor")]
1167 #[test]
1168 fn a_cancel_on_the_device_ends_selection_instead_of_skipping_it() {
1169 let selector = DeviceSelector {
1170 fingerprint: Some(Fingerprint::from(UNLOCKED)),
1171 ..DeviceSelector::default()
1172 };
1173 let err = block_on(DeviceManager::new(CancellingTrezor, selector).select())
1174 .err()
1175 .expect("the cancel is reported");
1176
1177 let SelectError::Device(err) = err else {
1178 panic!("expected the device error, got {err:?}");
1179 };
1180 assert_eq!(err.kind(), Some(ErrorKind::UserCancelled));
1181 let raised = raised::<bhwi::trezor::TrezorError>(&err);
1182 assert_eq!(raised.device_code(), Some(common::DeviceCode::Trezor(4)));
1183
1184 let built = crate::HWIDeviceError::new(common::Error::new(
1185 ErrorKind::UserCancelled,
1186 "action canceled by the user",
1187 ));
1188 assert_eq!(built.kind(), Some(ErrorKind::UserCancelled));
1189 let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(common::Error::new(
1190 ErrorKind::UserCancelled,
1191 "action canceled by the user",
1192 ));
1193 let converted = crate::HWIDeviceError::from(boxed);
1194 assert_eq!(converted.kind(), Some(ErrorKind::UserCancelled));
1195 }
1196
1197 #[test]
1198 fn multisig_display_address_needs_a_descriptor_capable_device() {
1199 for device_type in DeviceType::ALL {
1200 assert_eq!(
1201 supports_multisig_display_address(device_type),
1202 matches!(
1203 device_type,
1204 DeviceType::Coldcard
1205 | DeviceType::Jade
1206 | DeviceType::KeepKey
1207 | DeviceType::Trezor
1208 ),
1209 "{device_type}"
1210 );
1211 }
1212 }
1213}