Skip to main content

async_hwi/
service.rs

1//! Hardware wallet device discovery and management service.
2//!
3//! Polls for connected hardware wallets every 2 seconds and maintains a shared device map.
4//! Supports multiple concurrent consumers via reference-counted start/stop.
5
6use 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/// Wrapper that implements NoiseConfig by delegating to an Arc<dyn NoiseConfig>.
39/// This allows cloning the Arc and converting it to Box<dyn NoiseConfig> for APIs that require Box.
40#[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    /// Ledger-specific: Bitcoin app not open.
65    AppIsNotOpen,
66}
67
68pub enum LockedDevice {
69    BitBox02(Box<PairingBitbox02<runtime::TokioRuntime>>),
70    /// Unlocks via blind oracle (network required).
71    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    /// Inner Option is None while unlock is in progress.
269    Locked {
270        id: String,
271        device: Arc<Mutex<Option<LockedDevice>>>,
272        /// BitBox02 only.
273        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    /// Stable device identifier (serial-based when available).
306    /// Use this as the key for `set_bitbox_config`.
307    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    /// Hex-encoded 32-byte token.
360    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 with optional request Id (None for polling loop errors, Some for forwarding method errors).
376    Error(Option<Id>, String),
377    /// Device map changed.
378    Update,
379    XPub(Id, Fingerprint, DerivationPath, Xpub),
380    Version(Id, Fingerprint, Version),
381    /// Wallet registered with name and optional HMAC.
382    WalletRegistered(Id, Fingerprint, String, Option<[u8; 32]>),
383    /// Wallet registration check result.
384    WalletIsRegistered(Id, Fingerprint, String, bool),
385    /// Address displayed on device.
386    AddressDisplayed(Id, Fingerprint, AddressScript),
387    /// Transaction signed.
388    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    /// Holds the runtime if we created it internally (keeps it alive).
399    _owned_runtime: Option<tokio::runtime::Runtime>,
400    pub devices: Arc<Mutex<BTreeMap<String, SigningDevice<Message, Id>>>>,
401    // Reference counting for multiple modal consumers
402    ref_count: Arc<AtomicUsize>,
403    shutdown: Arc<AtomicBool>,
404    listener_handle: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
405    // BitBox02 pairing configuration. Contains the app's private key and public keys
406    // of all paired devices. The NoiseConfigData can store multiple device pubkeys,
407    // so a single config works for all devices.
408    #[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    /// Set the BitBox02 pairing configuration. The NoiseConfigData contains the app's
446    /// private key and public keys of all paired devices, so a single config works
447    /// for all BitBox02 devices.
448    #[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    /// Clear the BitBox02 pairing configuration.
455    #[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    /// Ref-counted start; only first caller spawns the listener thread.
474    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            // First caller - start the listener
480            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    /// Ref-counted stop; only last caller joins the listener thread.
520    pub fn stop(&self) {
521        tracing::debug!("stop() called");
522        // Use compare-and-swap loop to safely decrement only when > 0
523        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                        // Last caller - stop the listener
539                        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                        // Take the handle - the thread will stop on its own when it sees the shutdown flag
547                        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                    // Another thread modified ref_count, retry
559                    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        // Check for shutdown signal
663        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 device is already in the map, don't poll it again
779    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    // If device is already in the map and supported, don't poll it again
860    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                    // Create Jade transport inside tokio runtime context (required by tokio-serial)
1094                    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 device is already in the map, don't poll it again
1363    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    /// Prefer serial number for stable ID across USB ports; fall back to path.
1453    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                    // Get the pairing config if available
1505                    #[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            // Remove the device from the list since unlocking failed
1617            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        // Connect synchronously first
1823        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
2004/// (DeviceKind, min version) - None means all versions support it.
2005const 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}