1use std::{ops::Deref, sync::Arc};
12
13#[cfg(target_os = "linux")]
14pub use bhwi_async_transport::udev;
15pub use bhwi_async_transport::{
16 Device, DeviceSelector, DeviceType, Info, NativeError, NativeSource, SelectError,
17 SkippedDevice, can_sign_taproot, is_user_cancelled, networks_string, no_device,
18 reports_device_info,
19};
20use bitcoin::{Network, bip32::Fingerprint};
21use clap::ValueEnum;
22use serde::{Serialize, Serializer};
23
24pub struct DeviceManager {
29 manager: bhwi_async_transport::DeviceManager<NativeSource>,
30 password_given: bool,
31}
32
33impl Deref for DeviceManager {
34 type Target = bhwi_async_transport::DeviceManager<NativeSource>;
35
36 fn deref(&self) -> &Self::Target {
37 &self.manager
38 }
39}
40
41impl DeviceManager {
42 pub fn password_given(&self) -> bool {
44 self.password_given
45 }
46
47 pub fn refuse_password(&self, device_type: DeviceType) -> Result<(), SelectError<NativeError>> {
49 #[cfg(feature = "bitbox")]
50 if self.password_given && device_type == DeviceType::BitBox02 {
51 return Err(SelectError::HostPassphraseRejected);
52 }
53 let _ = device_type;
54 Ok(())
55 }
56}
57
58#[derive(Serialize)]
66pub struct DeviceJson<'a> {
67 name: &'a str,
68 #[serde(serialize_with = "serialize_device_type")]
69 device_type: DeviceType,
70 path: &'a str,
71 model: &'a str,
72 is_emulated: bool,
73 fingerprint: Option<Fingerprint>,
74 #[serde(flatten)]
75 info: Option<InfoJson<'a>>,
76}
77
78#[derive(Serialize)]
80struct InfoJson<'a> {
81 #[serde(skip_serializing_if = "Option::is_none")]
82 version: Option<&'a str>,
83 networks: &'a [Network],
84 #[serde(skip_serializing_if = "Option::is_none")]
85 firmware: Option<Option<&'a str>>,
86 #[serde(skip_serializing_if = "Option::is_none")]
87 label: Option<&'a str>,
88}
89
90impl<'a> InfoJson<'a> {
91 fn new(device_type: DeviceType, info: &'a Info) -> Self {
92 let reports_firmware = device_type != DeviceType::Specter;
94 Self {
95 version: reports_firmware.then_some(info.version.as_str()),
96 networks: &info.networks,
97 firmware: reports_firmware.then_some(info.firmware.as_deref()),
98 label: info.label.as_deref(),
99 }
100 }
101}
102
103impl<'a> From<&'a Device> for DeviceJson<'a> {
104 fn from(device: &'a Device) -> Self {
105 Self {
106 name: device.name(),
107 device_type: device.device_type(),
108 path: device.path(),
109 model: device.model(),
110 is_emulated: device.is_emulated(),
111 fingerprint: device.cached_fingerprint(),
112 info: device
113 .cached_info()
114 .map(|info| InfoJson::new(device.device_type(), info)),
115 }
116 }
117}
118
119fn serialize_device_type<S>(device_type: &DeviceType, ser: S) -> Result<S::Ok, S::Error>
120where
121 S: Serializer,
122{
123 ser.serialize_str(device_type.as_str())
124}
125
126pub fn device_manager(selector: DeviceSelector, password_given: bool) -> DeviceManager {
130 manager_over(NativeSource::default(), selector, password_given)
131}
132
133pub fn python_hwi_device_manager(selector: DeviceSelector, password_given: bool) -> DeviceManager {
138 manager_over(
139 NativeSource::excluding([DeviceType::Specter]),
140 selector,
141 password_given,
142 )
143}
144
145fn manager_over(
146 source: NativeSource,
147 selector: DeviceSelector,
148 password_given: bool,
149) -> DeviceManager {
150 let manager = bhwi_async_transport::DeviceManager::new(source, selector)
151 .with_pairing_code_prompt(Arc::new(|code| {
152 eprintln!("\nBitBox02 pairing code — confirm on device:\n\n{code}\n");
153 }));
154 #[cfg(feature = "keepkey")]
155 let manager = manager.with_host_interaction(host::cli_host_interaction());
156 DeviceManager {
157 manager,
158 password_given,
159 }
160}
161
162pub fn warn_skipped(entry: &SkippedDevice) {
164 eprintln!(
165 "Warning: skipping {} at {}: {}",
166 entry.device_type, entry.path, entry.error
167 );
168}
169
170pub async fn select_device(manager: &DeviceManager) -> anyhow::Result<Option<Device>> {
177 let (device, skipped) = manager.select().await?;
178 let Some(mut device) = device else {
179 let Some(first) = skipped.first() else {
180 return Ok(None);
181 };
182 anyhow::bail!("{}", first.error);
183 };
184 manager.refuse_password(device.device_type())?;
185 for entry in &skipped {
186 warn_skipped(entry);
187 }
188 if let Some(mismatch) = device.network_mismatch(manager.selector.network).await? {
189 eprintln!(
190 "Warning: device {} is on {}, expected {}",
191 device.name(),
192 networks_string(&mismatch.reported),
193 mismatch.expected
194 );
195 }
196 Ok(Some(device))
197}
198
199pub mod address;
200pub mod get_descriptors;
201#[cfg(feature = "keepkey")]
202pub mod host;
203pub mod hwi;
204pub mod management;
205
206#[derive(Debug, Clone, Copy, ValueEnum)]
208pub enum OutputFormat {
209 Pretty,
211 Json,
213}
214
215#[derive(Debug, Clone, Copy, ValueEnum)]
217pub enum DeviceTypeArg {
218 #[value(name = "bitbox02", alias = "bit-box02")]
220 BitBox02,
221 Coldcard,
223 Jade,
225 #[value(name = "keepkey", alias = "keep-key")]
227 KeepKey,
228 Ledger,
230 Specter,
232 Trezor,
234}
235
236impl From<DeviceTypeArg> for DeviceType {
237 fn from(arg: DeviceTypeArg) -> Self {
238 match arg {
239 DeviceTypeArg::BitBox02 => DeviceType::BitBox02,
240 DeviceTypeArg::Coldcard => DeviceType::Coldcard,
241 DeviceTypeArg::Jade => DeviceType::Jade,
242 DeviceTypeArg::KeepKey => DeviceType::KeepKey,
243 DeviceTypeArg::Ledger => DeviceType::Ledger,
244 DeviceTypeArg::Specter => DeviceType::Specter,
245 DeviceTypeArg::Trezor => DeviceType::Trezor,
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn a_given_password_is_refused_only_for_bitbox02() {
256 let given = device_manager(DeviceSelector::default(), true);
257 let not_given = device_manager(DeviceSelector::default(), false);
258 for device_type in DeviceType::ALL {
259 assert_eq!(
260 given.refuse_password(device_type).is_err(),
261 cfg!(feature = "bitbox") && device_type == DeviceType::BitBox02,
262 "{device_type}"
263 );
264 assert!(not_given.refuse_password(device_type).is_ok());
265 }
266 }
267
268 #[test]
269 fn ordinary_info_keeps_the_firmware_null_json_contract() {
270 let info = Info::default();
271 let value = serde_json::to_value(InfoJson::new(DeviceType::Ledger, &info)).unwrap();
272 assert!(value.get("version").is_some());
273 assert!(value.get("firmware").is_some_and(|value| value.is_null()));
274 }
275
276 #[test]
277 fn specter_info_omits_unavailable_firmware_from_json() {
278 let info = Info {
279 version: "unavailable".into(),
280 ..Info::default()
281 };
282 let value = serde_json::to_value(InfoJson::new(DeviceType::Specter, &info)).unwrap();
283 assert!(value.get("version").is_none());
284 assert!(value.get("firmware").is_none());
285 assert!(value.get("networks").is_some());
286 }
287}