1use std::{
7 collections::BTreeMap,
8 fmt::Debug,
9 marker::PhantomData,
10 sync::{
11 atomic::{AtomicBool, AtomicUsize, Ordering},
12 Arc, Mutex,
13 },
14 time::Duration,
15};
16
17use crate::{
18 bitbox::{api::runtime, BitBox02, PairingBitbox02},
19 coldcard,
20 jade::{self, api::GetInfoResponse, Jade, SerialTransport},
21 ledger, specter, AddressScript, DeviceKind, Error as HWIError, Version, HWI,
22};
23use bitbox_api::runtime::TokioRuntime;
24use bitcoin::{
25 bip32::{DerivationPath, Fingerprint, Xpub},
26 psbt::Psbt,
27 Network,
28};
29use crossbeam::channel;
30use hidapi::{DeviceInfo, HidApi};
31use ledger_transport_hidapi::TransportNativeHID;
32use serde::{Deserialize, Serialize};
33use tokio::task::JoinHandle;
34
35#[cfg(feature = "bitbox")]
36use crate::bitbox::{ConfigError, NoiseConfig, NoiseConfigData};
37
38#[cfg(feature = "bitbox")]
41struct ArcNoiseConfig(Arc<dyn NoiseConfig>);
42
43#[cfg(feature = "bitbox")]
44impl bitbox_api::Threading for ArcNoiseConfig {}
45
46#[cfg(feature = "bitbox")]
47impl NoiseConfig for ArcNoiseConfig {
48 fn read_config(&self) -> Result<NoiseConfigData, ConfigError> {
49 self.0.read_config()
50 }
51 fn store_config(&self, data: &NoiseConfigData) -> Result<(), ConfigError> {
52 self.0.store_config(data)
53 }
54}
55
56#[derive(Debug, Clone)]
57pub enum UnsupportedReason {
58 Version {
59 minimal_supported_version: &'static str,
60 },
61 Method(&'static str),
62 NotPartOfWallet(Fingerprint),
63 WrongNetwork,
64 AppIsNotOpen,
66}
67
68pub enum LockedDevice {
69 BitBox02(Box<PairingBitbox02<runtime::TokioRuntime>>),
70 Jade(Jade<jade::SerialTransport>),
72}
73
74impl Debug for LockedDevice {
75 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76 match self {
77 Self::BitBox02(_) => f.debug_tuple("LockedDevice::BitBox02").finish(),
78 Self::Jade(_) => f.debug_tuple("LockedDevice::Jade").finish(),
79 }
80 }
81}
82
83#[derive(Debug, Clone)]
84pub struct SupportedDevice<Message, Id = ()>
85where
86 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
87 Id: Send + Clone + 'static,
88{
89 id: String,
90 device: Arc<dyn HWI + Sync + Send>,
91 kind: DeviceKind,
92 fingerprint: Fingerprint,
93 version: Option<Version>,
94 rt: tokio::runtime::Handle,
95 sender: channel::Sender<Message>,
96 _phantom: PhantomData<Id>,
97}
98
99impl<Message, Id> SupportedDevice<Message, Id>
100where
101 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
102 Id: Send + Clone + 'static,
103{
104 pub fn device(&self) -> &Arc<dyn HWI + Sync + Send> {
105 &self.device
106 }
107
108 pub fn version(&self) -> Option<&Version> {
109 self.version.as_ref()
110 }
111
112 pub fn fingerprint(&self) -> &Fingerprint {
113 &self.fingerprint
114 }
115
116 pub fn kind(&self) -> &DeviceKind {
117 &self.kind
118 }
119
120 pub fn get_extended_pubkey(&self, id: Id, path: &DerivationPath) {
121 let path = path.clone();
122 let sender = self.sender.clone();
123 let fg = self.fingerprint;
124 let device = self.device.clone();
125 tracing::debug!(
126 "SupportedDevice[{}]::get_extended_pubkey: spawning task, path={:?}",
127 fg,
128 path
129 );
130 self.rt.spawn(async move {
131 match (*device).get_extended_pubkey(&path).await {
132 Ok(xpub) => {
133 tracing::debug!(
134 "SupportedDevice[{}]::get_extended_pubkey: success, xpub={}",
135 fg,
136 xpub
137 );
138 let _ = sender.send(SigningDeviceMsg::XPub(id, fg, path, xpub).into());
139 }
140 Err(e) => {
141 tracing::debug!("SupportedDevice[{}]::get_extended_pubkey: error={}", fg, e);
142 let _ = sender.send(SigningDeviceMsg::Error(Some(id), e.to_string()).into());
143 }
144 }
145 });
146 }
147
148 pub fn register_wallet(&self, id: Id, name: &str, policy: &str) {
149 let name = name.to_string();
150 let policy = policy.to_string();
151 let sender = self.sender.clone();
152 let fg = self.fingerprint;
153 let device = self.device.clone();
154 tracing::debug!(
155 "SupportedDevice[{}]::register_wallet: spawning task, name={}, policy={}",
156 fg,
157 name,
158 policy
159 );
160 self.rt.spawn(async move {
161 match (*device).register_wallet(&name, &policy).await {
162 Ok(hmac) => {
163 tracing::debug!(
164 "SupportedDevice[{}]::register_wallet: success, hmac={:?}",
165 fg,
166 hmac
167 );
168 let _ =
169 sender.send(SigningDeviceMsg::WalletRegistered(id, fg, name, hmac).into());
170 }
171 Err(e) => {
172 tracing::debug!("SupportedDevice[{}]::register_wallet: error={}", fg, e);
173 let _ = sender.send(SigningDeviceMsg::Error(Some(id), e.to_string()).into());
174 }
175 }
176 });
177 }
178
179 pub fn is_wallet_registered(&self, id: Id, name: &str, policy: &str) {
180 let name = name.to_string();
181 let policy = policy.to_string();
182 let sender = self.sender.clone();
183 let fg = self.fingerprint;
184 let device = self.device.clone();
185 tracing::debug!(
186 "SupportedDevice[{}]::is_wallet_registered: spawning task, name={}, policy={}",
187 fg,
188 name,
189 policy
190 );
191 self.rt.spawn(async move {
192 match (*device).is_wallet_registered(&name, &policy).await {
193 Ok(registered) => {
194 tracing::debug!(
195 "SupportedDevice[{}]::is_wallet_registered: success, registered={}",
196 fg,
197 registered
198 );
199 let _ = sender.send(
200 SigningDeviceMsg::WalletIsRegistered(id, fg, name, registered).into(),
201 );
202 }
203 Err(e) => {
204 tracing::debug!("SupportedDevice[{}]::is_wallet_registered: error={}", fg, e);
205 let _ = sender.send(SigningDeviceMsg::Error(Some(id), e.to_string()).into());
206 }
207 }
208 });
209 }
210
211 pub fn display_address(&self, id: Id, script: &AddressScript) {
212 let script = script.clone();
213 let sender = self.sender.clone();
214 let fg = self.fingerprint;
215 let device = self.device.clone();
216 tracing::debug!(
217 "SupportedDevice[{}]::display_address: spawning task, script={:?}",
218 fg,
219 script
220 );
221 self.rt.spawn(async move {
222 match (*device).display_address(&script).await {
223 Ok(()) => {
224 tracing::debug!("SupportedDevice[{}]::display_address: success", fg);
225 let _ = sender.send(SigningDeviceMsg::AddressDisplayed(id, fg, script).into());
226 }
227 Err(e) => {
228 tracing::debug!("SupportedDevice[{}]::display_address: error={}", fg, e);
229 let _ = sender.send(SigningDeviceMsg::Error(Some(id), e.to_string()).into());
230 }
231 }
232 });
233 }
234
235 pub fn sign_tx(&self, id: Id, tx: Psbt) {
236 let mut tx = tx;
237 let sender = self.sender.clone();
238 let fg = self.fingerprint;
239 let device = self.device.clone();
240 tracing::debug!("SupportedDevice[{}]::sign_tx: spawning task", fg);
241 self.rt.spawn(async move {
242 match (*device).sign_tx(&mut tx).await {
243 Ok(()) => {
244 tracing::debug!("SupportedDevice[{}]::sign_tx: success", fg);
245 let _ = sender.send(SigningDeviceMsg::TransactionSigned(id, fg, tx).into());
246 }
247 Err(e) => {
248 tracing::debug!("SupportedDevice[{}]::sign_tx: error={}", fg, e);
249 let _ = sender.send(SigningDeviceMsg::Error(Some(id), e.to_string()).into());
250 }
251 }
252 });
253 }
254}
255
256#[derive(Debug, Clone)]
257pub enum SigningDevice<Message, Id = ()>
258where
259 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
260 Id: Send + Clone + 'static,
261{
262 Unsupported {
263 id: String,
264 kind: DeviceKind,
265 version: Option<Version>,
266 reason: UnsupportedReason,
267 },
268 Locked {
270 id: String,
271 device: Arc<Mutex<Option<LockedDevice>>>,
272 pairing_code: Option<String>,
274 kind: DeviceKind,
275 },
276 Supported(SupportedDevice<Message, Id>),
277}
278
279impl<Message, Id> SigningDevice<Message, Id>
280where
281 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
282 Id: Send + Clone + 'static,
283{
284 async fn new(
285 id: String,
286 device: Arc<dyn HWI + Send + Sync>,
287 rt: tokio::runtime::Handle,
288 sender: channel::Sender<Message>,
289 ) -> Result<Self, HWIError> {
290 let kind = device.device_kind();
291 let fingerprint = device.get_master_fingerprint().await?;
292 let version = device.get_version().await.ok();
293 Ok(Self::Supported(SupportedDevice {
294 id,
295 device,
296 kind,
297 fingerprint,
298 version,
299 rt,
300 sender,
301 _phantom: PhantomData,
302 }))
303 }
304
305 pub fn id(&self) -> &str {
308 match self {
309 Self::Locked { id, .. } => id,
310 Self::Unsupported { id, .. } => id,
311 Self::Supported(SupportedDevice { id, .. }) => id,
312 }
313 }
314
315 pub fn kind(&self) -> &DeviceKind {
316 match self {
317 Self::Locked { kind, .. } => kind,
318 Self::Unsupported { kind, .. } => kind,
319 Self::Supported(SupportedDevice { kind, .. }) => kind,
320 }
321 }
322
323 pub fn fingerprint(&self) -> Option<Fingerprint> {
324 match self {
325 Self::Locked { .. } => None,
326 Self::Unsupported { .. } => None,
327 Self::Supported(SupportedDevice { fingerprint, .. }) => Some(*fingerprint),
328 }
329 }
330
331 pub fn is_supported(&self) -> bool {
332 matches!(self, Self::Supported { .. })
333 }
334
335 pub fn clone_locked(&self) -> Option<SigningDevice<Message, Id>> {
336 if let SigningDevice::Locked {
337 id,
338 device,
339 pairing_code,
340 kind,
341 } = self
342 {
343 Some(SigningDevice::Locked {
344 id: id.clone(),
345 device: device.clone(),
346 pairing_code: pairing_code.clone(),
347 kind: *kind,
348 })
349 } else {
350 None
351 }
352 }
353}
354
355#[derive(Debug, Clone, Deserialize, Serialize)]
356pub struct SigningDeviceConfig {
357 pub kind: String,
358 pub fingerprint: Fingerprint,
359 pub token: String,
361}
362
363impl SigningDeviceConfig {
364 pub fn new(kind: &crate::DeviceKind, fingerprint: Fingerprint, token: &[u8; 32]) -> Self {
365 Self {
366 kind: kind.to_string(),
367 fingerprint,
368 token: hex::encode(token),
369 }
370 }
371}
372
373#[derive(Debug, Clone)]
374pub enum SigningDeviceMsg<Id = ()> {
375 Error(Option<Id>, String),
377 Update,
379 XPub(Id, Fingerprint, DerivationPath, Xpub),
380 Version(Id, Fingerprint, Version),
381 WalletRegistered(Id, Fingerprint, String, Option<[u8; 32]>),
383 WalletIsRegistered(Id, Fingerprint, String, bool),
385 AddressDisplayed(Id, Fingerprint, AddressScript),
387 TransactionSigned(Id, Fingerprint, Psbt),
389}
390
391pub struct HwiService<Message, Id = ()>
392where
393 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
394 Id: Send + Clone + 'static,
395{
396 network: Network,
397 rt: tokio::runtime::Handle,
398 _owned_runtime: Option<tokio::runtime::Runtime>,
400 pub devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
401 ref_count: Arc<AtomicUsize>,
403 shutdown: Arc<AtomicBool>,
404 listener_handle: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
405 #[cfg(feature = "bitbox")]
409 bitbox_noise_config: Arc<Mutex<Option<Arc<dyn NoiseConfig>>>>,
410}
411
412impl<Message, Id> HwiService<Message, Id>
413where
414 Message: From<SigningDeviceMsg<Id>> + Send + 'static + Clone,
415 Id: Send + Clone + 'static,
416{
417 pub fn new(network: Network, rt: Option<tokio::runtime::Handle>) -> Self {
418 let (rt, owned_runtime) = if let Some(handle) = rt {
419 tracing::debug!("HwiService: using consumer-provided tokio runtime handle");
420 (handle, None)
421 } else {
422 tracing::debug!("HwiService: creating new internal tokio runtime");
423 let runtime = tokio::runtime::Runtime::new().expect("runtime must not fail");
424 let handle = runtime.handle().clone();
425 (handle, Some(runtime))
426 };
427 tracing::debug!("HwiService::new: network={:?}", network);
428 Self {
429 network,
430 devices: Default::default(),
431 rt,
432 _owned_runtime: owned_runtime,
433 ref_count: Arc::new(AtomicUsize::new(0)),
434 shutdown: Arc::new(AtomicBool::new(false)),
435 listener_handle: Arc::new(Mutex::new(None)),
436 #[cfg(feature = "bitbox")]
437 bitbox_noise_config: Arc::new(Mutex::new(None)),
438 }
439 }
440
441 pub fn list(&self) -> BTreeMap<String, SigningDevice<Message, Id>> {
442 self.devices.lock().expect("poisoned").clone()
443 }
444
445 #[cfg(feature = "bitbox")]
449 pub fn set_bitbox_noise_config(&self, noise_config: Arc<dyn NoiseConfig>) {
450 tracing::debug!("Setting BitBox02 pairing configuration");
451 *self.bitbox_noise_config.lock().expect("poisoned") = Some(noise_config);
452 }
453
454 #[cfg(feature = "bitbox")]
456 pub fn clear_bitbox_noise_config(&self) {
457 tracing::debug!("Clearing BitBox02 pairing configuration");
458 *self.bitbox_noise_config.lock().expect("poisoned") = None;
459 }
460
461 pub fn listen(&self, sender: channel::Sender<Message>, shutdown: Arc<AtomicBool>) {
462 listen(
463 sender,
464 self.devices.clone(),
465 self.network,
466 self.rt.clone(),
467 shutdown,
468 #[cfg(feature = "bitbox")]
469 self.bitbox_noise_config.clone(),
470 );
471 }
472
473 pub fn start(&self, sender: channel::Sender<Message>) {
475 let prev_count = self.ref_count.fetch_add(1, Ordering::SeqCst);
476 let new_count = prev_count + 1;
477
478 if prev_count == 0 {
479 tracing::info!(
481 "Starting HWI listener service (ref_count: {} -> {})",
482 prev_count,
483 new_count
484 );
485 self.shutdown.store(false, Ordering::SeqCst);
486
487 let sender = sender.clone();
488 let devices = self.devices.clone();
489 let network = self.network;
490 let rt = self.rt.clone();
491 let shutdown = self.shutdown.clone();
492 #[cfg(feature = "bitbox")]
493 let bitbox_noise_config = self.bitbox_noise_config.clone();
494
495 let handle = std::thread::spawn(move || {
496 tracing::debug!("HWI listener thread started");
497 listen(
498 sender,
499 devices,
500 network,
501 rt,
502 shutdown,
503 #[cfg(feature = "bitbox")]
504 bitbox_noise_config,
505 );
506 tracing::debug!("HWI listener thread stopped");
507 });
508
509 *self.listener_handle.lock().expect("poisoned") = Some(handle);
510 } else {
511 tracing::debug!(
512 "HWI listener already running, incrementing ref_count: {} -> {}",
513 prev_count,
514 new_count
515 );
516 }
517 }
518
519 pub fn stop(&self) {
521 tracing::debug!("stop() called");
522 loop {
524 let current = self.ref_count.load(Ordering::SeqCst);
525 if current == 0 {
526 tracing::warn!("stop() called but ref_count is already 0");
527 return;
528 }
529 match self.ref_count.compare_exchange(
530 current,
531 current - 1,
532 Ordering::SeqCst,
533 Ordering::SeqCst,
534 ) {
535 Ok(prev_count) => {
536 let new_count = prev_count - 1;
537 if prev_count == 1 {
538 tracing::info!(
540 "Stopping HWI listener service (ref_count: {} -> {})",
541 prev_count,
542 new_count
543 );
544 self.shutdown.store(true, Ordering::SeqCst);
545
546 let _ = self.listener_handle.lock().expect("poisoned").take();
548 } else {
549 tracing::debug!(
550 "HWI listener still in use, decrementing ref_count: {} -> {}",
551 prev_count,
552 new_count
553 );
554 }
555 return;
556 }
557 Err(_) => {
558 continue;
560 }
561 }
562 }
563 }
564}
565
566impl<Message, Id> Drop for HwiService<Message, Id>
567where
568 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
569 Id: Send + Clone + 'static,
570{
571 fn drop(&mut self) {
572 if self._owned_runtime.is_some() {
573 tracing::debug!(
574 "HwiService dropped - internal tokio runtime will be shut down, \
575 any pending async tasks will be cancelled"
576 );
577 } else {
578 tracing::debug!("HwiService dropped (using external runtime)");
579 }
580 }
581}
582
583#[cfg(feature = "bitbox")]
584async fn unlock_bitbox<Message, Id>(
585 id: String,
586 network: Network,
587 bb: Box<PairingBitbox02<runtime::TokioRuntime>>,
588 rt: tokio::runtime::Handle,
589 sender: channel::Sender<Message>,
590) -> Result<SigningDevice<Message, Id>, crate::Error>
591where
592 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
593 Id: Send + Clone + 'static,
594{
595 tracing::debug!("unlock_bitbox[{}]: waiting for pairing confirmation", id);
596 let paired_bb = bb.wait_confirm().await?;
597 tracing::debug!("unlock_bitbox[{}]: pairing confirmed", id);
598 let bitbox2 = BitBox02::from(paired_bb).with_network(network);
599 tracing::debug!("unlock_bitbox[{}]: getting fingerprint", id);
600 let fingerprint = bitbox2.get_master_fingerprint().await?;
601 tracing::debug!("unlock_bitbox[{}]: fingerprint={}", id, fingerprint);
602 let version = bitbox2.get_version().await.ok();
603 tracing::debug!("unlock_bitbox[{}]: version={:?}", id, version);
604 tracing::debug!(
605 "unlock_bitbox[{}]: returning Supported device with fingerprint={}",
606 id,
607 fingerprint
608 );
609 Ok(SigningDevice::Supported(SupportedDevice {
610 id: id.clone(),
611 kind: DeviceKind::BitBox02,
612 fingerprint,
613 device: bitbox2.into(),
614 version,
615 rt,
616 sender,
617 _phantom: PhantomData,
618 }))
619}
620
621fn listen<Message, Id>(
622 sender: channel::Sender<Message>,
623 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
624 network: Network,
625 rt: tokio::runtime::Handle,
626 shutdown: Arc<AtomicBool>,
627 #[cfg(feature = "bitbox")] bitbox_noise_config: Arc<Mutex<Option<Arc<dyn NoiseConfig>>>>,
628) where
629 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
630 Id: Send + Clone + 'static,
631{
632 tracing::info!("HWI listener starting for network: {:?}", network);
633
634 let mut hid = match ledger::HidApi::new() {
635 Ok(api) => {
636 tracing::debug!("HID API initialized successfully");
637 api
638 }
639 Err(e) => {
640 tracing::error!("Failed to initialize HID API: {}", e);
641 let _ = sender.send(SigningDeviceMsg::Error(None, e.to_string()).into());
642 return;
643 }
644 };
645
646 #[cfg(feature = "specter")]
647 let mut specter_simulator_handle = None;
648 #[cfg(feature = "specter")]
649 let mut specter_handles = BTreeMap::<String, JoinHandle<()>>::new();
650 #[cfg(feature = "jade")]
651 let mut jade_handles = BTreeMap::<String, JoinHandle<()>>::new();
652 #[cfg(feature = "ledger")]
653 let mut ledger_simulator_handle = None;
654 #[cfg(feature = "bitbox")]
655 let mut bitbox02_handles = BTreeMap::<String, JoinHandle<()>>::new();
656 #[cfg(feature = "coldcard")]
657 let mut coldcard_handles = BTreeMap::<String, JoinHandle<()>>::new();
658 #[cfg(feature = "ledger")]
659 let mut ledger_handles = BTreeMap::<String, JoinHandle<()>>::new();
660
661 loop {
662 if shutdown.load(Ordering::Relaxed) {
664 tracing::info!("HWI listener received shutdown signal, exiting");
665 return;
666 }
667
668 tracing::debug!("HWI poll cycle starting");
669
670 if let Err(e) = hid.refresh_devices() {
671 tracing::warn!("Failed to refresh HID devices: {}", e);
672 let _ = sender.send(SigningDeviceMsg::Error(None, e.to_string()).into());
673 continue;
674 };
675
676 tracing::trace!("HID devices refreshed successfully");
677
678 #[cfg(feature = "specter")]
679 handle_specter_simulator(
680 &rt,
681 sender.clone(),
682 &mut specter_simulator_handle,
683 devices.clone(),
684 );
685
686 #[cfg(feature = "specter")]
687 handle_specter(&rt, sender.clone(), &mut specter_handles, devices.clone());
688
689 #[cfg(feature = "jade")]
690 handle_jade(&rt, &sender, &mut jade_handles, devices.clone(), network);
691
692 #[cfg(feature = "ledger")]
693 handle_ledger_simulator(
694 &rt,
695 sender.clone(),
696 &mut ledger_simulator_handle,
697 devices.clone(),
698 );
699
700 let list = hid.device_list().collect::<Vec<_>>();
701 tracing::trace!("HID device list contains {} device(s)", list.len());
702
703 #[cfg(feature = "bitbox")]
704 let bitbox_devices: Vec<_> = list
705 .iter()
706 .filter_map(|d| crate::bitbox::is_bitbox02(d).then_some(*d))
707 .collect();
708 #[cfg(feature = "bitbox")]
709 tracing::trace!(
710 "Filtered {} BitBox02 device(s) from HID list",
711 bitbox_devices.len()
712 );
713
714 #[cfg(feature = "bitbox")]
715 handle_bitbox02(
716 &rt,
717 &sender,
718 &mut bitbox02_handles,
719 devices.clone(),
720 bitbox_devices,
721 &hid,
722 network,
723 bitbox_noise_config.clone(),
724 );
725
726 #[cfg(feature = "coldcard")]
727 let coldcard_devices: Vec<_> = list
728 .iter()
729 .filter_map(|d| crate::coldcard::is_coldcard(d).then_some(*d))
730 .collect();
731 #[cfg(feature = "coldcard")]
732 tracing::trace!(
733 "Filtered {} Coldcard device(s) from HID list",
734 coldcard_devices.len()
735 );
736
737 #[cfg(feature = "coldcard")]
738 handle_coldcard(
739 &rt,
740 &sender,
741 &mut coldcard_handles,
742 devices.clone(),
743 coldcard_devices,
744 &hid,
745 );
746
747 #[cfg(feature = "ledger")]
748 let ledger_devices: Vec<_> = TransportNativeHID::list_ledgers(&hid).collect();
749 #[cfg(feature = "ledger")]
750 tracing::trace!("Found {} Ledger device(s)", ledger_devices.len());
751
752 #[cfg(feature = "ledger")]
753 handle_ledger(
754 &rt,
755 &sender,
756 &mut ledger_handles,
757 devices.clone(),
758 ledger_devices,
759 &hid,
760 );
761
762 tracing::trace!("HWI poll cycle complete, sleeping for 2 seconds");
763 std::thread::sleep(Duration::from_secs(2));
764 }
765}
766
767#[cfg(feature = "specter")]
768fn handle_specter_simulator<Message, Id>(
769 rt: &tokio::runtime::Handle,
770 sender: channel::Sender<Message>,
771 handle: &mut Option<tokio::task::JoinHandle<()>>,
772 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
773) where
774 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
775 Id: Send + Clone + 'static,
776{
777 const SPECTER_SIMULATOR_ID: &str = "specter-simulator";
778 if devices
780 .lock()
781 .expect("poisoned")
782 .contains_key(SPECTER_SIMULATOR_ID)
783 {
784 tracing::trace!("handle_specter_simulator: device already in map, skipping");
785 return;
786 }
787 let poll = if let Some(h) = handle {
788 if h.is_finished() {
789 tracing::trace!("handle_specter_simulator: previous handle finished, will poll");
790 *handle = None;
791 true
792 } else {
793 tracing::debug!("handle_specter_simulator: previous handle still running, skipping");
794 false
795 }
796 } else {
797 tracing::trace!("handle_specter_simulator: no handle, will poll");
798 true
799 };
800 if poll {
801 tracing::trace!("handle_specter_simulator: spawning async task");
802 let rt_ = rt.clone();
803 let jh = rt.spawn(async move {
804 tracing::trace!("handle_specter_simulator: calling try_connect");
805 match specter::SpecterSimulator::try_connect().await {
806 Ok(device) => {
807 tracing::debug!("handle_specter_simulator: creating SigningDevice");
808 match SigningDevice::new(
809 SPECTER_SIMULATOR_ID.into(),
810 Arc::new(device),
811 rt_,
812 sender.clone(),
813 )
814 .await
815 {
816 Ok(hw) => {
817 tracing::debug!("handle_specter_simulator: inserting device into map");
818 devices
819 .lock()
820 .expect("poisoned")
821 .insert(SPECTER_SIMULATOR_ID.into(), hw);
822 let _ = sender.send(SigningDeviceMsg::Update.into());
823 }
824 Err(e) => {
825 tracing::debug!("Failed to initialize Specter Simulator: {}", e);
826 }
827 }
828 }
829 Err(HWIError::DeviceNotFound) => {
830 tracing::trace!("handle_specter_simulator: DeviceNotFound");
831 let was_present = devices
832 .lock()
833 .expect("poisoned")
834 .remove(SPECTER_SIMULATOR_ID)
835 .is_some();
836 if was_present {
837 tracing::debug!("handle_specter_simulator: removed from device map");
838 let _ = sender.send(SigningDeviceMsg::Update.into());
839 }
840 }
841 Err(e) => {
842 tracing::trace!("Specter Simulator not available: {}", e);
843 }
844 }
845 });
846 *handle = Some(jh);
847 }
848}
849
850fn should_poll<Message, Id>(
851 handles: &BTreeMap<String, JoinHandle<()>>,
852 devices: &Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
853 id: &str,
854) -> bool
855where
856 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
857 Id: Send + Clone + 'static,
858{
859 match devices.lock().expect("poisoned").get(id) {
861 Some(SigningDevice::Supported(_)) => {
862 tracing::trace!(
863 "should_poll({}): supported device already in map, returning false",
864 id
865 );
866 return false;
867 }
868 Some(SigningDevice::Locked { .. }) => {
869 tracing::trace!(
870 "should_poll({}): locked device already in map, returning false",
871 id
872 );
873 return false;
874 }
875 _ => {}
876 }
877
878 let result = match handles.get(id) {
879 Some(h) => {
880 let finished = h.is_finished();
881 tracing::trace!(
882 "should_poll({}): handle exists, is_finished={}",
883 id,
884 finished
885 );
886 finished
887 }
888 None => {
889 tracing::trace!("should_poll({}): no handle, returning true", id);
890 true
891 }
892 };
893 result
894}
895
896fn cleanup_disconnected<Message, Id>(
897 sender: &channel::Sender<Message>,
898 handles: &mut BTreeMap<String, JoinHandle<()>>,
899 devices: &Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
900 connected_ids: &[String],
901 prefix: &str,
902) where
903 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
904 Id: Send + Clone + 'static,
905{
906 tracing::trace!(
907 "cleanup_disconnected: checking prefix '{}', connected_ids={:?}",
908 prefix,
909 connected_ids
910 );
911
912 let ids_to_remove: Vec<_> = {
913 let devices_lock = devices.lock().expect("poisoned");
914 let current_ids: Vec<_> = devices_lock
915 .keys()
916 .filter(|id| id.starts_with(prefix))
917 .cloned()
918 .collect();
919 tracing::trace!(
920 "cleanup_disconnected: current devices with prefix '{}': {:?}",
921 prefix,
922 current_ids
923 );
924 current_ids
925 .into_iter()
926 .filter(|id| !connected_ids.contains(id))
927 .collect()
928 };
929
930 if !ids_to_remove.is_empty() {
931 tracing::trace!(
932 "Removing {} disconnected device(s) with prefix '{}': {:?}",
933 ids_to_remove.len(),
934 prefix,
935 ids_to_remove
936 );
937 let mut devices_lock = devices.lock().expect("poisoned");
938 for id in &ids_to_remove {
939 tracing::debug!("cleanup_disconnected: removing device {}", id);
940 devices_lock.remove(id);
941 handles.remove(id);
942 }
943 let _ = sender.send(SigningDeviceMsg::Update.into());
944 } else {
945 tracing::trace!(
946 "cleanup_disconnected: no devices to remove for prefix '{}'",
947 prefix
948 );
949 }
950}
951
952#[cfg(feature = "specter")]
953fn handle_specter<Message, Id>(
954 rt: &tokio::runtime::Handle,
955 sender: channel::Sender<Message>,
956 handles: &mut BTreeMap<String, tokio::task::JoinHandle<()>>,
957 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
958) where
959 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
960 Id: Send + Clone + 'static,
961{
962 fn specter_id(port: &str) -> String {
963 let id = format!("specter-{port}");
964 id.replace("\"", "")
965 }
966 tracing::trace!("handle_specter: enumerating potential ports");
967 match specter::SerialTransport::enumerate_potential_ports() {
968 Ok(ports) => {
969 if !ports.is_empty() {
970 tracing::debug!("Found {} potential Specter port(s)", ports.len());
971 }
972 tracing::trace!("handle_specter: ports={:?}", ports);
973 let connected_ids: Vec<_> = ports.iter().map(|p| specter_id(p)).collect();
974 cleanup_disconnected(&sender, handles, &devices, &connected_ids, "specter-");
975
976 for port in ports {
977 let id = specter_id(&port);
978 tracing::trace!("handle_specter: checking port {} (id={})", port, id);
979 if !should_poll(handles, &devices, &id) {
980 tracing::trace!("handle_specter: skipping {} (should_poll=false)", id);
981 continue;
982 }
983
984 tracing::trace!("handle_specter: spawning async task for device {}", id);
985 let devices = devices.clone();
986 let sender = sender.clone();
987 let id_ = id.clone();
988 let port_clone = port.clone();
989 let rt_ = rt.clone();
990 let jh = rt.spawn(async move {
991 tracing::trace!(
992 "handle_specter[{}]: creating Specter device on {}",
993 id_,
994 port_clone
995 );
996 let device =
997 match specter::Specter::<specter::SerialTransport>::new(port_clone.clone())
998 {
999 Err(e) => {
1000 tracing::trace!(
1001 "Failed to create Specter device on {}: {}",
1002 port_clone,
1003 e
1004 );
1005 return;
1006 }
1007 Ok(device) => device,
1008 };
1009 tracing::trace!("handle_specter[{}]: checking fingerprint with timeout", id_);
1010 if tokio::time::timeout(
1011 std::time::Duration::from_millis(500),
1012 device.fingerprint(),
1013 )
1014 .await
1015 .is_ok()
1016 {
1017 tracing::debug!(
1018 "handle_specter[{}]: creating SigningDevice on port {}",
1019 id_,
1020 port_clone
1021 );
1022 match SigningDevice::new(id_.clone(), Arc::new(device), rt_, sender.clone())
1023 .await
1024 {
1025 Ok(hw) => {
1026 tracing::debug!(
1027 "handle_specter[{}]: inserting device into map",
1028 id_
1029 );
1030 devices.lock().expect("poisoned").insert(id_, hw);
1031 let _ = sender.send(SigningDeviceMsg::Update.into());
1032 }
1033 Err(e) => {
1034 tracing::error!(
1035 "Failed to initialize Specter on {}: {}",
1036 port_clone,
1037 e
1038 );
1039 }
1040 }
1041 } else {
1042 tracing::trace!("Specter device timeout on port {}", port_clone);
1043 }
1044 });
1045 handles.insert(id, jh);
1046 }
1047 }
1048 Err(e) => tracing::error!("Error while listing specter wallets: {}", e),
1049 }
1050}
1051
1052#[cfg(feature = "jade")]
1053fn handle_jade<Message, Id>(
1054 rt: &tokio::runtime::Handle,
1055 sender: &channel::Sender<Message>,
1056 handles: &mut BTreeMap<String, tokio::task::JoinHandle<()>>,
1057 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1058 network: Network,
1059) where
1060 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1061 Id: Send + Clone + 'static,
1062{
1063 fn jade_id(port: &str) -> String {
1064 let id = format!("jade-{port}");
1065 id.replace("\"", "")
1066 }
1067
1068 tracing::trace!("handle_jade: enumerating potential ports");
1069 match jade::SerialTransport::enumerate_potential_ports() {
1070 Ok(ports) => {
1071 if !ports.is_empty() {
1072 tracing::debug!("Found {} potential Jade port(s)", ports.len());
1073 }
1074 tracing::trace!("handle_jade: ports={:?}", ports);
1075 let connected_ids: Vec<_> = ports.iter().map(|p| jade_id(p)).collect();
1076 cleanup_disconnected(sender, handles, &devices, &connected_ids, "jade-");
1077
1078 for port in ports {
1079 let id = jade_id(&port);
1080 tracing::trace!("handle_jade: checking port {} (id={})", port, id);
1081 if !should_poll(handles, &devices, &id) {
1082 tracing::trace!("handle_jade: skipping {} (should_poll=false)", id);
1083 continue;
1084 }
1085
1086 tracing::trace!("handle_jade: spawning async task for device {}", id);
1087 let devices = devices.clone();
1088 let sender = sender.clone();
1089 let id_ = id.clone();
1090 let port_clone = port.clone();
1091 let rt_ = rt.clone();
1092 let jh = rt.spawn(async move {
1093 tracing::trace!("handle_jade[{}]: creating transport on {}", id_, port_clone);
1095 let transport = match jade::SerialTransport::new(port_clone.clone()) {
1096 Err(e) => {
1097 tracing::error!(
1098 "Failed to create Jade transport on {}: {:?}",
1099 port_clone,
1100 e
1101 );
1102 return;
1103 }
1104 Ok(transport) => transport,
1105 };
1106 tracing::trace!("handle_jade[{}]: getting device info", id_);
1107 let device = Jade::new(transport).with_network(network);
1108 let info = match device.get_info().await {
1109 Ok(i) => {
1110 tracing::debug!(
1111 "handle_jade[{}]: got info, state={:?}, version={}",
1112 id_,
1113 i.jade_state,
1114 i.jade_version
1115 );
1116 i
1117 }
1118 Err(e) => {
1119 tracing::error!("Failed to get Jade info on {}: {}", port_clone, e);
1120 return;
1121 }
1122 };
1123 tracing::debug!(
1124 "Jade device detected on port {} (state: {:?})",
1125 port_clone,
1126 info.jade_state
1127 );
1128 let version = crate::parse_version(&info.jade_version).ok();
1129 tracing::debug!("handle_jade[{}]: calling handle_jade_device", id_);
1130 if let Some(dev) = handle_jade_device(
1131 info,
1132 network,
1133 device,
1134 id_.clone(),
1135 version,
1136 rt_.clone(),
1137 sender.clone(),
1138 )
1139 .await
1140 {
1141 tracing::debug!(
1142 "handle_jade[{}]: device created, variant={:?}",
1143 id_,
1144 match &dev {
1145 SigningDevice::Supported(_) => "Supported",
1146 SigningDevice::Locked { .. } => "Locked",
1147 SigningDevice::Unsupported { .. } => "Unsupported",
1148 }
1149 );
1150 let locked = dev.clone_locked();
1151 devices.lock().expect("poisoned").insert(id_.clone(), dev);
1152 let _ = sender.send(SigningDeviceMsg::Update.into());
1153 if let Some(SigningDevice::Locked { device, .. }) = locked {
1154 tracing::debug!(
1155 "handle_jade[{}]: device is locked, attempting unlock",
1156 id_
1157 );
1158 let jade = match device.lock().expect("poisoned").take() {
1159 Some(LockedDevice::Jade(jade)) => Some(jade),
1160 _ => None,
1161 };
1162 if let Some(jade) = jade {
1163 handle_locked_jade(jade, id_, devices, network, rt_, sender).await
1164 }
1165 }
1166 } else {
1167 tracing::trace!("handle_jade[{}]: handle_jade_device returned None", id_);
1168 }
1169 });
1170 handles.insert(id, jh);
1171 }
1172 }
1173 Err(e) => tracing::warn!("Error while listing jade devices: {}", e),
1174 }
1175}
1176
1177#[cfg(feature = "jade")]
1178async fn handle_jade_device<Message, Id>(
1179 info: GetInfoResponse,
1180 network: Network,
1181 device: Jade<SerialTransport>,
1182 id: String,
1183 version: Option<Version>,
1184 rt: tokio::runtime::Handle,
1185 sender: channel::Sender<Message>,
1186) -> Option<SigningDevice<Message, Id>>
1187where
1188 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1189 Id: Send + Clone + 'static,
1190{
1191 tracing::debug!(
1192 "handle_jade_device[{}]: network={:?}, jade_networks={:?}, jade_state={:?}",
1193 id,
1194 network,
1195 info.jade_networks,
1196 info.jade_state
1197 );
1198
1199 if (network == Network::Bitcoin
1200 && info.jade_networks != jade::api::JadeNetworks::Main
1201 && info.jade_networks != jade::api::JadeNetworks::All)
1202 || (network != Network::Bitcoin && info.jade_networks == jade::api::JadeNetworks::Main)
1203 {
1204 tracing::debug!(
1205 "handle_jade_device[{}]: network mismatch, returning Unsupported",
1206 id
1207 );
1208 Some(SigningDevice::Unsupported {
1209 id,
1210 kind: device.device_kind(),
1211 version,
1212 reason: UnsupportedReason::WrongNetwork,
1213 })
1214 } else {
1215 match info.jade_state {
1216 jade::api::JadeState::Locked
1217 | jade::api::JadeState::Temp
1218 | jade::api::JadeState::Uninit
1219 | jade::api::JadeState::Unsaved => {
1220 tracing::debug!(
1221 "handle_jade_device[{}]: state={:?}, returning Locked",
1222 id,
1223 info.jade_state
1224 );
1225 Some(SigningDevice::Locked {
1226 id,
1227 kind: DeviceKind::Jade,
1228 pairing_code: None,
1229 device: Arc::new(Mutex::new(Some(LockedDevice::Jade(device)))),
1230 })
1231 }
1232 jade::api::JadeState::Ready => {
1233 tracing::debug!(
1234 "handle_jade_device[{}]: state=Ready, getting fingerprint",
1235 id
1236 );
1237 let kind = device.device_kind();
1238 let version = device.get_version().await.ok();
1239 let fingerprint = match device.get_master_fingerprint().await {
1240 Err(HWIError::NetworkMismatch) => {
1241 tracing::debug!(
1242 "handle_jade_device[{}]: fingerprint returned NetworkMismatch",
1243 id
1244 );
1245 return Some(SigningDevice::Unsupported {
1246 id: id.clone(),
1247 kind,
1248 version,
1249 reason: UnsupportedReason::WrongNetwork,
1250 });
1251 }
1252 Err(e) => {
1253 tracing::error!("{e}");
1254 return None;
1255 }
1256 Ok(fingerprint) => {
1257 tracing::debug!(
1258 "handle_jade_device[{}]: got fingerprint={}",
1259 id,
1260 fingerprint
1261 );
1262 fingerprint
1263 }
1264 };
1265 tracing::debug!(
1266 "handle_jade_device[{}]: returning Supported with fingerprint={}",
1267 id,
1268 fingerprint
1269 );
1270 Some(SigningDevice::Supported(SupportedDevice {
1271 id: id.clone(),
1272 kind,
1273 fingerprint,
1274 device: Arc::new(device),
1275 version,
1276 rt,
1277 sender,
1278 _phantom: PhantomData,
1279 }))
1280 }
1281 }
1282 }
1283}
1284
1285#[cfg(feature = "jade")]
1286async fn handle_locked_jade<Message, Id>(
1287 device: Jade<SerialTransport>,
1288 id: String,
1289 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1290 network: Network,
1291 rt: tokio::runtime::Handle,
1292 sender: channel::Sender<Message>,
1293) where
1294 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1295 Id: Send + Clone + 'static,
1296{
1297 tracing::debug!("Attempting to unlock Jade device {}", id);
1298 if let Err(e) = device.auth().await {
1299 tracing::error!("Failed to unlock Jade {}: {}", id, e);
1300 tracing::debug!(
1301 "handle_locked_jade[{}]: removing from device map after auth failure",
1302 id
1303 );
1304 devices.lock().expect("poisoned").remove(&id);
1305 let _ = sender.send(SigningDeviceMsg::Update.into());
1306 return;
1307 }
1308 tracing::info!("Jade device {} successfully unlocked", id);
1309 tracing::trace!(
1310 "handle_locked_jade[{}]: getting device info after unlock",
1311 id
1312 );
1313 let info = match device.get_info().await {
1314 Ok(i) => {
1315 tracing::debug!(
1316 "handle_locked_jade[{}]: got info, state={:?}",
1317 id,
1318 i.jade_state
1319 );
1320 i
1321 }
1322 Err(e) => {
1323 tracing::error!("Failed to get Jade info {}: {}", id, e);
1324 tracing::debug!(
1325 "handle_locked_jade[{}]: removing from device map after info failure",
1326 id
1327 );
1328 devices.lock().expect("poisoned").remove(&id);
1329 let _ = sender.send(SigningDeviceMsg::Update.into());
1330 return;
1331 }
1332 };
1333 tracing::trace!("handle_locked_jade[{}]: calling handle_jade_device", id);
1334 if let Some(jade) =
1335 handle_jade_device(info, network, device, id.clone(), None, rt, sender.clone()).await
1336 {
1337 tracing::debug!(
1338 "handle_locked_jade[{}]: inserting unlocked device into map",
1339 id
1340 );
1341 devices.lock().expect("poisoned").insert(id, jade);
1342 let _ = sender.send(SigningDeviceMsg::Update.into());
1343 } else {
1344 tracing::trace!(
1345 "handle_locked_jade[{}]: handle_jade_device returned None",
1346 id
1347 );
1348 }
1349}
1350
1351#[cfg(feature = "ledger")]
1352fn handle_ledger_simulator<Message, Id>(
1353 rt: &tokio::runtime::Handle,
1354 sender: channel::Sender<Message>,
1355 handle: &mut Option<tokio::task::JoinHandle<()>>,
1356 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1357) where
1358 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1359 Id: Send + Clone + 'static,
1360{
1361 const LEDGER_SIMULATOR_ID: &str = "ledger-simulator";
1362 if devices
1364 .lock()
1365 .expect("poisoned")
1366 .contains_key(LEDGER_SIMULATOR_ID)
1367 {
1368 tracing::trace!("handle_ledger_simulator: device already in map, skipping");
1369 return;
1370 }
1371 let poll = if let Some(h) = handle {
1372 if h.is_finished() {
1373 tracing::trace!("handle_ledger_simulator: previous handle finished, will poll");
1374 *handle = None;
1375 true
1376 } else {
1377 tracing::debug!("handle_ledger_simulator: previous handle still running, skipping");
1378 false
1379 }
1380 } else {
1381 tracing::trace!("handle_ledger_simulator: no handle, will poll");
1382 true
1383 };
1384 if poll {
1385 tracing::trace!("handle_ledger_simulator: spawning async task");
1386 let rt_ = rt.clone();
1387 let sender_ = sender.clone();
1388 let jh = rt.spawn(async move {
1389 tracing::trace!("handle_ledger_simulator: calling try_connect");
1390 match ledger::LedgerSimulator::try_connect().await {
1391 Ok(device) => {
1392 tracing::debug!("Ledger Simulator connected");
1393 tracing::trace!("handle_ledger_simulator: calling handle_ledger_device");
1394 match handle_ledger_device(
1395 LEDGER_SIMULATOR_ID.into(),
1396 device,
1397 rt_,
1398 sender_.clone(),
1399 )
1400 .await
1401 {
1402 Ok(hw) => {
1403 tracing::debug!("handle_ledger_simulator: inserting device into map");
1404 devices
1405 .lock()
1406 .expect("poisoned")
1407 .insert(LEDGER_SIMULATOR_ID.into(), hw);
1408 let _ = sender.send(SigningDeviceMsg::Update.into());
1409 }
1410 Err(e) => {
1411 tracing::debug!("Failed to initialize Ledger Simulator: {}", e);
1412 }
1413 }
1414 }
1415 Err(HWIError::DeviceNotFound) => {
1416 tracing::trace!("handle_ledger_simulator: DeviceNotFound");
1417 let was_present = devices
1418 .lock()
1419 .expect("poisoned")
1420 .remove(LEDGER_SIMULATOR_ID)
1421 .is_some();
1422 if was_present {
1423 tracing::info!("Ledger Simulator disconnected");
1424 tracing::debug!("handle_ledger_simulator: removed from device map");
1425 let _ = sender.send(SigningDeviceMsg::Update.into());
1426 }
1427 }
1428 Err(e) => {
1429 tracing::trace!("Ledger Simulator not available: {}", e);
1430 }
1431 }
1432 });
1433 *handle = Some(jh);
1434 }
1435}
1436
1437#[cfg(feature = "bitbox")]
1438#[allow(clippy::too_many_arguments)]
1439fn handle_bitbox02<Message, Id>(
1440 rt: &tokio::runtime::Handle,
1441 sender: &channel::Sender<Message>,
1442 handles: &mut BTreeMap<String, tokio::task::JoinHandle<()>>,
1443 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1444 list: Vec<&DeviceInfo>,
1445 hid: &HidApi,
1446 network: Network,
1447 #[cfg(feature = "bitbox")] bitbox_noise_config: Arc<Mutex<Option<Arc<dyn NoiseConfig>>>>,
1448) where
1449 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1450 Id: Send + Clone + 'static,
1451{
1452 fn bitbox_id(device_info: &ledger::DeviceInfo) -> String {
1454 let id = if let Some(sn) = device_info.serial_number() {
1455 format!("bitbox-{sn}")
1456 } else {
1457 format!(
1458 "bitbox-{:?}-{}-{}",
1459 device_info.path(),
1460 device_info.vendor_id(),
1461 device_info.product_id()
1462 )
1463 };
1464 id.replace("\"", "")
1465 }
1466
1467 tracing::trace!("handle_bitbox02: processing {} device(s)", list.len());
1468
1469 if !list.is_empty() {
1470 tracing::trace!("Found {} potential BitBox02 device(s)", list.len());
1471 }
1472
1473 let connected_ids: Vec<_> = list.iter().map(|d| bitbox_id(d)).collect();
1474 tracing::trace!("handle_bitbox02: connected_ids={:?}", connected_ids);
1475 cleanup_disconnected(sender, handles, &devices, &connected_ids, "bitbox-");
1476
1477 for device_info in list {
1478 if crate::bitbox::is_bitbox02(device_info) {
1479 let id = bitbox_id(device_info);
1480 tracing::trace!(
1481 "handle_bitbox02: checking device {} (vid={}, pid={})",
1482 id,
1483 device_info.vendor_id(),
1484 device_info.product_id()
1485 );
1486 if !should_poll(handles, &devices, &id) {
1487 tracing::trace!("handle_bitbox02: skipping {} (should_poll=false)", id);
1488 continue;
1489 }
1490
1491 tracing::trace!("handle_bitbox02: opening HID device {}", id);
1492 if let Ok(device) = device_info.open_device(hid) {
1493 tracing::trace!("handle_bitbox02: spawning async task for device {}", id);
1494 let devices = devices.clone();
1495 let id_ = id.clone();
1496 let sender = sender.clone();
1497 let rt_ = rt.clone();
1498 #[cfg(feature = "bitbox")]
1499 let bitbox_noise_config = bitbox_noise_config.clone();
1500
1501 let jh = rt.spawn(async move {
1502 tracing::debug!("Connecting to BitBox02 device {}", id_);
1503
1504 #[cfg(feature = "bitbox")]
1506 let pairing_config: Option<Box<dyn NoiseConfig>> = {
1507 let config =
1508 bitbox_noise_config
1509 .lock()
1510 .expect("poisoned")
1511 .as_ref()
1512 .map(|arc| {
1513 tracing::debug!("Using pairing config for BitBox02 {}", id_);
1514 Box::new(ArcNoiseConfig(arc.clone())) as Box<dyn NoiseConfig>
1515 });
1516 tracing::debug!(
1517 "handle_bitbox02[{}]: pairing_config available={}",
1518 id_,
1519 config.is_some()
1520 );
1521 config
1522 };
1523
1524 tracing::debug!("handle_bitbox02[{}]: calling PairingBitbox02::connect", id_);
1525 match PairingBitbox02::connect(device, pairing_config).await {
1526 Ok(pairing_device) => {
1527 let pairing_code =
1528 pairing_device.pairing_code().map(|s| s.replace('\n', " "));
1529 tracing::debug!(
1530 "handle_bitbox02[{}]: connected, pairing_code={}",
1531 id_,
1532 pairing_code.as_deref().unwrap_or("none")
1533 );
1534 if let Some(ref code) = pairing_code {
1535 tracing::info!(
1536 "BitBox02 {} requires pairing with code: {}",
1537 id_,
1538 code
1539 );
1540 } else {
1541 tracing::debug!("BitBox02 {} connected (already paired)", id_);
1542 }
1543 let locked_device = Arc::new(Mutex::new(Some(LockedDevice::BitBox02(
1544 Box::new(pairing_device),
1545 ))));
1546
1547 tracing::debug!(
1548 "handle_bitbox02[{}]: inserting Locked device into map",
1549 id_
1550 );
1551 devices.lock().expect("poisoned").insert(
1552 id_.clone(),
1553 SigningDevice::Locked {
1554 id: id_.clone(),
1555 kind: DeviceKind::BitBox02,
1556 pairing_code,
1557 device: locked_device.clone(),
1558 },
1559 );
1560 let _ = sender.send(SigningDeviceMsg::Update.into());
1561
1562 tracing::debug!("handle_bitbox02[{}]: taking device for unlock", id_);
1563 let bb = locked_device.lock().expect("poisoned").take();
1564 if let Some(LockedDevice::BitBox02(bb)) = bb {
1565 tracing::trace!(
1566 "handle_bitbox02[{}]: calling handle_locked_bitbox",
1567 id_
1568 );
1569 handle_locked_bitbox(bb, id_, devices, network, rt_, sender).await;
1570 } else {
1571 unreachable!()
1572 }
1573 }
1574 Err(e) => {
1575 tracing::error!("Failed to connect to BitBox02 {}: {}", id_, e);
1576 }
1577 }
1578 });
1579 handles.insert(id, jh);
1580 } else {
1581 tracing::trace!("handle_bitbox02: failed to open HID device {}", id);
1582 }
1583 }
1584 }
1585}
1586
1587#[cfg(feature = "bitbox")]
1588async fn handle_locked_bitbox<Message, Id>(
1589 device: Box<PairingBitbox02<TokioRuntime>>,
1590 id: String,
1591 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1592 network: Network,
1593 rt: tokio::runtime::Handle,
1594 sender: channel::Sender<Message>,
1595) where
1596 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1597 Id: Send + Clone + 'static,
1598{
1599 tracing::debug!("Waiting for BitBox02 {} pairing confirmation", id);
1600 match unlock_bitbox(id.clone(), network, device, rt, sender.clone()).await {
1601 Ok(bb) => {
1602 tracing::info!("BitBox02 {} successfully unlocked", id);
1603 tracing::debug!(
1604 "handle_locked_bitbox[{}]: inserting unlocked device into map",
1605 id
1606 );
1607 devices.lock().expect("poisoned").insert(id, bb);
1608 let _ = sender.send(SigningDeviceMsg::Update.into());
1609 }
1610 Err(e) => {
1611 tracing::error!("Failed to unlock BitBox02 {}: {}", id, e);
1612 tracing::debug!(
1613 "handle_locked_bitbox[{}]: removing device from map after unlock failure",
1614 id
1615 );
1616 devices.lock().expect("poisoned").remove(&id);
1618 let _ = sender.send(SigningDeviceMsg::Update.into());
1619 }
1620 }
1621}
1622
1623#[cfg(feature = "coldcard")]
1624fn handle_coldcard<Message, Id>(
1625 rt: &tokio::runtime::Handle,
1626 sender: &channel::Sender<Message>,
1627 handles: &mut BTreeMap<String, tokio::task::JoinHandle<()>>,
1628 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1629 list: Vec<&DeviceInfo>,
1630 hid: &HidApi,
1631) where
1632 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1633 Id: Send + Clone + 'static,
1634{
1635 fn coldcard_id(device_info: &ledger::DeviceInfo) -> String {
1636 let id = format!(
1637 "coldcard-{:?}-{}-{}",
1638 device_info.path(),
1639 device_info.vendor_id(),
1640 device_info.product_id()
1641 );
1642 id.replace("\"", "")
1643 }
1644
1645 tracing::trace!("handle_coldcard: processing {} device(s)", list.len());
1646
1647 if !list.is_empty() {
1648 tracing::debug!("Found {} potential Coldcard device(s)", list.len());
1649 }
1650
1651 let connected_ids: Vec<_> = list.iter().map(|d| coldcard_id(d)).collect();
1652 tracing::trace!("handle_coldcard: connected_ids={:?}", connected_ids);
1653 cleanup_disconnected(sender, handles, &devices, &connected_ids, "coldcard-");
1654
1655 for device_info in list {
1656 if crate::coldcard::is_coldcard(device_info) {
1657 let id = coldcard_id(device_info);
1658 tracing::trace!(
1659 "handle_coldcard: checking device {} (vid={}, pid={})",
1660 id,
1661 device_info.vendor_id(),
1662 device_info.product_id()
1663 );
1664 if !should_poll(handles, &devices, &id) {
1665 tracing::debug!("handle_coldcard: skipping {} (should_poll=false)", id);
1666 continue;
1667 }
1668
1669 if let Some(sn) = device_info.serial_number() {
1670 tracing::debug!("handle_coldcard: device {} has serial_number={}", id, sn);
1671 let devices = devices.clone();
1672 let id_clone = id.clone();
1673 let sn = sn.to_string();
1674 tracing::debug!("handle_coldcard: opening Coldcard with serial {}", sn);
1675 if let Ok((cc, _)) =
1676 coldcard::api::Coldcard::open(AsRefWrap { inner: hid }, &sn, None)
1677 {
1678 tracing::trace!("handle_coldcard: spawning async task for device {}", id);
1679 let sender = sender.clone();
1680 let rt_ = rt.clone();
1681 let jh = rt.spawn(async move {
1682 tracing::debug!("Connecting to Coldcard device {}", id_clone);
1683 let device: Arc<dyn HWI + Send + Sync> =
1684 Arc::new(coldcard::Coldcard::from(cc));
1685 tracing::debug!(
1686 "handle_coldcard[{}]: getting fingerprint and version",
1687 id_clone
1688 );
1689 match (
1690 device.get_master_fingerprint().await,
1691 device.get_version().await,
1692 ) {
1693 (Ok(fingerprint), Ok(version)) => {
1694 tracing::debug!("Coldcard {} detected (version: {}, fingerprint: {})",
1695 id_clone, version, fingerprint);
1696 let hw = if version
1697 >= (Version {
1698 major: 6,
1699 minor: 2,
1700 patch: 1,
1701 prerelease: None,
1702 }) {
1703 tracing::debug!(
1704 "handle_coldcard[{}]: version supported, creating Supported device",
1705 id_clone
1706 );
1707 SigningDevice::Supported (SupportedDevice{
1708 id: id_clone.clone(),
1709 device,
1710 kind: DeviceKind::Coldcard,
1711 fingerprint,
1712 version: Some(version),
1713 rt: rt_,
1714 sender: sender.clone(),
1715 _phantom: PhantomData,
1716 })
1717 } else {
1718 tracing::debug!("Coldcard {} has unsupported version {} (requires >= 6.2.1)",
1719 id_clone, version);
1720 SigningDevice::Unsupported {
1721 id: id_clone.clone(),
1722 kind: DeviceKind::Coldcard,
1723 version: Some(version),
1724 reason: UnsupportedReason::Version {
1725 minimal_supported_version: "Edge firmware v6.2.1",
1726 },
1727 }
1728 };
1729 tracing::debug!(
1730 "handle_coldcard[{}]: inserting device into map",
1731 id_clone
1732 );
1733 devices.lock().expect("poisoned").insert(id_clone, hw);
1734 let _ = sender.send(SigningDeviceMsg::Update.into());
1735 }
1736 (Err(e1), Err(e2)) => {
1737 tracing::error!("Failed to connect to coldcard {}", id_clone);
1738 tracing::debug!(
1739 "handle_coldcard[{}]: fingerprint error={}, version error={}",
1740 id_clone,
1741 e1,
1742 e2
1743 );
1744 }
1745 (Err(e), _) => {
1746 tracing::error!("Failed to connect to coldcard {}", id_clone);
1747 tracing::debug!(
1748 "handle_coldcard[{}]: fingerprint error={}",
1749 id_clone,
1750 e
1751 );
1752 }
1753 (_, Err(e)) => {
1754 tracing::error!("Failed to connect to coldcard {}", id_clone);
1755 tracing::debug!(
1756 "handle_coldcard[{}]: version error={}",
1757 id_clone,
1758 e
1759 );
1760 }
1761 }
1762 });
1763 handles.insert(id, jh);
1764 } else {
1765 tracing::debug!(
1766 "handle_coldcard: failed to open Coldcard with serial {}",
1767 sn
1768 );
1769 }
1770 } else {
1771 tracing::trace!("handle_coldcard: device {} has no serial number", id);
1772 }
1773 }
1774 }
1775}
1776
1777#[cfg(feature = "ledger")]
1778fn handle_ledger<Message, Id>(
1779 rt: &tokio::runtime::Handle,
1780 sender: &channel::Sender<Message>,
1781 handles: &mut BTreeMap<String, tokio::task::JoinHandle<()>>,
1782 devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
1783 list: Vec<&DeviceInfo>,
1784 hid: &HidApi,
1785) where
1786 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1787 Id: Send + Clone + 'static,
1788{
1789 fn ledger_id(detected: &ledger::DeviceInfo) -> String {
1790 let id = format!(
1791 "ledger-{:?}-{}-{}",
1792 detected.path(),
1793 detected.vendor_id(),
1794 detected.product_id()
1795 );
1796 id.replace("\"", "")
1797 }
1798
1799 tracing::trace!("handle_ledger: processing {} device(s)", list.len());
1800
1801 if !list.is_empty() {
1802 tracing::trace!("Found {} Ledger device(s)", list.len());
1803 }
1804
1805 let connected_ids: Vec<_> = list.iter().map(|d| ledger_id(d)).collect();
1806 tracing::trace!("handle_ledger: connected_ids={:?}", connected_ids);
1807 cleanup_disconnected(sender, handles, &devices, &connected_ids, "ledger-");
1808
1809 for detected in list {
1810 let id = ledger_id(detected);
1811 tracing::trace!(
1812 "handle_ledger: checking device {} (vid={}, pid={})",
1813 id,
1814 detected.vendor_id(),
1815 detected.product_id()
1816 );
1817 if !should_poll(handles, &devices, &id) {
1818 tracing::debug!("handle_ledger: skipping {} (should_poll=false)", id);
1819 continue;
1820 }
1821
1822 tracing::trace!("handle_ledger: connecting to device {}", id);
1824 match ledger::Ledger::<ledger::TransportHID>::connect(hid, detected) {
1825 Ok(device) => {
1826 tracing::trace!("handle_ledger: spawning async task for device {}", id);
1827 let devices = devices.clone();
1828 let id_clone = id.clone();
1829 let rt_ = rt.clone();
1830 let sender_ = sender.clone();
1831 let jh = rt.spawn(async move {
1832 tracing::trace!("handle_ledger[{}]: calling handle_ledger_device", id_clone);
1833 match handle_ledger_device(id_clone.clone(), device, rt_, sender_).await {
1834 Ok(hw) => {
1835 if let SigningDevice::Supported(SupportedDevice {
1836 fingerprint,
1837 version,
1838 ..
1839 }) = &hw
1840 {
1841 tracing::info!(
1842 "Ledger {} connected (version: {:?}, fingerprint: {})",
1843 id_clone,
1844 version,
1845 fingerprint
1846 );
1847 tracing::debug!(
1848 "handle_ledger[{}]: created Supported device",
1849 id_clone
1850 );
1851 } else if let SigningDevice::Unsupported { reason, .. } = &hw {
1852 tracing::debug!("Ledger {} is unsupported: {:?}", id_clone, reason);
1853 }
1854 tracing::debug!(
1855 "handle_ledger[{}]: inserting device into map",
1856 id_clone
1857 );
1858 devices.lock().expect("poisoned").insert(id_clone, hw);
1859 }
1860 Err(e) => {
1861 tracing::debug!("Failed to initialize Ledger {}: {:?}", id_clone, e);
1862 }
1863 }
1864 });
1865 handles.insert(id, jh);
1866 let _ = sender.send(SigningDeviceMsg::Update.into());
1867 }
1868 Err(HWIError::DeviceNotFound) => {
1869 tracing::trace!("handle_ledger: device {} returned DeviceNotFound", id);
1870 }
1871 Err(e) => {
1872 tracing::trace!("handle_ledger: device {} connect error: {:?}", id, e);
1873 }
1874 }
1875 }
1876}
1877
1878#[cfg(feature = "ledger")]
1879async fn handle_ledger_device<Message, Id, T: crate::ledger::Transport + Sync + Send + 'static>(
1880 id: String,
1881 device: ledger::Ledger<T>,
1882 rt: tokio::runtime::Handle,
1883 sender: channel::Sender<Message>,
1884) -> Result<SigningDevice<Message, Id>, HWIError>
1885where
1886 Message: From<SigningDeviceMsg<Id>> + Send + Clone + 'static,
1887 Id: Send + Clone + 'static,
1888{
1889 tracing::debug!(
1890 "handle_ledger_device[{}]: getting fingerprint and version",
1891 id
1892 );
1893 match (
1894 device.get_master_fingerprint().await,
1895 device.get_version().await,
1896 ) {
1897 (Ok(fingerprint), Ok(version)) => {
1898 tracing::debug!(
1899 "handle_ledger_device[{}]: fingerprint={}, version={}",
1900 id,
1901 fingerprint,
1902 version
1903 );
1904 let supported = ledger_version_supported(&version);
1905 tracing::debug!(
1906 "handle_ledger_device[{}]: version {} supported={}",
1907 id,
1908 version,
1909 supported
1910 );
1911 if supported {
1912 tracing::debug!("handle_ledger_device[{}]: returning Supported", id);
1913 Ok(SigningDevice::Supported(SupportedDevice {
1914 id,
1915 kind: device.device_kind(),
1916 fingerprint,
1917 device: Arc::new(device),
1918 version: Some(version),
1919 rt,
1920 sender,
1921 _phantom: PhantomData,
1922 }))
1923 } else {
1924 tracing::debug!(
1925 "handle_ledger_device[{}]: returning Unsupported (version too old)",
1926 id
1927 );
1928 Ok(SigningDevice::Unsupported {
1929 id,
1930 kind: device.device_kind(),
1931 version: Some(version),
1932 reason: UnsupportedReason::Version {
1933 minimal_supported_version: "2.1.0",
1934 },
1935 })
1936 }
1937 }
1938 (Err(e1), Err(e2)) => {
1939 tracing::debug!(
1940 "handle_ledger_device[{}]: fingerprint error={}, version error={}, returning Unsupported (AppIsNotOpen)",
1941 id,
1942 e1,
1943 e2
1944 );
1945 Ok(SigningDevice::Unsupported {
1946 id,
1947 kind: device.device_kind(),
1948 version: None,
1949 reason: UnsupportedReason::AppIsNotOpen,
1950 })
1951 }
1952 (Err(e), _) => {
1953 tracing::debug!(
1954 "handle_ledger_device[{}]: fingerprint error={}, returning Unsupported (AppIsNotOpen)",
1955 id,
1956 e
1957 );
1958 Ok(SigningDevice::Unsupported {
1959 id,
1960 kind: device.device_kind(),
1961 version: None,
1962 reason: UnsupportedReason::AppIsNotOpen,
1963 })
1964 }
1965 (_, Err(e)) => {
1966 tracing::debug!(
1967 "handle_ledger_device[{}]: version error={}, returning Unsupported (AppIsNotOpen)",
1968 id,
1969 e
1970 );
1971 Ok(SigningDevice::Unsupported {
1972 id,
1973 kind: device.device_kind(),
1974 version: None,
1975 reason: UnsupportedReason::AppIsNotOpen,
1976 })
1977 }
1978 }
1979}
1980
1981struct AsRefWrap<'a, T> {
1982 inner: &'a T,
1983}
1984
1985impl<T> AsRef<T> for AsRefWrap<'_, T> {
1986 fn as_ref(&self) -> &T {
1987 self.inner
1988 }
1989}
1990
1991#[cfg(feature = "ledger")]
1992fn ledger_version_supported(version: &Version) -> bool {
1993 if version.major >= 2 {
1994 if version.major == 2 {
1995 version.minor >= 1
1996 } else {
1997 true
1998 }
1999 } else {
2000 false
2001 }
2002}
2003
2004const DEVICES_COMPATIBLE_WITH_TAPMINISCRIPT: [(DeviceKind, Option<Version>); 5] = [
2006 (
2007 DeviceKind::Ledger,
2008 Some(Version {
2009 major: 2,
2010 minor: 2,
2011 patch: 0,
2012 prerelease: None,
2013 }),
2014 ),
2015 (DeviceKind::Specter, None),
2016 (DeviceKind::SpecterSimulator, None),
2017 (
2018 DeviceKind::Coldcard,
2019 Some(Version {
2020 major: 6,
2021 minor: 3,
2022 patch: 3,
2023 prerelease: None,
2024 }),
2025 ),
2026 (
2027 DeviceKind::BitBox02,
2028 Some(Version {
2029 major: 9,
2030 minor: 21,
2031 patch: 0,
2032 prerelease: None,
2033 }),
2034 ),
2035];
2036
2037pub fn is_compatible_with_tapminiscript(
2038 device_kind: &DeviceKind,
2039 version: Option<&Version>,
2040) -> bool {
2041 DEVICES_COMPATIBLE_WITH_TAPMINISCRIPT
2042 .iter()
2043 .any(|(kind, minimal_version)| {
2044 device_kind == kind
2045 && match (version, minimal_version) {
2046 (Some(v1), Some(v2)) => v1 >= v2,
2047 (None, Some(_)) => false,
2048 (Some(_), None) => true,
2049 (None, None) => true,
2050 }
2051 })
2052}