From 00be1968c422bad91fe6f0ed414c7748110ddfb0 Mon Sep 17 00:00:00 2001 From: Trevor Arjeski Date: Tue, 14 Apr 2026 13:20:08 +0000 Subject: [PATCH 1/2] refactor: rename get_version command to get_info This makes more sense since the struct actually contains more than just the device firmware version. Also change network field to networks for devices that may support multiple networks at a time (currently just jade). --- bhwi-async/src/lib.rs | 14 ++++++------ bhwi-cli/Cargo.toml | 2 +- bhwi-cli/src/bin/bhwi.rs | 6 ++--- bhwi-cli/src/lib.rs | 47 +++++++++++++++++++++++++++++++--------- bhwi/src/coldcard/mod.rs | 6 ++--- bhwi/src/common.rs | 12 +++++----- bhwi/src/jade/api.rs | 18 +++++++++++++++ bhwi/src/jade/mod.rs | 6 ++--- bhwi/src/ledger/mod.rs | 6 ++--- e2e/coldcard/src/lib.rs | 8 +++---- e2e/jade/src/lib.rs | 18 +++++++++++---- e2e/ledger/src/lib.rs | 8 +++---- 12 files changed, 102 insertions(+), 49 deletions(-) diff --git a/bhwi-async/src/lib.rs b/bhwi-async/src/lib.rs index 646983e..f7224e1 100644 --- a/bhwi-async/src/lib.rs +++ b/bhwi-async/src/lib.rs @@ -6,7 +6,7 @@ pub mod transport; use std::{error::Error as StdError, fmt::Debug}; use async_trait::async_trait; -pub use bhwi::common::Version; +pub use bhwi::common::Info; use bhwi::{ Interpreter, bitcoin::{ @@ -35,7 +35,7 @@ pub trait HttpClient { pub trait HWI { type Error: Debug; async fn unlock(&mut self, network: Network) -> Result<(), Self::Error>; - async fn get_version(&mut self) -> Result; + async fn get_info(&mut self) -> Result; async fn get_master_fingerprint(&mut self) -> Result; async fn get_extended_pubkey( &mut self, @@ -55,7 +55,7 @@ pub trait HWI { #[async_trait(?Send)] pub trait HWIDevice { async fn unlock(&mut self, network: Network) -> Result<(), HWIDeviceError>; - async fn get_version(&mut self) -> Result; + async fn get_info(&mut self) -> Result; async fn get_master_fingerprint(&mut self) -> Result; async fn get_extended_pubkey( &mut self, @@ -112,8 +112,8 @@ where Ok(()) } - async fn get_version(&mut self) -> Result { - if let common::Response::Version(version) = + async fn get_info(&mut self) -> Result { + if let common::Response::Info(version) = run_command(self, common::Command::GetVersion).await? { Ok(version) @@ -179,8 +179,8 @@ where .map_err(HWIDeviceError::new) } - async fn get_version(&mut self) -> Result { - HWI::get_version(self).await.map_err(HWIDeviceError::new) + async fn get_info(&mut self) -> Result { + HWI::get_info(self).await.map_err(HWIDeviceError::new) } async fn get_master_fingerprint(&mut self) -> Result { diff --git a/bhwi-cli/Cargo.toml b/bhwi-cli/Cargo.toml index 7f61542..6d20fc9 100644 --- a/bhwi-cli/Cargo.toml +++ b/bhwi-cli/Cargo.toml @@ -15,7 +15,7 @@ path = "src/bin/bhwi.rs" [dependencies] anyhow.workspace = true async-trait.workspace = true -bitcoin.workspace = true +bitcoin = { workspace = true, features = ["serde"] } bhwi-async = { workspace = true, features = ["emulators"] } futures.workspace = true hex = { workspace = true, features = ["serde"] } diff --git a/bhwi-cli/src/bin/bhwi.rs b/bhwi-cli/src/bin/bhwi.rs index 71fba84..f0fbb09 100644 --- a/bhwi-cli/src/bin/bhwi.rs +++ b/bhwi-cli/src/bin/bhwi.rs @@ -94,7 +94,7 @@ async fn main() -> Result<()> { let fingerprint = device.fingerprint().await?; let name = device.name().to_string(); let is_emulated = device.is_emulated(); - let version = device.version().await?; + let info = device.info().await?; match format { Some(OutputFormat::Pretty) => { if i == 0 { @@ -104,10 +104,10 @@ async fn main() -> Result<()> { ); } println!("{}", "-".repeat(80)); - let network = version.network.unwrap_or_default(); + let network = info.networks_string(); println!( "{name:<18} | {is_emulated:<8} | {fingerprint:<15} | {network:<12} | {:<8}", - version.version + info.version ); println!("{}", "-".repeat(80)); } diff --git a/bhwi-cli/src/lib.rs b/bhwi-cli/src/lib.rs index c2d62a3..814163a 100644 --- a/bhwi-cli/src/lib.rs +++ b/bhwi-cli/src/lib.rs @@ -1,8 +1,7 @@ use anyhow::Result; use async_trait::async_trait; use bhwi_async::HWIDevice; -use bhwi_async::Version; -use bitcoin::bip32::Fingerprint; +use bitcoin::{Network, bip32::Fingerprint}; use clap::ValueEnum; use futures::future::join_all; use serde::{Serialize, Serializer}; @@ -26,7 +25,35 @@ pub struct Device { #[serde(default, serialize_with = "option_fingerprint")] fingerprint: Option, #[serde(flatten, skip_serializing_if = "Option::is_none")] - version: Option, + info: Option, +} + +/// Serializable Device Information +#[derive(Debug, Clone, Default, Serialize)] +pub struct Info { + pub version: String, + pub networks: Vec, + pub firmware: Option, +} + +impl Info { + pub fn networks_string(&self) -> String { + self.networks + .iter() + .map(|n| n.to_string()) + .collect::>() + .join(", ") + } +} + +impl From for Info { + fn from(info: bhwi_async::Info) -> Self { + Self { + version: info.version, + networks: info.networks, + firmware: info.firmware, + } + } } impl Device { @@ -36,7 +63,7 @@ impl Device { device, is_emulated, fingerprint: None, - version: None, + info: None, }) } @@ -62,13 +89,13 @@ impl Device { } } - pub async fn version(&mut self) -> Result { - if let Some(ref version) = self.version { - Ok(version.clone()) + pub async fn info(&mut self) -> Result { + if let Some(ref info) = self.info { + Ok(info.clone()) } else { - let version = self.device.get_version().await?; - self.version = Some(version.clone()); - Ok(version) + let info: Info = self.device.get_info().await?.into(); + self.info = Some(info.clone()); + Ok(info) } } } diff --git a/bhwi/src/coldcard/mod.rs b/bhwi/src/coldcard/mod.rs index 5a24391..aef756b 100644 --- a/bhwi/src/coldcard/mod.rs +++ b/bhwi/src/coldcard/mod.rs @@ -8,7 +8,7 @@ use bitcoin::secp256k1::ecdsa::Signature; use crate::Interpreter; use crate::coldcard::api::response::ResponseMessage; -use crate::common::{Command, Error, Recipient, Response, Transmit, Version}; +use crate::common::{Command, Error, Info, Recipient, Response, Transmit}; use crate::device::DeviceId; pub const DEFAULT_CKCC_SOCKET: &str = "/tmp/ckcc-simulator.sock"; @@ -219,9 +219,9 @@ impl From for Response { ColdcardResponse::Version { version, device_model, - } => Response::Version(Version { + } => Response::Info(Info { version: version.as_str().into(), - network: None, + networks: vec![], firmware: Some(device_model), }), ColdcardResponse::MyPub { encryption_key, .. } => { diff --git a/bhwi/src/common.rs b/bhwi/src/common.rs index e3e0270..21ae46b 100644 --- a/bhwi/src/common.rs +++ b/bhwi/src/common.rs @@ -28,20 +28,18 @@ pub enum Command { pub enum Response { TaskDone, TaskBusy, - Version(Version), + Info(Info), MasterFingerprint(Fingerprint), Xpub(Xpub), EncryptionKey([u8; 64]), Signature(u8, Signature), } -/// Version information returned from a device. -#[derive(Debug, Clone, Default, serde::Serialize)] -pub struct Version { +/// Device Information +#[derive(Debug, Clone, Default)] +pub struct Info { pub version: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub network: Option, - #[serde(skip_serializing_if = "Option::is_none")] + pub networks: Vec, pub firmware: Option, } diff --git a/bhwi/src/jade/api.rs b/bhwi/src/jade/api.rs index 379a425..faf23a3 100644 --- a/bhwi/src/jade/api.rs +++ b/bhwi/src/jade/api.rs @@ -1,4 +1,5 @@ // See https://github.com/Blockstream/Jade/blob/master/docs/index.rst +use bitcoin::Network; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt::Display}; @@ -133,6 +134,23 @@ pub enum JadeNetworks { All, } +impl From for Vec { + fn from(networks: JadeNetworks) -> Self { + let main = [Network::Bitcoin]; + let testnets = [Network::Testnet, Network::Testnet4]; + match networks { + JadeNetworks::Main => main.to_vec(), + JadeNetworks::Test => testnets.to_vec(), + JadeNetworks::All => main + .iter() + .chain(testnets.iter()) + .chain([Network::Signet, Network::Regtest].iter()) + .copied() + .collect(), + } + } +} + impl Display for JadeNetworks { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( diff --git a/bhwi/src/jade/mod.rs b/bhwi/src/jade/mod.rs index 6a8c869..7c26cf9 100644 --- a/bhwi/src/jade/mod.rs +++ b/bhwi/src/jade/mod.rs @@ -11,7 +11,7 @@ use serde::Serialize; use serde::de::DeserializeOwned; use crate::Interpreter; -use crate::common::{Command, Error, Recipient, Response, Transmit, Version}; +use crate::common::{Command, Error, Info, Recipient, Response, Transmit}; use crate::device::DeviceId; use crate::jade::api::GetInfoResponse; @@ -287,9 +287,9 @@ impl From for Response { JadeResponse::MasterFingerprint(fg) => Response::MasterFingerprint(fg), JadeResponse::Xpub(xpub) => Response::Xpub(xpub), JadeResponse::Signature(header, signature) => Response::Signature(header, signature), - JadeResponse::GetInfo(info) => Response::Version(Version { + JadeResponse::GetInfo(info) => Response::Info(Info { version: info.jade_version.as_str().into(), - network: Some(info.jade_networks.to_string()), + networks: info.jade_networks.into(), firmware: None, }), } diff --git a/bhwi/src/ledger/mod.rs b/bhwi/src/ledger/mod.rs index e7548d7..4ce1714 100644 --- a/bhwi/src/ledger/mod.rs +++ b/bhwi/src/ledger/mod.rs @@ -18,7 +18,7 @@ use store::{DelegatedStore, StoreError}; pub use wallet::{WalletPolicy, WalletPubKey}; use crate::Interpreter; -use crate::common::{Command, Error, Response, Version}; +use crate::common::{Command, Error, Info, Response}; use crate::device::DeviceId; pub const LEDGER_DEVICE_ID: DeviceId = DeviceId::new(0x2c97) @@ -310,9 +310,9 @@ impl TryFrom for LedgerCommand { impl From for Response { fn from(res: LedgerResponse) -> Response { match res { - LedgerResponse::AppInfo(res) => Response::Version(Version { + LedgerResponse::AppInfo(res) => Response::Info(Info { version: res.version.to_string(), - network: Some(res.network().to_string()), + networks: vec![res.network()], firmware: None, }), LedgerResponse::Signature(header, signature) => Response::Signature(header, signature), diff --git a/e2e/coldcard/src/lib.rs b/e2e/coldcard/src/lib.rs index 2dd0a5a..f908508 100644 --- a/e2e/coldcard/src/lib.rs +++ b/e2e/coldcard/src/lib.rs @@ -87,10 +87,10 @@ mod tests { } #[tokio::test] - async fn can_get_version() { + async fn can_get_info() { let (mut dev, _) = device().await; - let version = dev.get_version().await.unwrap(); - assert_eq!(version.firmware, Some("mk4".to_string())); - assert_eq!(version.version.to_string(), "5.x.x"); + let info = dev.get_info().await.unwrap(); + assert_eq!(info.firmware, Some("mk4".to_string())); + assert_eq!(info.version.to_string(), "5.x.x"); } } diff --git a/e2e/jade/src/lib.rs b/e2e/jade/src/lib.rs index eccd6bf..e946e78 100644 --- a/e2e/jade/src/lib.rs +++ b/e2e/jade/src/lib.rs @@ -55,11 +55,21 @@ mod tests { } #[tokio::test] - async fn can_get_version() { + async fn can_get_info() { let mut dev = device().await; - let version = dev.get_version().await.unwrap(); + let info = dev.get_info().await.unwrap(); // 1.0.39-beta2-11-g1ca0a0a4-dirty - assert!(version.version.to_string().contains("1.0.39")); - assert_eq!(version.network.unwrap(), "all"); + assert!(info.version.to_string().contains("1.0.39")); + // jade has "all" networks + assert_eq!( + info.networks, + vec![ + Network::Bitcoin, + Network::Testnet, + Network::Testnet4, + Network::Regtest, + Network::Signet + ] + ); } } diff --git a/e2e/ledger/src/lib.rs b/e2e/ledger/src/lib.rs index 0505688..6c48721 100644 --- a/e2e/ledger/src/lib.rs +++ b/e2e/ledger/src/lib.rs @@ -160,9 +160,9 @@ mod tests { #[tokio::test] async fn can_get_version() { let (mut dev, _) = init().await; - let version = dev.get_version().await.unwrap(); - assert_eq!(version.version.to_string(), "2.4.5"); - assert_eq!(version.firmware, Some("Bitcoin Test".to_string())); - assert_eq!(version.network.unwrap().to_string(), "testnet"); + let info = dev.get_info().await.unwrap(); + assert_eq!(info.version.to_string(), "2.4.5"); + assert_eq!(info.firmware, Some("Bitcoin Test".to_string())); + assert_eq!(info.networks.first().unwrap().to_string(), "testnet"); } } From 4bafd7bc56cf6b4f80afac7ddefdfc5435e9272c Mon Sep 17 00:00:00 2001 From: Trevor Arjeski Date: Tue, 14 Apr 2026 13:21:27 +0000 Subject: [PATCH 2/2] fix(bhwi-cli): add warning when device network doesn't match cli Show a warning to the user when the device's configured network doesn't match what bhwi-cli expects. --- bhwi-cli/src/lib.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/bhwi-cli/src/lib.rs b/bhwi-cli/src/lib.rs index 814163a..761bee0 100644 --- a/bhwi-cli/src/lib.rs +++ b/bhwi-cli/src/lib.rs @@ -127,17 +127,31 @@ impl DeviceManager { } pub async fn get_device_with_fingerprint(&self) -> Result> { + let mut target_dev = None; for mut d in self.enumerate().await? { d.device.unlock(self.config.network).await?; if let Some(fingerprint) = self.config.fingerprint { if fingerprint == d.fingerprint().await? { - return Ok(Some(d)); + target_dev = Some(d); } } else { - return Ok(Some(d)); + target_dev = Some(d); } } - Ok(None) + let Some(mut dev) = target_dev else { + return Ok(None); + }; + let info = dev.info().await?; + let networks = &info.networks; + let net = self.config.network; + if !networks.is_empty() && !networks.contains(&net) { + eprintln!( + "Warning: device {} is on {}, expected {net}", + dev.name, + info.networks_string() + ); + } + Ok(Some(dev)) } pub async fn enumerate(&self) -> Result> {