#[cfg(feature = "bitbox")]
pub mod bitbox;
#[cfg(feature = "coldcard")]
pub mod coldcard;
pub mod descriptors;
pub mod device;
pub mod display_address;
#[cfg(feature = "jade")]
pub mod jade;
#[cfg(feature = "keepkey")]
pub mod keepkey;
#[cfg(feature = "ledger")]
pub mod ledger;
#[cfg(any(feature = "bitbox", feature = "keepkey", feature = "trezor"))]
pub mod management;
pub mod psbt;
pub mod signing;
#[cfg(feature = "specter")]
pub mod specter;
pub mod transport;
#[cfg(feature = "trezor")]
pub mod trezor;
use std::{error::Error as StdError, fmt::Debug, str::FromStr};
use async_trait::async_trait;
pub use bhwi::common::DeviceBackup;
pub use bhwi::common::DeviceContext;
pub use bhwi::common::DisplayAddress;
pub use bhwi::common::ErrorKind;
pub use bhwi::common::Info;
pub use bhwi::common::RestoreOptions;
pub use bhwi::common::SetupOptions;
pub use bhwi::common::WalletRegistration;
pub use bhwi::common::{Error as HostError, HostRequest, HostResponse, PinMatrixRequestKind};
pub use bhwi::common::{MultisigAddressType, MultisigDisplayAddress};
use bhwi::miniscript::descriptor::WalletPolicy;
#[cfg(any(feature = "trezor", feature = "keepkey"))]
pub use bhwi::passphrase::HostPassphrase;
use bhwi::{
Interpreter,
bitcoin::{
Network,
bip32::{DerivationPath, Fingerprint, Xpub},
psbt::Psbt,
secp256k1::ecdsa::Signature,
},
common::{self},
};
#[cfg(feature = "jade")]
pub use jade::Jade;
#[cfg(feature = "keepkey")]
pub use keepkey::KeepKey;
#[cfg(feature = "ledger")]
pub use ledger::Ledger;
#[cfg(feature = "specter")]
pub use specter::Specter;
#[cfg(feature = "trezor")]
pub use trezor::Trezor;
#[async_trait(?Send)]
pub trait Transport {
type Error: Debug;
async fn exchange(&mut self, command: &[u8], encrypted: bool) -> Result<Vec<u8>, Self::Error>;
fn is_post_write_disconnect(&self, _error: &Self::Error) -> bool {
false
}
fn error_kind(&self, _error: &Self::Error) -> ErrorKind {
ErrorKind::Transport
}
}
#[async_trait(?Send)]
pub trait HttpClient {
type Error: Debug;
async fn request(&self, url: &str, request: &[u8]) -> Result<Vec<u8>, Self::Error>;
fn error_kind(&self, _error: &Self::Error) -> ErrorKind {
ErrorKind::Transport
}
}
#[async_trait(?Send)]
pub trait HostInteraction {
async fn respond(
&mut self,
request: &common::HostRequest,
) -> Result<common::HostResponse, common::Error>;
}
#[async_trait(?Send)]
pub trait HWI {
type Error: Debug;
async fn backup_device(&mut self) -> Result<DeviceBackup, Self::Error>;
async fn setup_device(
&mut self,
options: SetupOptions,
context: Option<DeviceContext>,
) -> Result<bool, Self::Error>;
async fn wipe_device(&mut self) -> Result<bool, Self::Error>;
async fn restore_device(
&mut self,
options: RestoreOptions,
context: Option<DeviceContext>,
) -> Result<bool, Self::Error>;
async fn toggle_passphrase(&mut self) -> Result<bool, Self::Error>;
async fn prompt_pin(&mut self) -> Result<bool, Self::Error>;
async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, Self::Error>;
async fn unlock(&mut self, network: Network) -> Result<(), Self::Error>;
async fn get_info(&mut self) -> Result<Info, Self::Error>;
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, Self::Error>;
async fn get_extended_pubkey(
&mut self,
path: DerivationPath,
display: bool,
) -> Result<Xpub, Self::Error>;
async fn sign_message(
&mut self,
message: &[u8],
path: DerivationPath,
) -> Result<(u8, Signature), Self::Error>;
async fn display_address(
&mut self,
address: common::DisplayAddress,
context: Option<common::DeviceContext>,
) -> Result<String, Self::Error>;
async fn register_wallet(
&mut self,
name: &str,
policy: &str,
) -> Result<WalletRegistration, Self::Error>;
async fn sign_tx(
&mut self,
psbt: Psbt,
context: Option<common::DeviceContext>,
) -> Result<Psbt, Self::Error>;
}
#[async_trait(?Send)]
pub trait HWIDevice {
async fn backup_device(&mut self) -> Result<DeviceBackup, HWIDeviceError>;
async fn setup_device(
&mut self,
options: SetupOptions,
context: Option<DeviceContext>,
) -> Result<bool, HWIDeviceError>;
async fn wipe_device(&mut self) -> Result<bool, HWIDeviceError>;
async fn restore_device(
&mut self,
options: RestoreOptions,
context: Option<DeviceContext>,
) -> Result<bool, HWIDeviceError>;
async fn toggle_passphrase(&mut self) -> Result<bool, HWIDeviceError>;
async fn prompt_pin(&mut self) -> Result<bool, HWIDeviceError>;
async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, HWIDeviceError>;
async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError>;
async fn get_info(&mut self) -> Result<Info, HWIDeviceError>;
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError>;
async fn get_extended_pubkey(
&mut self,
path: DerivationPath,
display: bool,
) -> Result<Xpub, HWIDeviceError>;
async fn sign_message(
&mut self,
message: &[u8],
path: DerivationPath,
) -> Result<(u8, Signature), HWIDeviceError>;
async fn display_address(
&mut self,
address: common::DisplayAddress,
context: Option<common::DeviceContext>,
) -> Result<String, HWIDeviceError>;
async fn register_wallet(
&mut self,
name: &str,
policy: &str,
) -> Result<WalletRegistration, HWIDeviceError>;
async fn sign_tx(
&mut self,
psbt: Psbt,
context: Option<common::DeviceContext>,
) -> Result<Psbt, HWIDeviceError>;
}
#[derive(Debug, thiserror::Error)]
#[error("{error}")]
pub struct HWIDeviceError {
#[source]
error: Box<dyn StdError + Send + Sync + 'static>,
kind: Option<ErrorKind>,
}
impl HWIDeviceError {
pub fn new(error: impl StdError + Send + Sync + 'static) -> Self {
Self {
error: Box::new(error),
kind: None,
}
}
pub fn with_kind(error: impl StdError + Send + Sync + 'static, kind: ErrorKind) -> Self {
Self {
kind: Some(kind),
..Self::new(error)
}
}
pub fn kind(&self) -> Option<ErrorKind> {
if self.kind.is_some() {
return self.kind;
}
let mut source: Option<&(dyn StdError + 'static)> = Some(self.error.as_ref());
while let Some(current) = source {
if let Some(error) = current.downcast_ref::<common::Error>() {
return Some(error.kind());
}
if let Some(kind) = current
.downcast_ref::<HWIDeviceError>()
.and_then(Self::kind)
{
return Some(kind);
}
source = current.source();
}
None
}
}
impl From<Box<dyn StdError + Send + Sync + 'static>> for HWIDeviceError {
fn from(error: Box<dyn StdError + Send + Sync + 'static>) -> Self {
Self { error, kind: None }
}
}
pub trait ErrorKindOf {
fn error_kind(&self) -> ErrorKind;
}
impl ErrorKindOf for common::Error {
fn error_kind(&self) -> ErrorKind {
self.kind()
}
}
#[derive(Debug, thiserror::Error)]
pub enum Error<E, F> {
#[error("transport error: {error}")]
Transport {
kind: ErrorKind,
error: E,
},
#[error("http client error: {error}")]
HttpClient {
kind: ErrorKind,
error: F,
},
#[error("{0}")]
Interpreter(
#[from]
common::Error,
),
}
impl<E, F> Error<E, F> {
pub fn kind(&self) -> ErrorKind {
match self {
Self::Transport { kind, .. } | Self::HttpClient { kind, .. } => *kind,
Self::Interpreter(error) => error.kind(),
}
}
}
impl<E, F> ErrorKindOf for Error<E, F> {
fn error_kind(&self) -> ErrorKind {
self.kind()
}
}
fn kinded<E>(error: E) -> HWIDeviceError
where
E: ErrorKindOf + StdError + Send + Sync + 'static,
{
let kind = error.error_kind();
HWIDeviceError::with_kind(error, kind)
}
#[async_trait(?Send)]
impl<D> HWI for D
where
D: CommonInterface<common::Command, common::Transmit, common::Response, common::Error>
+ OnUnlock,
{
type Error = Error<D::TransportError, D::HttpClientError>;
async fn backup_device(&mut self) -> Result<DeviceBackup, Self::Error> {
if let common::Response::Backup(backup) = run_command(self, common::Command::Backup).await?
{
Ok(backup)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn setup_device(
&mut self,
options: SetupOptions,
context: Option<DeviceContext>,
) -> Result<bool, Self::Error> {
if let common::Response::DeviceAction(success) =
run_command(self, common::Command::Setup(options, context)).await?
{
Ok(success)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn wipe_device(&mut self) -> Result<bool, Self::Error> {
if let common::Response::DeviceAction(success) =
run_command_allowing_final_disconnect(self, common::Command::Wipe).await?
{
Ok(success)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn restore_device(
&mut self,
options: RestoreOptions,
context: Option<DeviceContext>,
) -> Result<bool, Self::Error> {
if let common::Response::DeviceAction(success) =
run_command(self, common::Command::Restore(options, context)).await?
{
Ok(success)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn toggle_passphrase(&mut self) -> Result<bool, Self::Error> {
if let common::Response::DeviceAction(success) =
run_command(self, common::Command::TogglePassphrase).await?
{
Ok(success)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn prompt_pin(&mut self) -> Result<bool, Self::Error> {
if let common::Response::DeviceAction(success) =
run_command(self, common::Command::PromptPin).await?
{
Ok(success)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, Self::Error> {
if let common::Response::DeviceAction(success) =
run_command(self, common::Command::SendPin(context)).await?
{
Ok(success)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn unlock(&mut self, network: Network) -> Result<(), Self::Error> {
let res = run_command(
self,
common::Command::Unlock {
options: common::UnlockOptions {
network: Some(network),
},
},
)
.await?;
self.on_unlock(res)?;
Ok(())
}
async fn get_info(&mut self) -> Result<Info, Self::Error> {
if let common::Response::Info(version) =
run_command(self, common::Command::GetVersion).await?
{
Ok(version)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, Self::Error> {
if let common::Response::MasterFingerprint(fg) =
run_command(self, common::Command::GetMasterFingerprint).await?
{
Ok(fg)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn get_extended_pubkey(
&mut self,
path: DerivationPath,
display: bool,
) -> Result<Xpub, Self::Error> {
if let common::Response::Xpub(xpub) =
run_command(self, common::Command::GetXpub { path, display }).await?
{
Ok(xpub)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn sign_message(
&mut self,
message: &[u8],
path: DerivationPath,
) -> Result<(u8, Signature), Self::Error> {
if let common::Response::Signature(header, signature) = run_command(
self,
common::Command::SignMessage {
message: message.to_vec(),
path,
},
)
.await?
{
Ok((header, signature))
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn display_address(
&mut self,
address: common::DisplayAddress,
context: Option<common::DeviceContext>,
) -> Result<String, Self::Error> {
if let common::Response::Address(addr) =
run_command(self, common::Command::DisplayAddress(address, context)).await?
{
Ok(addr)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn register_wallet(
&mut self,
name: &str,
policy: &str,
) -> Result<WalletRegistration, Self::Error> {
let wallet_policy = WalletPolicy::from_str(policy)
.map_err(|e| common::Error::new(ErrorKind::Serialization, e.to_string()))?;
if let common::Response::WalletRegistration(registration) = run_command(
self,
common::Command::RegisterWallet {
name: name.to_string(),
policy: wallet_policy,
},
)
.await?
{
Ok(registration)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
async fn sign_tx(
&mut self,
psbt: Psbt,
context: Option<common::DeviceContext>,
) -> Result<Psbt, Self::Error> {
if let common::Response::SignedPsbt(psbt) =
run_command(self, common::Command::SignTx(psbt, context)).await?
{
Ok(psbt)
} else {
Err(
common::Error::new(ErrorKind::UnexpectedResponse, "no error or result returned")
.into(),
)
}
}
}
#[async_trait(?Send)]
impl<T> HWIDevice for T
where
T: HWI,
T::Error: ErrorKindOf + StdError + Send + Sync + 'static,
{
async fn backup_device(&mut self) -> Result<DeviceBackup, HWIDeviceError> {
HWI::backup_device(self).await.map_err(kinded)
}
async fn setup_device(
&mut self,
options: SetupOptions,
context: Option<DeviceContext>,
) -> Result<bool, HWIDeviceError> {
HWI::setup_device(self, options, context)
.await
.map_err(kinded)
}
async fn wipe_device(&mut self) -> Result<bool, HWIDeviceError> {
HWI::wipe_device(self).await.map_err(kinded)
}
async fn restore_device(
&mut self,
options: RestoreOptions,
context: Option<DeviceContext>,
) -> Result<bool, HWIDeviceError> {
HWI::restore_device(self, options, context)
.await
.map_err(kinded)
}
async fn toggle_passphrase(&mut self) -> Result<bool, HWIDeviceError> {
HWI::toggle_passphrase(self).await.map_err(kinded)
}
async fn prompt_pin(&mut self) -> Result<bool, HWIDeviceError> {
HWI::prompt_pin(self).await.map_err(kinded)
}
async fn send_pin(&mut self, context: Option<DeviceContext>) -> Result<bool, HWIDeviceError> {
HWI::send_pin(self, context).await.map_err(kinded)
}
async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError> {
HWI::unlock(self, network).await.map_err(kinded)
}
async fn get_info(&mut self) -> Result<Info, HWIDeviceError> {
HWI::get_info(self).await.map_err(kinded)
}
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
HWI::get_master_fingerprint(self).await.map_err(kinded)
}
async fn get_extended_pubkey(
&mut self,
path: DerivationPath,
display: bool,
) -> Result<Xpub, HWIDeviceError> {
HWI::get_extended_pubkey(self, path, display)
.await
.map_err(kinded)
}
async fn sign_message(
&mut self,
message: &[u8],
path: DerivationPath,
) -> Result<(u8, Signature), HWIDeviceError> {
HWI::sign_message(self, message, path).await.map_err(kinded)
}
async fn display_address(
&mut self,
address: DisplayAddress,
context: Option<DeviceContext>,
) -> Result<String, HWIDeviceError> {
HWI::display_address(self, address, context)
.await
.map_err(kinded)
}
async fn register_wallet(
&mut self,
name: &str,
policy: &str,
) -> Result<WalletRegistration, HWIDeviceError> {
HWI::register_wallet(self, name, policy)
.await
.map_err(kinded)
}
async fn sign_tx(
&mut self,
psbt: Psbt,
context: Option<common::DeviceContext>,
) -> Result<Psbt, HWIDeviceError> {
HWI::sign_tx(self, psbt, context).await.map_err(kinded)
}
}
pub trait OnUnlock {
fn on_unlock(&mut self, _response: common::Response) -> Result<(), common::Error>;
}
pub trait CommonInterface<C, T, R, E> {
type TransportError: Debug;
type HttpClientError: Debug;
#[allow(clippy::type_complexity)]
fn components(
&mut self,
) -> (
&mut (dyn Transport<Error = Self::TransportError> + '_),
&(dyn HttpClient<Error = Self::HttpClientError> + '_),
Option<&mut (dyn HostInteraction + 'static)>,
impl Interpreter<Command = C, Transmit = T, Response = R, Error = E>,
);
}
async fn run_command<D, C, E, F>(
device: &mut D,
command: C,
) -> Result<common::Response, Error<E, F>>
where
E: Debug,
F: Debug,
D: CommonInterface<
common::Command,
common::Transmit,
common::Response,
common::Error,
TransportError = E,
HttpClientError = F,
>,
C: Into<common::Command>,
{
run_command_inner(device, command, false).await
}
async fn run_command_allowing_final_disconnect<D, C, E, F>(
device: &mut D,
command: C,
) -> Result<common::Response, Error<E, F>>
where
E: Debug,
F: Debug,
D: CommonInterface<
common::Command,
common::Transmit,
common::Response,
common::Error,
TransportError = E,
HttpClientError = F,
>,
C: Into<common::Command>,
{
run_command_inner(device, command, true).await
}
async fn run_command_inner<D, C, E, F>(
device: &mut D,
command: C,
allow_final_disconnect: bool,
) -> Result<common::Response, Error<E, F>>
where
E: Debug,
F: Debug,
D: CommonInterface<
common::Command,
common::Transmit,
common::Response,
common::Error,
TransportError = E,
HttpClientError = F,
>,
C: Into<common::Command>,
{
let (transport, http_client, mut host_interaction, mut intpr) = device.components();
let mut transmit = Some(intpr.start(command.into())?);
while let Some(t) = &transmit {
match &t.recipient {
common::Recipient::PinServer { url } => {
let res = http_client
.request(url, &t.payload)
.await
.map_err(|error| Error::HttpClient {
kind: http_client.error_kind(&error),
error,
})?;
transmit = intpr.exchange(res)?;
}
common::Recipient::Host(request) => {
let interaction = host_interaction.as_deref_mut().ok_or_else(|| {
common::Error::new(ErrorKind::Unsupported, "host interaction required")
})?;
let response = interaction.respond(request).await?;
transmit = intpr.exchange(response.into_bytes_for(request)?)?;
}
common::Recipient::Device => {
let exchange = match transport.exchange(&t.payload, t.encrypted).await {
Ok(exchange) => exchange,
Err(error)
if allow_final_disconnect && transport.is_post_write_disconnect(&error) =>
{
return Ok(common::Response::DeviceAction(true));
}
Err(error) => {
return Err(Error::Transport {
kind: transport.error_kind(&error),
error,
});
}
};
transmit = intpr.exchange(exchange)?;
}
}
}
intpr.end().map_err(|e| e.into())
}
#[cfg(test)]
mod tests {
use super::*;
use futures::executor::block_on;
use std::{
cell::{Cell, RefCell},
convert::Infallible,
rc::Rc,
};
struct ScriptedTransport {
calls: usize,
events: Rc<RefCell<Vec<&'static str>>>,
}
#[async_trait(?Send)]
impl Transport for ScriptedTransport {
type Error = Infallible;
async fn exchange(
&mut self,
command: &[u8],
encrypted: bool,
) -> Result<Vec<u8>, Self::Error> {
self.events.borrow_mut().push("device");
self.calls += 1;
assert_eq!(command, &[0x01]);
assert!(!encrypted);
Ok(vec![0x02])
}
}
struct NoHttp;
#[async_trait(?Send)]
impl HttpClient for NoHttp {
type Error = Infallible;
async fn request(&self, _url: &str, _request: &[u8]) -> Result<Vec<u8>, Self::Error> {
panic!("host interaction must not use HTTP")
}
}
struct ScriptedHost {
response: Option<common::HostResponse>,
calls: Rc<Cell<usize>>,
events: Rc<RefCell<Vec<&'static str>>>,
}
#[async_trait(?Send)]
impl HostInteraction for ScriptedHost {
async fn respond(
&mut self,
request: &common::HostRequest,
) -> Result<common::HostResponse, common::Error> {
assert_eq!(
request,
&common::HostRequest::PinMatrix {
kind: common::PinMatrixRequestKind::Current,
}
);
self.events.borrow_mut().push("host");
self.calls.set(self.calls.get() + 1);
Ok(self.response.take().expect("one scripted host response"))
}
}
#[derive(Default)]
struct ScriptedInterpreter {
state: u8,
host_first: bool,
}
impl Interpreter for ScriptedInterpreter {
type Command = common::Command;
type Transmit = common::Transmit;
type Response = common::Response;
type Error = common::Error;
fn start(&mut self, command: Self::Command) -> Result<Self::Transmit, Self::Error> {
assert!(matches!(command, common::Command::GetVersion));
assert_eq!(self.state, 0);
self.state = 1;
if self.host_first {
Ok(common::HostRequest::PinMatrix {
kind: common::PinMatrixRequestKind::Current,
}
.into())
} else {
Ok(vec![0x01].into())
}
}
fn exchange(&mut self, data: Vec<u8>) -> Result<Option<Self::Transmit>, Self::Error> {
match (self.host_first, self.state) {
(false, 1) => {
assert_eq!(data.as_slice(), &[0x02]);
self.state = 2;
Ok(Some(
common::HostRequest::PinMatrix {
kind: common::PinMatrixRequestKind::Current,
}
.into(),
))
}
(false, 2) => {
assert_eq!(data.as_slice(), b"123");
self.state = 3;
Ok(None)
}
(true, 1) => {
assert_eq!(data.as_slice(), b"123");
self.state = 2;
Ok(Some(vec![0x01].into()))
}
(true, 2) => {
assert_eq!(data.as_slice(), &[0x02]);
self.state = 3;
Ok(None)
}
_ => panic!("unexpected scripted interpreter state"),
}
}
fn end(self) -> Result<Self::Response, Self::Error> {
assert_eq!(self.state, 3);
Ok(common::Response::DeviceAction(true))
}
}
struct ScriptedDevice {
transport: ScriptedTransport,
host_interaction: Option<Box<dyn HostInteraction>>,
host_first: bool,
}
impl CommonInterface<common::Command, common::Transmit, common::Response, common::Error>
for ScriptedDevice
{
type TransportError = Infallible;
type HttpClientError = Infallible;
fn components(
&mut self,
) -> (
&mut (dyn Transport<Error = Self::TransportError> + '_),
&(dyn HttpClient<Error = Self::HttpClientError> + '_),
Option<&mut (dyn HostInteraction + 'static)>,
impl Interpreter<
Command = common::Command,
Transmit = common::Transmit,
Response = common::Response,
Error = common::Error,
>,
) {
(
&mut self.transport,
&NoHttp,
self.host_interaction.as_deref_mut(),
ScriptedInterpreter {
host_first: self.host_first,
..Default::default()
},
)
}
}
fn scripted_device(
response: Option<common::HostResponse>,
) -> (ScriptedDevice, Rc<Cell<usize>>) {
scripted_device_with_sequence(response, false)
}
fn scripted_device_with_sequence(
response: Option<common::HostResponse>,
host_first: bool,
) -> (ScriptedDevice, Rc<Cell<usize>>) {
let calls = Rc::new(Cell::new(0));
let events = Rc::new(RefCell::new(Vec::new()));
let host_interaction = response.map(|response| {
Box::new(ScriptedHost {
response: Some(response),
calls: Rc::clone(&calls),
events: Rc::clone(&events),
}) as Box<dyn HostInteraction>
});
(
ScriptedDevice {
transport: ScriptedTransport { calls: 0, events },
host_interaction,
host_first,
},
calls,
)
}
#[test]
fn initial_host_recipient_uses_provider_before_device_exchange() {
let (mut device, host_calls) = scripted_device_with_sequence(
Some(common::HostResponse::PinPositions("123".into())),
true,
);
let response = block_on(run_command_inner(
&mut device,
common::Command::GetVersion,
false,
))
.expect("scripted command succeeds");
assert!(matches!(response, common::Response::DeviceAction(true)));
assert_eq!(host_calls.get(), 1);
assert_eq!(device.transport.calls, 1);
assert_eq!(
device.transport.events.borrow().as_slice(),
&["host", "device"]
);
}
#[test]
fn host_recipient_uses_provider_without_second_device_exchange() {
let (mut device, host_calls) =
scripted_device(Some(common::HostResponse::PinPositions("123".into())));
let response = block_on(run_command_inner(
&mut device,
common::Command::GetVersion,
false,
))
.expect("scripted command succeeds");
assert!(matches!(response, common::Response::DeviceAction(true)));
assert_eq!(device.transport.calls, 1);
assert_eq!(host_calls.get(), 1);
}
#[test]
fn host_recipient_requires_provider_without_second_device_exchange() {
let (mut device, host_calls) = scripted_device(None);
let error = match block_on(run_command_inner(
&mut device,
common::Command::GetVersion,
false,
)) {
Err(error) => error,
Ok(_) => panic!("missing host provider succeeds"),
};
assert!(matches!(
error,
Error::Interpreter(e) if e.kind() == common::ErrorKind::Unsupported
&& e.message() == "host interaction required"
));
assert_eq!(device.transport.calls, 1);
assert_eq!(host_calls.get(), 0);
}
#[test]
fn host_recipient_validates_response_without_second_device_exchange() {
let (mut device, host_calls) =
scripted_device(Some(common::HostResponse::RecoveryCharacter('a')));
let error = match block_on(run_command_inner(
&mut device,
common::Command::GetVersion,
false,
)) {
Err(error) => error,
Ok(_) => panic!("mismatched host response succeeds"),
};
assert!(matches!(
error,
Error::Interpreter(e) if e.kind() == common::ErrorKind::InvalidInput
&& e.message() == "host response does not match request"
));
assert_eq!(device.transport.calls, 1);
assert_eq!(host_calls.get(), 1);
}
}