use core::fmt;
use std::rc::Rc;
use std::sync::Arc;
use async_trait::async_trait;
use bhwi::bitcoin::{Network, bip32::Fingerprint};
use bhwi::common::{self, ErrorKind};
#[cfg(any(feature = "trezor", feature = "keepkey"))]
use bhwi::passphrase::HostPassphrase;
use crate::{ErrorKindOf, HWIDevice, HWIDeviceError, Info};
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum DeviceType {
BitBox02,
Coldcard,
Jade,
KeepKey,
Ledger,
Specter,
Trezor,
}
impl DeviceType {
pub const ALL: [DeviceType; 7] = [
DeviceType::BitBox02,
DeviceType::Coldcard,
DeviceType::Jade,
DeviceType::KeepKey,
DeviceType::Ledger,
DeviceType::Specter,
DeviceType::Trezor,
];
pub fn as_str(self) -> &'static str {
match self {
DeviceType::BitBox02 => "bitbox02",
DeviceType::Coldcard => "coldcard",
DeviceType::Jade => "jade",
DeviceType::KeepKey => "keepkey",
DeviceType::Ledger => "ledger",
DeviceType::Specter => "specter",
DeviceType::Trezor => "trezor",
}
}
}
impl fmt::Display for DeviceType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
pub fn supports_multisig_display_address(device_type: DeviceType) -> bool {
matches!(
device_type,
DeviceType::Coldcard | DeviceType::Jade | DeviceType::KeepKey | DeviceType::Trezor
)
}
pub fn can_sign_taproot(device_type: DeviceType, model: &str) -> bool {
match device_type {
DeviceType::BitBox02 => false,
DeviceType::Ledger => true,
DeviceType::Jade => false,
DeviceType::KeepKey => false,
DeviceType::Coldcard => model.contains("edge"),
DeviceType::Specter => false,
DeviceType::Trezor => model != "trezor_one",
}
}
pub fn reports_device_info(device_type: DeviceType) -> bool {
matches!(device_type, DeviceType::KeepKey | DeviceType::Trezor)
}
#[derive(Debug, Clone)]
pub struct DeviceSelector {
pub network: Network,
pub fingerprint: Option<Fingerprint>,
pub device_type: Option<DeviceType>,
pub device_path: Option<String>,
pub include_emulators: bool,
#[cfg(any(feature = "trezor", feature = "keepkey"))]
pub passphrase: Option<HostPassphrase>,
}
impl Default for DeviceSelector {
fn default() -> Self {
Self {
network: Network::Bitcoin,
fingerprint: None,
device_type: None,
device_path: None,
include_emulators: false,
#[cfg(any(feature = "trezor", feature = "keepkey"))]
passphrase: None,
}
}
}
impl DeviceSelector {
pub fn matches(&self, device_type: DeviceType, path: &str) -> bool {
self.device_type.is_none_or(|target| target == device_type)
&& self
.device_path
.as_ref()
.is_none_or(|target| target == path)
}
}
pub type PairingCodePrompt = Arc<dyn Fn(&str) + Send + Sync>;
#[derive(Debug, Clone)]
pub struct DeviceCandidate {
pub device_type: DeviceType,
pub name: String,
pub model: String,
pub path: String,
pub is_emulated: bool,
}
pub type HostInteractionFactory = Rc<dyn Fn() -> Box<dyn crate::HostInteraction>>;
#[async_trait(?Send)]
pub trait DeviceSource {
type Error: std::error::Error + ErrorKindOf + 'static;
async fn list(&self, selector: &DeviceSelector) -> Result<Vec<DeviceCandidate>, Self::Error>;
async fn open(
&self,
candidate: &DeviceCandidate,
selector: &DeviceSelector,
pairing_code: Option<&PairingCodePrompt>,
host_interaction: Option<&HostInteractionFactory>,
) -> Result<Device, Self::Error>;
}
#[derive(Debug, thiserror::Error)]
pub enum SelectError<E> {
#[error(transparent)]
Source(
E,
),
#[error(transparent)]
Device(
#[from]
HWIDeviceError,
),
#[error(transparent)]
NoUsableDevice(
#[from]
NoUsableDevice,
),
#[cfg(feature = "bitbox")]
#[error("{}", crate::bitbox::HOST_PASSPHRASE_REJECTED)]
HostPassphraseRejected,
}
pub struct DeviceManager<S> {
pub selector: DeviceSelector,
source: S,
pairing_code: Option<PairingCodePrompt>,
host_interaction: Option<HostInteractionFactory>,
}
impl<S: DeviceSource> DeviceManager<S> {
pub fn new(source: S, selector: DeviceSelector) -> Self {
Self {
selector,
source,
pairing_code: None,
host_interaction: None,
}
}
pub fn with_pairing_code_prompt(mut self, prompt: PairingCodePrompt) -> Self {
self.pairing_code = Some(prompt);
self
}
pub fn with_host_interaction(mut self, factory: HostInteractionFactory) -> Self {
self.host_interaction = Some(factory);
self
}
pub async fn list(&self) -> Result<Vec<DeviceCandidate>, S::Error> {
self.source.list(&self.selector).await
}
pub async fn open(&self, candidate: &DeviceCandidate) -> Result<Device, S::Error> {
self.source
.open(
candidate,
&self.selector,
self.pairing_code.as_ref(),
self.host_interaction.as_ref(),
)
.await
}
pub async fn enumerate(&self) -> Result<DeviceScan, S::Error> {
let mut scan = DeviceScan::default();
for candidate in self.list().await? {
match self.open(&candidate).await {
Ok(device) => scan.devices.push(device),
Err(err) => scan.skipped.push(unopened_candidate(&candidate, &err)),
}
}
Ok(scan)
}
pub async fn select(
&self,
) -> Result<(Option<Device>, Vec<SkippedDevice>), SelectError<S::Error>> {
let candidates = self.list().await.map_err(SelectError::Source)?;
let mut skipped = Vec::new();
let mut target_dev = None;
for candidate in candidates {
let mut d = match self.open(&candidate).await {
Ok(device) => device,
Err(err) => {
skipped.push(unopened_candidate(&candidate, &err));
continue;
}
};
if let Err(err) = d.device().unlock(self.selector.network).await {
if err.kind() == Some(ErrorKind::UserCancelled)
|| self.selector.fingerprint.is_none()
{
return Err(err.into());
}
skipped.push(skipped_candidate(&candidate, &err));
continue;
}
let Some(fingerprint) = self.selector.fingerprint else {
target_dev = Some(d);
break;
};
match d.fingerprint().await {
Ok(found) if found == fingerprint => {
target_dev = Some(d);
break;
}
Ok(_) => {}
Err(err) => {
if err.kind() == Some(ErrorKind::UserCancelled) {
return Err(err.into());
}
skipped.push(skipped_candidate(&candidate, &err));
}
}
}
let Some(dev) = target_dev else {
return Ok((None, skipped));
};
#[cfg(all(feature = "bitbox", any(feature = "trezor", feature = "keepkey")))]
if self.selector.passphrase.is_some() && dev.device_type() == DeviceType::BitBox02 {
return Err(SelectError::HostPassphraseRejected);
}
Ok((Some(dev), skipped))
}
pub async fn scan(&self) -> Result<DeviceScan, SelectError<S::Error>> {
let candidates = self.list().await.map_err(SelectError::Source)?;
let mut scan = DeviceScan::default();
for candidate in candidates {
let mut device = match self.open(&candidate).await {
Ok(device) => device,
Err(err) => {
scan.skipped.push(unopened_candidate(&candidate, &err));
continue;
}
};
match probe(&mut device, self.selector.network).await {
Ok(()) => scan.devices.push(device),
Err(err) if err.kind() == Some(ErrorKind::UserCancelled) => return Err(err.into()),
Err(err) => scan.skipped.push(skipped_candidate(&candidate, &err)),
}
}
Ok(scan)
}
pub async fn get_device_with_fingerprint(
&self,
) -> Result<Option<Device>, SelectError<S::Error>> {
let (device, skipped) = self.select().await?;
match device {
Some(device) => Ok(Some(device)),
None => Ok(no_device(skipped)?),
}
}
pub async fn get_device_without_contacting(
&self,
) -> Result<Option<Device>, SelectError<S::Error>> {
let scan = self.enumerate().await.map_err(SelectError::Source)?;
match scan.devices.into_iter().next() {
Some(device) => Ok(Some(device)),
None => Ok(no_device(scan.skipped)?),
}
}
}
#[derive(Debug)]
pub struct NoUsableDevice {
pub skipped: Vec<SkippedDevice>,
}
impl fmt::Display for NoUsableDevice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let reasons = self
.skipped
.iter()
.map(|s| format!("{} at {}: {}", s.device_type, s.path, s.error))
.collect::<Vec<_>>()
.join("; ");
write!(f, "no usable device found, could not open: {reasons}")
}
}
impl std::error::Error for NoUsableDevice {}
fn skipped_candidate(
candidate: &DeviceCandidate,
error: &(dyn std::error::Error + 'static),
) -> SkippedDevice {
SkippedDevice::new(
candidate.device_type,
&candidate.model,
&candidate.path,
error,
)
}
fn unopened_candidate<E>(candidate: &DeviceCandidate, error: &E) -> SkippedDevice
where
E: std::error::Error + ErrorKindOf + 'static,
{
SkippedDevice {
device_type: candidate.device_type,
model: candidate.model.clone(),
path: candidate.path.clone(),
error: classify(error, Some(error.error_kind())),
}
}
async fn probe(device: &mut Device, network: Network) -> Result<(), HWIDeviceError> {
device.device().unlock(network).await?;
let info = device.info().await?;
if info.initialized != Some(false) {
device.fingerprint().await?;
}
Ok(())
}
pub fn no_device(skipped: Vec<SkippedDevice>) -> Result<Option<Device>, NoUsableDevice> {
if skipped.is_empty() {
return Ok(None);
}
Err(NoUsableDevice { skipped })
}
pub fn is_user_cancelled(err: &(dyn std::error::Error + 'static)) -> bool {
classify_error(err).kind() == ErrorKind::UserCancelled
}
pub fn classify_error(err: &(dyn std::error::Error + 'static)) -> common::Error {
classify(err, None)
}
fn classify(err: &(dyn std::error::Error + 'static), kind: Option<ErrorKind>) -> common::Error {
let message = err.to_string();
let mut kind = kind;
let mut source = Some(err);
while let Some(current) = source {
if let Some(error) = current.downcast_ref::<common::Error>() {
return reworded(error, message);
}
kind = kind.or_else(|| {
current
.downcast_ref::<HWIDeviceError>()
.and_then(HWIDeviceError::kind)
});
source = current.source();
}
common::Error::new(kind.unwrap_or(ErrorKind::Other), message)
}
fn reworded(error: &common::Error, message: String) -> common::Error {
if error.to_string() == message {
return error.clone();
}
let inner = error.to_string();
let message = if message.contains(&inner) {
message
.replace(&inner, error.message())
.trim_end_matches(": ")
.to_owned()
} else if message.contains(error.message()) {
message
} else {
format!("{message}: {}", error.message())
};
let mut outer = common::Error::new(error.kind(), message);
if let Some(code) = error.device_code() {
outer = outer.with_device_code(code);
}
if let Some(data) = error.data() {
outer = outer.with_data(data.to_vec());
}
outer
}
#[derive(Debug, Clone)]
pub struct SkippedDevice {
pub device_type: DeviceType,
pub model: String,
pub path: String,
pub error: common::Error,
}
pub enum ScanEntry {
Found(
Device,
),
Skipped(
SkippedDevice,
),
}
impl SkippedDevice {
pub fn new(
device_type: DeviceType,
model: impl Into<String>,
path: impl Into<String>,
error: &(dyn std::error::Error + 'static),
) -> Self {
Self {
device_type,
model: model.into(),
path: path.into(),
error: classify_error(error),
}
}
}
impl ScanEntry {
pub fn skipped(
device_type: DeviceType,
model: impl Into<String>,
path: impl Into<String>,
error: &(dyn std::error::Error + 'static),
) -> Self {
Self::Skipped(SkippedDevice::new(device_type, model, path, error))
}
}
#[derive(Default)]
pub struct DeviceScan {
pub devices: Vec<Device>,
pub skipped: Vec<SkippedDevice>,
}
impl Extend<ScanEntry> for DeviceScan {
fn extend<I: IntoIterator<Item = ScanEntry>>(&mut self, iter: I) {
for entry in iter {
match entry {
ScanEntry::Found(device) => self.devices.push(device),
ScanEntry::Skipped(skipped) => self.skipped.push(skipped),
}
}
}
}
#[derive(Debug, Clone)]
pub struct NetworkMismatch {
pub expected: Network,
pub reported: Vec<Network>,
}
pub fn networks_string(networks: &[Network]) -> String {
networks
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(", ")
}
pub struct Device {
name: String,
device_type: DeviceType,
path: String,
model: String,
device: Box<dyn HWIDevice>,
is_emulated: bool,
fingerprint: Option<Fingerprint>,
info: Option<Info>,
}
impl Device {
pub fn new(
name: &str,
device_type: DeviceType,
path: impl Into<String>,
model: impl Into<String>,
device: Box<dyn HWIDevice>,
is_emulated: bool,
) -> Self {
Self {
name: name.into(),
device_type,
path: path.into(),
model: model.into(),
device,
is_emulated,
fingerprint: None,
info: None,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn device_type(&self) -> DeviceType {
self.device_type
}
pub fn path(&self) -> &str {
&self.path
}
pub fn model(&self) -> &str {
&self.model
}
pub fn device(&mut self) -> &mut Box<dyn HWIDevice> {
&mut self.device
}
pub fn is_emulated(&self) -> bool {
self.is_emulated
}
pub fn cached_fingerprint(&self) -> Option<Fingerprint> {
self.fingerprint
}
pub fn cached_info(&self) -> Option<&Info> {
self.info.as_ref()
}
pub async fn fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
if let Some(fingerprint) = self.fingerprint {
Ok(fingerprint)
} else {
let fingerprint = self.device.get_master_fingerprint().await?;
self.fingerprint = Some(fingerprint);
Ok(fingerprint)
}
}
pub async fn network_mismatch(
&mut self,
expected: Network,
) -> Result<Option<NetworkMismatch>, HWIDeviceError> {
let info = self.info().await?;
if info.networks.is_empty() || info.networks.contains(&expected) {
return Ok(None);
}
Ok(Some(NetworkMismatch {
expected,
reported: info.networks,
}))
}
pub async fn info(&mut self) -> Result<Info, HWIDeviceError> {
if let Some(ref info) = self.info {
Ok(info.clone())
} else if self.device_type == DeviceType::Specter {
let info = Info {
version: "unavailable".into(),
..Info::default()
};
self.info = Some(info.clone());
Ok(info)
} else {
let info = self.device.get_info().await?;
self.info = Some(info.clone());
Ok(info)
}
}
}
#[cfg(test)]
mod tests {
use bhwi::bitcoin::{
bip32::{DerivationPath, Xpub},
psbt::Psbt,
secp256k1::ecdsa::Signature,
};
use futures::executor::block_on;
use super::*;
use crate::{
DeviceBackup, DeviceContext, DisplayAddress, RestoreOptions, SetupOptions,
WalletRegistration,
};
struct Wallet {
fingerprint: Fingerprint,
refuses_unlock: bool,
}
#[async_trait(?Send)]
impl HWIDevice for Wallet {
async fn backup_device(&mut self) -> Result<DeviceBackup, HWIDeviceError> {
unimplemented!()
}
async fn setup_device(
&mut self,
_: SetupOptions,
_: Option<DeviceContext>,
) -> Result<bool, HWIDeviceError> {
unimplemented!()
}
async fn wipe_device(&mut self) -> Result<bool, HWIDeviceError> {
unimplemented!()
}
async fn restore_device(
&mut self,
_: RestoreOptions,
_: Option<DeviceContext>,
) -> Result<bool, HWIDeviceError> {
unimplemented!()
}
async fn toggle_passphrase(&mut self) -> Result<bool, HWIDeviceError> {
unimplemented!()
}
async fn prompt_pin(&mut self) -> Result<bool, HWIDeviceError> {
unimplemented!()
}
async fn send_pin(&mut self, _: Option<DeviceContext>) -> Result<bool, HWIDeviceError> {
unimplemented!()
}
async fn unlock(&mut self, _: Network) -> Result<(), HWIDeviceError> {
if self.refuses_unlock {
let refusal: Box<dyn std::error::Error + Send + Sync> =
"authentication refused".into();
return Err(refusal.into());
}
Ok(())
}
async fn get_info(&mut self) -> Result<Info, HWIDeviceError> {
unimplemented!()
}
async fn get_master_fingerprint(&mut self) -> Result<Fingerprint, HWIDeviceError> {
Ok(self.fingerprint)
}
async fn get_extended_pubkey(
&mut self,
_: DerivationPath,
_: bool,
) -> Result<Xpub, HWIDeviceError> {
unimplemented!()
}
async fn sign_message(
&mut self,
_: &[u8],
_: DerivationPath,
) -> Result<(u8, Signature), HWIDeviceError> {
unimplemented!()
}
async fn display_address(
&mut self,
_: DisplayAddress,
_: Option<DeviceContext>,
) -> Result<String, HWIDeviceError> {
unimplemented!()
}
async fn register_wallet(
&mut self,
_: &str,
_: &str,
) -> Result<WalletRegistration, HWIDeviceError> {
unimplemented!()
}
async fn sign_tx(
&mut self,
_: Psbt,
_: Option<DeviceContext>,
) -> Result<Psbt, HWIDeviceError> {
unimplemented!()
}
}
const REFUSING: [u8; 4] = [0xaa; 4];
const UNLOCKED: [u8; 4] = [0xbb; 4];
#[derive(Debug, thiserror::Error)]
#[error("device went away")]
struct Unplugged;
impl ErrorKindOf for Unplugged {
fn error_kind(&self) -> ErrorKind {
ErrorKind::Disconnected
}
}
struct Wallets(DeviceType);
#[async_trait(?Send)]
impl DeviceSource for Wallets {
type Error = Unplugged;
async fn list(&self, _: &DeviceSelector) -> Result<Vec<DeviceCandidate>, Self::Error> {
Ok(["refusing", "unlocked"]
.into_iter()
.map(|path| DeviceCandidate {
device_type: self.0,
name: "wallet".to_owned(),
model: "wallet".to_owned(),
path: path.to_owned(),
is_emulated: true,
})
.collect())
}
async fn open(
&self,
candidate: &DeviceCandidate,
_: &DeviceSelector,
_: Option<&PairingCodePrompt>,
_: Option<&HostInteractionFactory>,
) -> Result<Device, Self::Error> {
let refuses_unlock = candidate.path == "refusing";
let wallet = Wallet {
fingerprint: Fingerprint::from(if refuses_unlock { REFUSING } else { UNLOCKED }),
refuses_unlock,
};
Ok(Device::new(
&candidate.name,
candidate.device_type,
&candidate.path,
&candidate.model,
Box::new(wallet),
candidate.is_emulated,
))
}
}
fn manager(fingerprint: Option<Fingerprint>) -> DeviceManager<Wallets> {
let selector = DeviceSelector {
fingerprint,
..DeviceSelector::default()
};
DeviceManager::new(Wallets(DeviceType::Trezor), selector)
}
#[cfg(all(feature = "bitbox", any(feature = "trezor", feature = "keepkey")))]
#[test]
fn a_bitbox02_refuses_any_supplied_passphrase() {
for passphrase in [Some("secret"), Some(""), None] {
let selector = DeviceSelector {
fingerprint: Some(Fingerprint::from(UNLOCKED)),
passphrase: passphrase.map(|p| HostPassphrase::new(p.to_owned())),
..DeviceSelector::default()
};
let result =
block_on(DeviceManager::new(Wallets(DeviceType::BitBox02), selector).select());
assert_eq!(
matches!(result, Err(SelectError::HostPassphraseRejected)),
passphrase.is_some()
);
}
}
#[test]
fn a_refused_unlock_ends_selection_when_no_fingerprint_was_named() {
let err = block_on(manager(None).select())
.err()
.expect("the refusal is reported");
assert!(matches!(err, SelectError::Device(_)));
}
#[test]
fn a_refused_unlock_is_skipped_while_looking_for_a_named_fingerprint() {
let unlocked = Fingerprint::from(UNLOCKED);
let (device, skipped) =
block_on(manager(Some(unlocked)).select()).expect("the named wallet is found");
assert_eq!(
device.expect("a wallet").cached_fingerprint(),
Some(unlocked)
);
assert_eq!(skipped.len(), 1);
}
#[test]
fn taproot_support_matches_python_hwi() {
assert!(can_sign_taproot(
DeviceType::Ledger,
"ledger_nano_s_simulator"
));
assert!(!can_sign_taproot(
DeviceType::BitBox02,
"bitbox02_simulator"
));
assert!(!can_sign_taproot(DeviceType::Jade, "jade_simulator"));
assert!(can_sign_taproot(
DeviceType::Coldcard,
"coldcard_simulator_edge"
));
assert!(!can_sign_taproot(
DeviceType::Coldcard,
"coldcard_simulator"
));
assert!(can_sign_taproot(DeviceType::Trezor, "trezor_t"));
assert!(!can_sign_taproot(DeviceType::Trezor, "trezor_one"));
}
#[test]
fn an_interpreter_error_reads_as_its_cause() {
let wrapped: crate::Error<std::io::Error, std::io::Error> = crate::Error::Interpreter(
bhwi::common::Error::new(ErrorKind::InvalidInput, "Passphrase too long"),
);
assert_eq!(wrapped.to_string(), "[InvalidInput] Passphrase too long");
assert_eq!(
std::error::Error::source(&wrapped)
.expect("the cause is kept")
.to_string(),
"[InvalidInput] Passphrase too long"
);
let skipped = SkippedDevice::new(DeviceType::KeepKey, "keepkey", "udp:11044", &wrapped);
assert_eq!(
skipped.error,
bhwi::common::Error::new(ErrorKind::InvalidInput, "Passphrase too long")
);
assert_eq!(
skipped.error.to_string(),
"[InvalidInput] Passphrase too long"
);
}
#[test]
fn a_transport_error_keeps_the_layer_that_carries_its_meaning() {
let wrapped = HWIDeviceError::with_kind(
crate::Error::<std::io::Error, std::io::Error>::Transport {
kind: ErrorKind::Transport,
error: std::io::Error::other("Broken pipe"),
},
ErrorKind::Transport,
);
let skipped = SkippedDevice::new(DeviceType::KeepKey, "keepkey", "udp:11044", &wrapped);
assert_eq!(skipped.error.kind(), ErrorKind::Transport);
assert_eq!(
skipped.error.to_string(),
"[Transport] transport error: Broken pipe"
);
}
#[cfg(feature = "trezor")]
struct Replies(std::collections::VecDeque<Result<Vec<u8>, std::io::Error>>);
#[cfg(feature = "trezor")]
impl Replies {
fn new(replies: impl IntoIterator<Item = Result<Vec<u8>, std::io::Error>>) -> Self {
Self(replies.into_iter().collect())
}
}
#[cfg(feature = "trezor")]
#[async_trait(?Send)]
impl crate::Transport for Replies {
type Error = std::io::Error;
async fn exchange(&mut self, _: &[u8], _: bool) -> Result<Vec<u8>, Self::Error> {
self.0.pop_front().expect("a scripted reply")
}
fn error_kind(&self, error: &Self::Error) -> ErrorKind {
crate::transport::io_error_kind(error)
}
}
#[cfg(feature = "trezor")]
fn raised<F>(error: &HWIDeviceError) -> common::Error
where
F: std::error::Error + 'static,
{
match std::error::Error::source(error)
.and_then(|source| source.downcast_ref::<crate::Error<std::io::Error, F>>())
{
Some(crate::Error::Interpreter(error)) => error.clone(),
other => panic!("expected an interpreter error, got {other:?}"),
}
}
#[cfg(feature = "trezor")]
fn assert_kept_whole(error: &HWIDeviceError, raised: &common::Error) {
assert_eq!(classify_error(error), *raised);
let skipped = SkippedDevice::new(DeviceType::Trezor, "trezor_one", "udp:21324", error);
assert_eq!(skipped.error, *raised);
assert_eq!(skipped.error.to_string(), error.to_string());
}
#[cfg(feature = "trezor")]
fn trezor_reply(
message_type: bhwi::trezor::api::MessageType,
payload: &[u8],
) -> Result<Vec<u8>, std::io::Error> {
Ok(bhwi::trezor::api::frame(message_type as u16, payload))
}
#[cfg(feature = "trezor")]
#[test]
fn a_trezor_failure_keeps_its_kind_code_and_message() {
use bhwi::trezor::{TrezorError, api::MessageType};
let mut failure = vec![0x08, 0x07, 0x12, 0x0b];
failure.extend_from_slice(b"PIN invalid");
let mut trezor =
crate::Trezor::new(Replies::new([trezor_reply(MessageType::Failure, &failure)]));
let error = block_on(HWIDevice::get_master_fingerprint(&mut trezor))
.expect_err("the device reports a failure");
let raised = raised::<TrezorError>(&error);
assert_eq!(raised.kind(), ErrorKind::WrongPin);
assert_eq!(raised.device_code(), Some(common::DeviceCode::Trezor(7)));
assert_eq!(raised.message(), "PIN invalid");
assert_kept_whole(&error, &raised);
#[derive(Debug, thiserror::Error)]
#[error("getting fingerprint failed")]
struct Context(#[source] crate::HWIDeviceError);
let classified = classify_error(&Context(error));
assert_eq!(classified.kind(), ErrorKind::WrongPin);
assert_eq!(
classified.message(),
"getting fingerprint failed: PIN invalid"
);
#[derive(Debug, thiserror::Error)]
#[error("getting fingerprint failed: {0}")]
struct Quoting(#[source] common::Error);
let quoted = classify_error(&Quoting(common::Error::new(
ErrorKind::WrongPin,
"PIN invalid",
)));
assert_eq!(
quoted.to_string(),
"[WrongPin] getting fingerprint failed: PIN invalid"
);
}
#[cfg(feature = "trezor")]
#[test]
fn a_disconnect_keeps_the_kind_recorded_by_the_device_error() {
let mut trezor = crate::Trezor::new(Replies::new([Err(std::io::Error::new(
std::io::ErrorKind::BrokenPipe,
"device went away",
))]));
let error = block_on(HWIDevice::get_master_fingerprint(&mut trezor))
.expect_err("the transport fails");
let classified = classify_error(&error);
assert_eq!(classified.kind(), ErrorKind::Disconnected);
assert_eq!(classified.device_code(), None);
assert_eq!(classified.data(), None);
assert_eq!(
classified.to_string(),
"[Disconnected] transport error: device went away"
);
let skipped = SkippedDevice::new(DeviceType::Trezor, "trezor_one", "udp:21324", &error);
assert_eq!(skipped.error, classified);
assert!(!is_user_cancelled(&error));
}
#[cfg(feature = "trezor")]
struct CancellingTrezor;
#[cfg(feature = "trezor")]
#[async_trait(?Send)]
impl DeviceSource for CancellingTrezor {
type Error = Unplugged;
async fn list(&self, _: &DeviceSelector) -> Result<Vec<DeviceCandidate>, Self::Error> {
Ok(vec![DeviceCandidate {
device_type: DeviceType::Trezor,
name: "trezor".to_owned(),
model: "trezor_one".to_owned(),
path: "udp:21324".to_owned(),
is_emulated: true,
}])
}
async fn open(
&self,
candidate: &DeviceCandidate,
_: &DeviceSelector,
_: Option<&PairingCodePrompt>,
_: Option<&HostInteractionFactory>,
) -> Result<Device, Self::Error> {
use bhwi::trezor::api::MessageType;
let mut failure = vec![0x08, 0x04, 0x12, 0x18];
failure.extend_from_slice(b"Action cancelled by user");
let replies = Replies::new([
trezor_reply(MessageType::Features, &[]),
trezor_reply(MessageType::Failure, &failure),
]);
Ok(Device::new(
&candidate.name,
candidate.device_type,
&candidate.path,
&candidate.model,
Box::new(crate::Trezor::new(replies)),
candidate.is_emulated,
))
}
}
#[cfg(feature = "trezor")]
#[test]
fn a_cancel_on_the_device_ends_selection_instead_of_skipping_it() {
let selector = DeviceSelector {
fingerprint: Some(Fingerprint::from(UNLOCKED)),
..DeviceSelector::default()
};
let err = block_on(DeviceManager::new(CancellingTrezor, selector).select())
.err()
.expect("the cancel is reported");
let SelectError::Device(err) = err else {
panic!("expected the device error, got {err:?}");
};
assert_eq!(err.kind(), Some(ErrorKind::UserCancelled));
let raised = raised::<bhwi::trezor::TrezorError>(&err);
assert_eq!(raised.device_code(), Some(common::DeviceCode::Trezor(4)));
let built = crate::HWIDeviceError::new(common::Error::new(
ErrorKind::UserCancelled,
"action canceled by the user",
));
assert_eq!(built.kind(), Some(ErrorKind::UserCancelled));
let boxed: Box<dyn std::error::Error + Send + Sync> = Box::new(common::Error::new(
ErrorKind::UserCancelled,
"action canceled by the user",
));
let converted = crate::HWIDeviceError::from(boxed);
assert_eq!(converted.kind(), Some(ErrorKind::UserCancelled));
}
#[test]
fn multisig_display_address_needs_a_descriptor_capable_device() {
for device_type in DeviceType::ALL {
assert_eq!(
supports_multisig_display_address(device_type),
matches!(
device_type,
DeviceType::Coldcard
| DeviceType::Jade
| DeviceType::KeepKey
| DeviceType::Trezor
),
"{device_type}"
);
}
}
}