Skip to main content

bhwi_async/
lib.rs

1//! Asynchronous hardware-wallet operations over caller-provided transports.
2//!
3//! [`HWI`] preserves backend error types; [`HWIDevice`] provides an object-safe,
4//! error-erased interface. The asynchronous traits use `async_trait(?Send)`:
5//! their futures need not be `Send`, and no particular async runtime is required.
6//!
7//! Shared command types are reexported from `bhwi::common`, including typed
8//! host requests and responses for [`HostInteraction`] implementations.
9
10/// BitBox02 sessions and pairing.
11#[cfg(feature = "bitbox")]
12pub mod bitbox;
13/// Coldcard sessions.
14#[cfg(feature = "coldcard")]
15pub mod coldcard;
16/// Standard descriptor and keypool construction.
17pub mod descriptors;
18/// Device discovery, selection, and cached metadata.
19pub mod device;
20/// Address-display requests prepared from descriptors.
21pub mod display_address;
22/// Jade sessions and PIN-server integration.
23#[cfg(feature = "jade")]
24pub mod jade;
25/// KeepKey sessions and host interaction.
26#[cfg(feature = "keepkey")]
27pub mod keepkey;
28/// Ledger sessions.
29#[cfg(feature = "ledger")]
30pub mod ledger;
31/// Device-specific setup, recovery, and PIN contexts.
32#[cfg(any(feature = "bitbox", feature = "keepkey", feature = "trezor"))]
33pub mod management;
34/// PSBT input cleanup and signature accumulation.
35pub mod psbt;
36/// Message-signature encoding and device-specific signing preparation.
37pub mod signing;
38#[cfg(feature = "specter")]
39pub mod specter;
40/// Framing adapters over caller-provided I/O channels.
41pub mod transport;
42/// Trezor sessions and host interaction.
43#[cfg(feature = "trezor")]
44pub mod trezor;
45
46use std::{error::Error as StdError, fmt::Debug, str::FromStr};
47
48use async_trait::async_trait;
49pub use bhwi::common::DeviceBackup;
50pub use bhwi::common::DeviceContext;
51pub use bhwi::common::DisplayAddress;
52pub use bhwi::common::ErrorKind;
53pub use bhwi::common::Info;
54pub use bhwi::common::RestoreOptions;
55pub use bhwi::common::SetupOptions;
56pub use bhwi::common::WalletRegistration;
57pub use bhwi::common::{Error as HostError, HostRequest, HostResponse, PinMatrixRequestKind};
58pub use bhwi::common::{MultisigAddressType, MultisigDisplayAddress};
59use bhwi::miniscript::descriptor::WalletPolicy;
60#[cfg(any(feature = "trezor", feature = "keepkey"))]
61pub use bhwi::passphrase::HostPassphrase;
62use bhwi::{
63    Interpreter,
64    bitcoin::{
65        Network,
66        bip32::{DerivationPath, Fingerprint, Xpub},
67        psbt::Psbt,
68        secp256k1::ecdsa::Signature,
69    },
70    common::{self},
71};
72/// The asynchronous Jade client.
73#[cfg(feature = "jade")]
74pub use jade::Jade;
75/// The asynchronous KeepKey client.
76#[cfg(feature = "keepkey")]
77pub use keepkey::KeepKey;
78/// The asynchronous Ledger client.
79#[cfg(feature = "ledger")]
80pub use ledger::Ledger;
81/// The asynchronous Specter-DIY client.
82#[cfg(feature = "specter")]
83pub use specter::Specter;
84/// The asynchronous Trezor client.
85#[cfg(feature = "trezor")]
86pub use trezor::Trezor;
87
88/// Exchanges already-encoded interpreter payloads with a device.
89///
90/// Implementations provide framing and I/O, not interpreter-level encryption.
91/// Futures need not be `Send`; the trait does not select an async runtime.
92#[async_trait(?Send)]
93pub trait Transport {
94    /// An I/O or framing failure.
95    type Error: Debug;
96    /// Sends a payload and returns the device response.
97    ///
98    /// `encrypted` identifies an already-encrypted payload for framing purposes;
99    /// it is not a request to encrypt `command` again.
100    async fn exchange(&mut self, command: &[u8], encrypted: bool) -> Result<Vec<u8>, Self::Error>;
101
102    /// Returns whether the backend classifies `error` as a post-write read failure.
103    ///
104    /// Commands that allow a final disconnect may treat this classification as
105    /// success. BitBox includes timeouts and short reports; `true` proves neither
106    /// that the device disconnected nor that its action completed.
107    fn is_post_write_disconnect(&self, _error: &Self::Error) -> bool {
108        false
109    }
110
111    /// Returns the kind of `error`; defaults to [`ErrorKind::Transport`].
112    fn error_kind(&self, _error: &Self::Error) -> ErrorKind {
113        ErrorKind::Transport
114    }
115}
116
117/// Sends interpreter PIN-server requests using caller-provided HTTP I/O.
118///
119/// Futures need not be `Send`; the trait does not select an async runtime.
120#[async_trait(?Send)]
121pub trait HttpClient {
122    /// An HTTP request failure.
123    type Error: Debug;
124    /// Sends the encoded request to `url` and returns the encoded response body.
125    async fn request(&self, url: &str, request: &[u8]) -> Result<Vec<u8>, Self::Error>;
126
127    /// Returns the kind of `error`; defaults to [`ErrorKind::Transport`].
128    fn error_kind(&self, _error: &Self::Error) -> ErrorKind {
129        ErrorKind::Transport
130    }
131}
132
133/// Collects typed user input requested by a device.
134///
135/// Futures need not be `Send`. The command driver validates and encodes returned
136/// responses with [`HostResponse::into_bytes_for`].
137#[async_trait(?Send)]
138pub trait HostInteraction {
139    /// Returns input for `request`, or an error such as cancellation.
140    async fn respond(
141        &mut self,
142        request: &common::HostRequest,
143    ) -> Result<common::HostResponse, common::Error>;
144}
145
146/// Asynchronous wallet operations with a backend-specific error type.
147///
148/// Support and required [`DeviceContext`] values vary by device. Unsupported
149/// operations and missing context return errors. Futures need not be `Send`,
150/// and the trait does not select an async runtime.
151#[async_trait(?Send)]
152pub trait HWI {
153    /// A device, transport, or host-interaction failure.
154    type Error: Debug;
155    /// Requests a device backup, which need not produce a downloadable file.
156    async fn backup_device(&mut self) -> Result<DeviceBackup, Self::Error>;
157    /// Initializes a device using options and any required caller-supplied context.
158    async fn setup_device(
159        &mut self,
160        options: SetupOptions,
161        context: Option<DeviceContext>,
162    ) -> Result<bool, Self::Error>;
163    /// Requests a device wipe and returns the reported or assumed outcome.
164    ///
165    /// Qualifying post-write read errors are treated as `Ok(true)` without
166    /// confirming the wipe. On BitBox this includes timeouts and short reports,
167    /// even on the initial information query before the wipe request is sent.
168    async fn wipe_device(&mut self) -> Result<bool, Self::Error>;
169    /// Restores a device using options and any required recovery context.
170    async fn restore_device(
171        &mut self,
172        options: RestoreOptions,
173        context: Option<DeviceContext>,
174    ) -> Result<bool, Self::Error>;
175    /// Requests a passphrase-protection toggle and returns the backend's outcome.
176    ///
177    /// KeepKey can return `true` while awaiting PIN entry, before the setting changes.
178    async fn toggle_passphrase(&mut self) -> Result<bool, Self::Error>;
179    /// Requests PIN entry and returns whether the prompt was accepted.
180    async fn prompt_pin(&mut self) -> Result<bool, Self::Error>;
181    /// Submits scrambled keypad positions supplied in the device-specific context.
182    async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, Self::Error>;
183    /// Runs the backend's initialization or unlock handshake for `network`.
184    ///
185    /// This may involve device or host interaction. Trezor and KeepKey initialization
186    /// can succeed while a PIN is still required; `Ok(())` does not guarantee an
187    /// unlocked wallet.
188    async fn unlock(&mut self, network: Network) -> Result<(), Self::Error>;
189    /// Returns device information, which may be synthesized by the backend.
190    async fn get_info(&mut self) -> Result<Info, Self::Error>;
191    /// Returns the master fingerprint of the currently selected wallet.
192    async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, Self::Error>;
193    /// Derives an extended public key, optionally requesting on-device display.
194    async fn get_extended_pubkey(
195        &mut self,
196        path: DerivationPath,
197        display: bool,
198    ) -> Result<Xpub, Self::Error>;
199    /// Signs a message and returns the backend's header and ECDSA signature.
200    async fn sign_message(
201        &mut self,
202        message: &[u8],
203        path: DerivationPath,
204    ) -> Result<(u8, Signature), Self::Error>;
205    /// Displays an address using any backend-required policy or registration context.
206    async fn display_address(
207        &mut self,
208        address: common::DisplayAddress,
209        context: Option<common::DeviceContext>,
210    ) -> Result<String, Self::Error>;
211    /// Registers a named wallet policy, possibly leaving user confirmation pending.
212    async fn register_wallet(
213        &mut self,
214        name: &str,
215        policy: &str,
216    ) -> Result<WalletRegistration, Self::Error>;
217    /// Signs a PSBT using any backend-required context and returns the updated PSBT.
218    ///
219    /// Ledger requires a Ledger context even for a policy without a registration HMAC.
220    async fn sign_tx(
221        &mut self,
222        psbt: Psbt,
223        context: Option<common::DeviceContext>,
224    ) -> Result<Psbt, Self::Error>;
225}
226
227// TODO: this will become a pain to maintain, but we can have a proc-macro
228// generate this trait by putting it over HWI's definition and then also
229// generate the blanket impl which will map the errors to HWIDeviceError
230/// Object-safe wallet operations with errors erased into [`HWIDeviceError`].
231///
232/// The blanket implementation delegates to [`HWI`]. Support and context
233/// requirements remain backend-specific; futures need not be `Send`.
234#[async_trait(?Send)]
235pub trait HWIDevice {
236    /// Requests a device backup, which need not produce a downloadable file.
237    async fn backup_device(&mut self) -> Result<DeviceBackup, HWIDeviceError>;
238    /// Initializes a device using options and any required caller-supplied context.
239    async fn setup_device(
240        &mut self,
241        options: SetupOptions,
242        context: Option<DeviceContext>,
243    ) -> Result<bool, HWIDeviceError>;
244    /// Requests a device wipe and returns the reported or assumed outcome.
245    ///
246    /// Qualifying post-write read errors are treated as `Ok(true)` without
247    /// confirming the wipe. On BitBox this includes timeouts and short reports,
248    /// even on the initial information query before the wipe request is sent.
249    async fn wipe_device(&mut self) -> Result<bool, HWIDeviceError>;
250    /// Restores a device using options and any required recovery context.
251    async fn restore_device(
252        &mut self,
253        options: RestoreOptions,
254        context: Option<DeviceContext>,
255    ) -> Result<bool, HWIDeviceError>;
256    /// Requests a passphrase-protection toggle and returns the backend's outcome.
257    ///
258    /// KeepKey can return `true` while awaiting PIN entry, before the setting changes.
259    async fn toggle_passphrase(&mut self) -> Result<bool, HWIDeviceError>;
260    /// Requests PIN entry and returns whether the prompt was accepted.
261    async fn prompt_pin(&mut self) -> Result<bool, HWIDeviceError>;
262    /// Submits scrambled keypad positions supplied in the device-specific context.
263    async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, HWIDeviceError>;
264    /// Runs the backend's initialization or unlock handshake for `network`.
265    ///
266    /// This may involve device or host interaction. Trezor and KeepKey initialization
267    /// can succeed while a PIN is still required; `Ok(())` does not guarantee an
268    /// unlocked wallet.
269    async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError>;
270    /// Returns device information, which may be synthesized by the backend.
271    async fn get_info(&mut self) -> Result<Info, HWIDeviceError>;
272    /// Returns the master fingerprint of the currently selected wallet.
273    async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError>;
274    /// Derives an extended public key, optionally requesting on-device display.
275    async fn get_extended_pubkey(
276        &mut self,
277        path: DerivationPath,
278        display: bool,
279    ) -> Result<Xpub, HWIDeviceError>;
280    /// Signs a message and returns the backend's header and ECDSA signature.
281    async fn sign_message(
282        &mut self,
283        message: &[u8],
284        path: DerivationPath,
285    ) -> Result<(u8, Signature), HWIDeviceError>;
286    /// Displays an address using any backend-required policy or registration context.
287    async fn display_address(
288        &mut self,
289        address: common::DisplayAddress,
290        context: Option<common::DeviceContext>,
291    ) -> Result<String, HWIDeviceError>;
292    /// Registers a named wallet policy, possibly leaving user confirmation pending.
293    async fn register_wallet(
294        &mut self,
295        name: &str,
296        policy: &str,
297    ) -> Result<WalletRegistration, HWIDeviceError>;
298    /// Signs a PSBT using any backend-required context and returns the updated PSBT.
299    ///
300    /// Ledger requires a Ledger context even for a policy without a registration HMAC.
301    async fn sign_tx(
302        &mut self,
303        psbt: Psbt,
304        context: Option<common::DeviceContext>,
305    ) -> Result<Psbt, HWIDeviceError>;
306}
307
308/// An erased wallet-operation error that retains the original error as its source.
309#[derive(Debug, thiserror::Error)]
310#[error("{error}")]
311pub struct HWIDeviceError {
312    #[source]
313    error: Box<dyn StdError + Send + Sync + 'static>,
314    kind: Option<ErrorKind>,
315}
316
317impl HWIDeviceError {
318    /// Wraps a backend error while retaining its error chain.
319    pub fn new(error: impl StdError + Send + Sync + 'static) -> Self {
320        Self {
321            error: Box::new(error),
322            kind: None,
323        }
324    }
325
326    /// Creates an erased error with an explicit kind.
327    pub fn with_kind(error: impl StdError + Send + Sync + 'static, kind: ErrorKind) -> Self {
328        Self {
329            kind: Some(kind),
330            ..Self::new(error)
331        }
332    }
333
334    /// Returns the kind set on this error, or else the first one in its source chain.
335    pub fn kind(&self) -> Option<ErrorKind> {
336        if self.kind.is_some() {
337            return self.kind;
338        }
339        let mut source: Option<&(dyn StdError + 'static)> = Some(self.error.as_ref());
340        while let Some(current) = source {
341            if let Some(error) = current.downcast_ref::<common::Error>() {
342                return Some(error.kind());
343            }
344            if let Some(kind) = current
345                .downcast_ref::<HWIDeviceError>()
346                .and_then(Self::kind)
347            {
348                return Some(kind);
349            }
350            source = current.source();
351        }
352        None
353    }
354}
355
356impl From<Box<dyn StdError + Send + Sync + 'static>> for HWIDeviceError {
357    fn from(error: Box<dyn StdError + Send + Sync + 'static>) -> Self {
358        Self { error, kind: None }
359    }
360}
361
362/// An error that reports its [`ErrorKind`].
363pub trait ErrorKindOf {
364    /// Returns the error's kind.
365    fn error_kind(&self) -> ErrorKind;
366}
367
368impl ErrorKindOf for common::Error {
369    fn error_kind(&self) -> ErrorKind {
370        self.kind()
371    }
372}
373
374/// A failure while driving a common command through its interpreter and I/O.
375#[derive(Debug, thiserror::Error)]
376pub enum Error<E, F> {
377    /// A device transport failure.
378    #[error("transport error: {error}")]
379    Transport {
380        /// The failure kind.
381        kind: ErrorKind,
382        /// The transport's error.
383        error: E,
384    },
385
386    /// A PIN-server HTTP failure.
387    #[error("http client error: {error}")]
388    HttpClient {
389        /// The failure kind.
390        kind: ErrorKind,
391        /// The HTTP client's error.
392        error: F,
393    },
394
395    /// An interpreter or host-interaction failure.
396    #[error("{0}")]
397    Interpreter(
398        /// The command or input error.
399        #[from]
400        common::Error,
401    ),
402}
403
404impl<E, F> Error<E, F> {
405    /// Returns the failure kind from whichever layer failed.
406    pub fn kind(&self) -> ErrorKind {
407        match self {
408            Self::Transport { kind, .. } | Self::HttpClient { kind, .. } => *kind,
409            Self::Interpreter(error) => error.kind(),
410        }
411    }
412}
413
414impl<E, F> ErrorKindOf for Error<E, F> {
415    fn error_kind(&self) -> ErrorKind {
416        self.kind()
417    }
418}
419
420fn kinded<E>(error: E) -> HWIDeviceError
421where
422    E: ErrorKindOf + StdError + Send + Sync + 'static,
423{
424    let kind = error.error_kind();
425    HWIDeviceError::with_kind(error, kind)
426}
427
428#[async_trait(?Send)]
429impl<D> HWI for D
430where
431    D: CommonInterface<common::Command, common::Transmit, common::Response, common::Error>
432        + OnUnlock,
433{
434    type Error = Error<D::TransportError, D::HttpClientError>;
435    async fn backup_device(&mut self) -> Result<DeviceBackup, Self::Error> {
436        if let common::Response::Backup(backup) = run_command(self, common::Command::Backup).await?
437        {
438            Ok(backup)
439        } else {
440            Err(
441                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
442                    .into(),
443            )
444        }
445    }
446
447    async fn setup_device(
448        &mut self,
449        options: SetupOptions,
450        context: Option<DeviceContext>,
451    ) -> Result<bool, Self::Error> {
452        if let common::Response::DeviceAction(success) =
453            run_command(self, common::Command::Setup(options, context)).await?
454        {
455            Ok(success)
456        } else {
457            Err(
458                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
459                    .into(),
460            )
461        }
462    }
463
464    async fn wipe_device(&mut self) -> Result<bool, Self::Error> {
465        if let common::Response::DeviceAction(success) =
466            run_command_allowing_final_disconnect(self, common::Command::Wipe).await?
467        {
468            Ok(success)
469        } else {
470            Err(
471                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
472                    .into(),
473            )
474        }
475    }
476
477    async fn restore_device(
478        &mut self,
479        options: RestoreOptions,
480        context: Option<DeviceContext>,
481    ) -> Result<bool, Self::Error> {
482        if let common::Response::DeviceAction(success) =
483            run_command(self, common::Command::Restore(options, context)).await?
484        {
485            Ok(success)
486        } else {
487            Err(
488                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
489                    .into(),
490            )
491        }
492    }
493
494    async fn toggle_passphrase(&mut self) -> Result<bool, Self::Error> {
495        if let common::Response::DeviceAction(success) =
496            run_command(self, common::Command::TogglePassphrase).await?
497        {
498            Ok(success)
499        } else {
500            Err(
501                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
502                    .into(),
503            )
504        }
505    }
506
507    async fn prompt_pin(&mut self) -> Result<bool, Self::Error> {
508        if let common::Response::DeviceAction(success) =
509            run_command(self, common::Command::PromptPin).await?
510        {
511            Ok(success)
512        } else {
513            Err(
514                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
515                    .into(),
516            )
517        }
518    }
519
520    async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, Self::Error> {
521        if let common::Response::DeviceAction(success) =
522            run_command(self, common::Command::SendPin(context)).await?
523        {
524            Ok(success)
525        } else {
526            Err(
527                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
528                    .into(),
529            )
530        }
531    }
532
533    async fn unlock(&mut self, network: Network) -> Result<(), Self::Error> {
534        let res = run_command(
535            self,
536            common::Command::Unlock {
537                options: common::UnlockOptions {
538                    network: Some(network),
539                },
540            },
541        )
542        .await?;
543        self.on_unlock(res)?;
544        Ok(())
545    }
546
547    async fn get_info(&mut self) -> Result<Info, Self::Error> {
548        if let common::Response::Info(version) =
549            run_command(self, common::Command::GetVersion).await?
550        {
551            Ok(version)
552        } else {
553            Err(
554                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
555                    .into(),
556            )
557        }
558    }
559
560    async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, Self::Error> {
561        if let common::Response::MasterFingerprint(fg) =
562            run_command(self, common::Command::GetMasterFingerprint).await?
563        {
564            Ok(fg)
565        } else {
566            Err(
567                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
568                    .into(),
569            )
570        }
571    }
572
573    async fn get_extended_pubkey(
574        &mut self,
575        path: DerivationPath,
576        display: bool,
577    ) -> Result<Xpub, Self::Error> {
578        if let common::Response::Xpub(xpub) =
579            run_command(self, common::Command::GetXpub { path, display }).await?
580        {
581            Ok(xpub)
582        } else {
583            Err(
584                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
585                    .into(),
586            )
587        }
588    }
589
590    async fn sign_message(
591        &mut self,
592        message: &[u8],
593        path: DerivationPath,
594    ) -> Result<(u8, Signature), Self::Error> {
595        if let common::Response::Signature(header, signature) = run_command(
596            self,
597            common::Command::SignMessage {
598                message: message.to_vec(),
599                path,
600            },
601        )
602        .await?
603        {
604            Ok((header, signature))
605        } else {
606            Err(
607                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
608                    .into(),
609            )
610        }
611    }
612
613    async fn display_address(
614        &mut self,
615        address: common::DisplayAddress,
616        context: Option<common::DeviceContext>,
617    ) -> Result<String, Self::Error> {
618        if let common::Response::Address(addr) =
619            run_command(self, common::Command::DisplayAddress(address, context)).await?
620        {
621            Ok(addr)
622        } else {
623            Err(
624                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
625                    .into(),
626            )
627        }
628    }
629
630    async fn register_wallet(
631        &mut self,
632        name: &str,
633        policy: &str,
634    ) -> Result<WalletRegistration, Self::Error> {
635        let wallet_policy = WalletPolicy::from_str(policy)
636            .map_err(|e| common::Error::new(ErrorKind::Serialization, e.to_string()))?;
637        if let common::Response::WalletRegistration(registration) = run_command(
638            self,
639            common::Command::RegisterWallet {
640                name: name.to_string(),
641                policy: wallet_policy,
642            },
643        )
644        .await?
645        {
646            Ok(registration)
647        } else {
648            Err(
649                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
650                    .into(),
651            )
652        }
653    }
654
655    async fn sign_tx(
656        &mut self,
657        psbt: Psbt,
658        context: Option<common::DeviceContext>,
659    ) -> Result<Psbt, Self::Error> {
660        if let common::Response::SignedPsbt(psbt) =
661            run_command(self, common::Command::SignTx(psbt, context)).await?
662        {
663            Ok(psbt)
664        } else {
665            Err(
666                common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
667                    .into(),
668            )
669        }
670    }
671}
672
673#[async_trait(?Send)]
674impl<T> HWIDevice for T
675where
676    T: HWI,
677    T::Error: ErrorKindOf + StdError + Send + Sync + 'static,
678{
679    async fn backup_device(&mut self) -> Result<DeviceBackup, HWIDeviceError> {
680        HWI::backup_device(self).await.map_err(kinded)
681    }
682
683    async fn setup_device(
684        &mut self,
685        options: SetupOptions,
686        context: Option<DeviceContext>,
687    ) -> Result<bool, HWIDeviceError> {
688        HWI::setup_device(self, options, context)
689            .await
690            .map_err(kinded)
691    }
692
693    async fn wipe_device(&mut self) -> Result<bool, HWIDeviceError> {
694        HWI::wipe_device(self).await.map_err(kinded)
695    }
696
697    async fn restore_device(
698        &mut self,
699        options: RestoreOptions,
700        context: Option<DeviceContext>,
701    ) -> Result<bool, HWIDeviceError> {
702        HWI::restore_device(self, options, context)
703            .await
704            .map_err(kinded)
705    }
706
707    async fn toggle_passphrase(&mut self) -> Result<bool, HWIDeviceError> {
708        HWI::toggle_passphrase(self).await.map_err(kinded)
709    }
710
711    async fn prompt_pin(&mut self) -> Result<bool, HWIDeviceError> {
712        HWI::prompt_pin(self).await.map_err(kinded)
713    }
714
715    async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, HWIDeviceError> {
716        HWI::send_pin(self, context).await.map_err(kinded)
717    }
718
719    async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError> {
720        HWI::unlock(self, network).await.map_err(kinded)
721    }
722
723    async fn get_info(&mut self) -> Result<Info, HWIDeviceError> {
724        HWI::get_info(self).await.map_err(kinded)
725    }
726
727    async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
728        HWI::get_master_fingerprint(self).await.map_err(kinded)
729    }
730
731    async fn get_extended_pubkey(
732        &mut self,
733        path: DerivationPath,
734        display: bool,
735    ) -> Result<Xpub, HWIDeviceError> {
736        HWI::get_extended_pubkey(self, path, display)
737            .await
738            .map_err(kinded)
739    }
740
741    async fn sign_message(
742        &mut self,
743        message: &[u8],
744        path: DerivationPath,
745    ) -> Result<(u8, Signature), HWIDeviceError> {
746        HWI::sign_message(self, message, path).await.map_err(kinded)
747    }
748
749    async fn display_address(
750        &mut self,
751        address: DisplayAddress,
752        context: Option<DeviceContext>,
753    ) -> Result<String, HWIDeviceError> {
754        HWI::display_address(self, address, context)
755            .await
756            .map_err(kinded)
757    }
758
759    async fn register_wallet(
760        &mut self,
761        name: &str,
762        policy: &str,
763    ) -> Result<WalletRegistration, HWIDeviceError> {
764        HWI::register_wallet(self, name, policy)
765            .await
766            .map_err(kinded)
767    }
768
769    async fn sign_tx(
770        &mut self,
771        psbt: Psbt,
772        context: Option<common::DeviceContext>,
773    ) -> Result<Psbt, HWIDeviceError> {
774        HWI::sign_tx(self, psbt, context).await.map_err(kinded)
775    }
776}
777
778/// Updates persistent client state after the unlock interpreter completes.
779pub trait OnUnlock {
780    /// Applies the unlock response, or returns an error if it cannot be accepted.
781    fn on_unlock(&mut self, _response: common::Response) -> Result<(), common::Error>;
782}
783
784/// Supplies the I/O endpoints and interpreter used to drive a command.
785pub trait CommonInterface<C, T, R, E> {
786    /// The device transport's error type.
787    type TransportError: Debug;
788    /// The PIN-server client's error type.
789    type HttpClientError: Debug;
790
791    /// Borrows the transport, HTTP client, optional host input, and a fresh interpreter.
792    ///
793    /// Session state may be borrowed by the interpreter and retained between commands.
794    #[allow(clippy::type_complexity)]
795    fn components(
796        &mut self,
797    ) -> (
798        &mut (dyn Transport<Error = Self::TransportError> + '_),
799        &(dyn HttpClient<Error = Self::HttpClientError> + '_),
800        Option<&mut (dyn HostInteraction + 'static)>,
801        impl Interpreter<Command = C, Transmit = T, Response = R, Error = E>,
802    );
803}
804
805async fn run_command<D, C, E, F>(
806    device: &mut D,
807    command: C,
808) -> Result<common::Response, Error<E, F>>
809where
810    E: Debug,
811    F: Debug,
812    D: CommonInterface<
813            common::Command,
814            common::Transmit,
815            common::Response,
816            common::Error,
817            TransportError = E,
818            HttpClientError = F,
819        >,
820    C: Into<common::Command>,
821{
822    run_command_inner(device, command, false).await
823}
824
825async fn run_command_allowing_final_disconnect<D, C, E, F>(
826    device: &mut D,
827    command: C,
828) -> Result<common::Response, Error<E, F>>
829where
830    E: Debug,
831    F: Debug,
832    D: CommonInterface<
833            common::Command,
834            common::Transmit,
835            common::Response,
836            common::Error,
837            TransportError = E,
838            HttpClientError = F,
839        >,
840    C: Into<common::Command>,
841{
842    run_command_inner(device, command, true).await
843}
844
845async fn run_command_inner<D, C, E, F>(
846    device: &mut D,
847    command: C,
848    allow_final_disconnect: bool,
849) -> Result<common::Response, Error<E, F>>
850where
851    E: Debug,
852    F: Debug,
853    D: CommonInterface<
854            common::Command,
855            common::Transmit,
856            common::Response,
857            common::Error,
858            TransportError = E,
859            HttpClientError = F,
860        >,
861    C: Into<common::Command>,
862{
863    let (transport, http_client, mut host_interaction, mut intpr) = device.components();
864    let mut transmit = Some(intpr.start(command.into())?);
865    while let Some(t) = &transmit {
866        match &t.recipient {
867            common::Recipient::PinServer { url } => {
868                let res = http_client
869                    .request(url, &t.payload)
870                    .await
871                    .map_err(|error| Error::HttpClient {
872                        kind: http_client.error_kind(&error),
873                        error,
874                    })?;
875                transmit = intpr.exchange(res)?;
876            }
877            common::Recipient::Host(request) => {
878                let interaction = host_interaction.as_deref_mut().ok_or_else(|| {
879                    common::Error::new(ErrorKind::Unsupported, "host interaction required")
880                })?;
881                let response = interaction.respond(request).await?;
882                transmit = intpr.exchange(response.into_bytes_for(request)?)?;
883            }
884            common::Recipient::Device => {
885                let exchange = match transport.exchange(&t.payload, t.encrypted).await {
886                    Ok(exchange) => exchange,
887                    Err(error)
888                        if allow_final_disconnect && transport.is_post_write_disconnect(&error) =>
889                    {
890                        return Ok(common::Response::DeviceAction(true));
891                    }
892                    Err(error) => {
893                        return Err(Error::Transport {
894                            kind: transport.error_kind(&error),
895                            error,
896                        });
897                    }
898                };
899                transmit = intpr.exchange(exchange)?;
900            }
901        }
902    }
903    intpr.end().map_err(|e| e.into())
904}
905
906#[cfg(test)]
907mod tests {
908    use super::*;
909    use futures::executor::block_on;
910    use std::{
911        cell::{Cell, RefCell},
912        convert::Infallible,
913        rc::Rc,
914    };
915
916    struct ScriptedTransport {
917        calls: usize,
918        events: Rc<RefCell<Vec<&'static str>>>,
919    }
920
921    #[async_trait(?Send)]
922    impl Transport for ScriptedTransport {
923        type Error = Infallible;
924
925        async fn exchange(
926            &mut self,
927            command: &[u8],
928            encrypted: bool,
929        ) -> Result<Vec<u8>, Self::Error> {
930            self.events.borrow_mut().push("device");
931            self.calls += 1;
932            assert_eq!(command, &[0x01]);
933            assert!(!encrypted);
934            Ok(vec![0x02])
935        }
936    }
937
938    struct NoHttp;
939
940    #[async_trait(?Send)]
941    impl HttpClient for NoHttp {
942        type Error = Infallible;
943
944        async fn request(&self, _url: &str, _request: &[u8]) -> Result<Vec<u8>, Self::Error> {
945            panic!("host interaction must not use HTTP")
946        }
947    }
948
949    struct ScriptedHost {
950        response: Option<common::HostResponse>,
951        calls: Rc<Cell<usize>>,
952        events: Rc<RefCell<Vec<&'static str>>>,
953    }
954
955    #[async_trait(?Send)]
956    impl HostInteraction for ScriptedHost {
957        async fn respond(
958            &mut self,
959            request: &common::HostRequest,
960        ) -> Result<common::HostResponse, common::Error> {
961            assert_eq!(
962                request,
963                &common::HostRequest::PinMatrix {
964                    kind: common::PinMatrixRequestKind::Current,
965                }
966            );
967            self.events.borrow_mut().push("host");
968            self.calls.set(self.calls.get() + 1);
969            Ok(self.response.take().expect("one scripted host response"))
970        }
971    }
972
973    #[derive(Default)]
974    struct ScriptedInterpreter {
975        state: u8,
976        host_first: bool,
977    }
978
979    impl Interpreter for ScriptedInterpreter {
980        type Command = common::Command;
981        type Transmit = common::Transmit;
982        type Response = common::Response;
983        type Error = common::Error;
984
985        fn start(&mut self, command: Self::Command) -> Result<Self::Transmit, Self::Error> {
986            assert!(matches!(command, common::Command::GetVersion));
987            assert_eq!(self.state, 0);
988            self.state = 1;
989            if self.host_first {
990                Ok(common::HostRequest::PinMatrix {
991                    kind: common::PinMatrixRequestKind::Current,
992                }
993                .into())
994            } else {
995                Ok(vec![0x01].into())
996            }
997        }
998
999        fn exchange(&mut self, data: Vec<u8>) -> Result<Option<Self::Transmit>, Self::Error> {
1000            match (self.host_first, self.state) {
1001                (false, 1) => {
1002                    assert_eq!(data.as_slice(), &[0x02]);
1003                    self.state = 2;
1004                    Ok(Some(
1005                        common::HostRequest::PinMatrix {
1006                            kind: common::PinMatrixRequestKind::Current,
1007                        }
1008                        .into(),
1009                    ))
1010                }
1011                (false, 2) => {
1012                    assert_eq!(data.as_slice(), b"123");
1013                    self.state = 3;
1014                    Ok(None)
1015                }
1016                (true, 1) => {
1017                    assert_eq!(data.as_slice(), b"123");
1018                    self.state = 2;
1019                    Ok(Some(vec![0x01].into()))
1020                }
1021                (true, 2) => {
1022                    assert_eq!(data.as_slice(), &[0x02]);
1023                    self.state = 3;
1024                    Ok(None)
1025                }
1026                _ => panic!("unexpected scripted interpreter state"),
1027            }
1028        }
1029
1030        fn end(self) -> Result<Self::Response, Self::Error> {
1031            assert_eq!(self.state, 3);
1032            Ok(common::Response::DeviceAction(true))
1033        }
1034    }
1035
1036    struct ScriptedDevice {
1037        transport: ScriptedTransport,
1038        host_interaction: Option<Box<dyn HostInteraction>>,
1039        host_first: bool,
1040    }
1041
1042    impl CommonInterface<common::Command, common::Transmit, common::Response, common::Error>
1043        for ScriptedDevice
1044    {
1045        type TransportError = Infallible;
1046        type HttpClientError = Infallible;
1047
1048        fn components(
1049            &mut self,
1050        ) -> (
1051            &mut (dyn Transport<Error = Self::TransportError> + '_),
1052            &(dyn HttpClient<Error = Self::HttpClientError> + '_),
1053            Option<&mut (dyn HostInteraction + 'static)>,
1054            impl Interpreter<
1055                Command = common::Command,
1056                Transmit = common::Transmit,
1057                Response = common::Response,
1058                Error = common::Error,
1059            >,
1060        ) {
1061            (
1062                &mut self.transport,
1063                &NoHttp,
1064                self.host_interaction.as_deref_mut(),
1065                ScriptedInterpreter {
1066                    host_first: self.host_first,
1067                    ..Default::default()
1068                },
1069            )
1070        }
1071    }
1072
1073    fn scripted_device(
1074        response: Option<common::HostResponse>,
1075    ) -> (ScriptedDevice, Rc<Cell<usize>>) {
1076        scripted_device_with_sequence(response, false)
1077    }
1078
1079    fn scripted_device_with_sequence(
1080        response: Option<common::HostResponse>,
1081        host_first: bool,
1082    ) -> (ScriptedDevice, Rc<Cell<usize>>) {
1083        let calls = Rc::new(Cell::new(0));
1084        let events = Rc::new(RefCell::new(Vec::new()));
1085        let host_interaction = response.map(|response| {
1086            Box::new(ScriptedHost {
1087                response: Some(response),
1088                calls: Rc::clone(&calls),
1089                events: Rc::clone(&events),
1090            }) as Box<dyn HostInteraction>
1091        });
1092        (
1093            ScriptedDevice {
1094                transport: ScriptedTransport { calls: 0, events },
1095                host_interaction,
1096                host_first,
1097            },
1098            calls,
1099        )
1100    }
1101
1102    #[test]
1103    fn initial_host_recipient_uses_provider_before_device_exchange() {
1104        let (mut device, host_calls) = scripted_device_with_sequence(
1105            Some(common::HostResponse::PinPositions("123".into())),
1106            true,
1107        );
1108
1109        let response = block_on(run_command_inner(
1110            &mut device,
1111            common::Command::GetVersion,
1112            false,
1113        ))
1114        .expect("scripted command succeeds");
1115
1116        assert!(matches!(response, common::Response::DeviceAction(true)));
1117        assert_eq!(host_calls.get(), 1);
1118        assert_eq!(device.transport.calls, 1);
1119        assert_eq!(
1120            device.transport.events.borrow().as_slice(),
1121            &["host", "device"]
1122        );
1123    }
1124
1125    #[test]
1126    fn host_recipient_uses_provider_without_second_device_exchange() {
1127        let (mut device, host_calls) =
1128            scripted_device(Some(common::HostResponse::PinPositions("123".into())));
1129
1130        let response = block_on(run_command_inner(
1131            &mut device,
1132            common::Command::GetVersion,
1133            false,
1134        ))
1135        .expect("scripted command succeeds");
1136
1137        assert!(matches!(response, common::Response::DeviceAction(true)));
1138        assert_eq!(device.transport.calls, 1);
1139        assert_eq!(host_calls.get(), 1);
1140    }
1141
1142    #[test]
1143    fn host_recipient_requires_provider_without_second_device_exchange() {
1144        let (mut device, host_calls) = scripted_device(None);
1145
1146        let error = match block_on(run_command_inner(
1147            &mut device,
1148            common::Command::GetVersion,
1149            false,
1150        )) {
1151            Err(error) => error,
1152            Ok(_) => panic!("missing host provider succeeds"),
1153        };
1154
1155        assert!(matches!(
1156            error,
1157            Error::Interpreter(e) if e.kind() == common::ErrorKind::Unsupported
1158                && e.message() == "host interaction required"
1159        ));
1160        assert_eq!(device.transport.calls, 1);
1161        assert_eq!(host_calls.get(), 0);
1162    }
1163
1164    #[test]
1165    fn host_recipient_validates_response_without_second_device_exchange() {
1166        let (mut device, host_calls) =
1167            scripted_device(Some(common::HostResponse::RecoveryCharacter('a')));
1168
1169        let error = match block_on(run_command_inner(
1170            &mut device,
1171            common::Command::GetVersion,
1172            false,
1173        )) {
1174            Err(error) => error,
1175            Ok(_) => panic!("mismatched host response succeeds"),
1176        };
1177
1178        assert!(matches!(
1179            error,
1180            Error::Interpreter(e) if e.kind() == common::ErrorKind::InvalidInput
1181                && e.message() == "host response does not match request"
1182        ));
1183        assert_eq!(device.transport.calls, 1);
1184        assert_eq!(host_calls.get(), 1);
1185    }
1186}