1use async_trait::async_trait;
11use futures::future::join_all;
12
13#[cfg(feature = "bitbox")]
14use crate::bitbox::BitBoxDevice;
15#[cfg(feature = "coldcard")]
16use crate::coldcard::ColdcardDevice;
17#[cfg(feature = "jade")]
18use crate::jade::JadeDevice;
19#[cfg(feature = "keepkey")]
20use crate::keepkey::KeepKeyDevice;
21#[cfg(feature = "ledger")]
22use crate::ledger::LedgerDevice;
23#[cfg(feature = "specter")]
24use crate::specter::SpecterDevice;
25#[cfg(feature = "trezor")]
26use crate::trezor::TrezorDevice;
27
28#[cfg(feature = "bitbox")]
29pub mod bitbox;
30#[cfg(feature = "coldcard")]
31pub mod coldcard;
32#[cfg(any(feature = "keepkey", feature = "trezor"))]
33pub mod emulator;
34pub mod error;
35#[cfg(any(
36 feature = "bitbox",
37 feature = "coldcard",
38 feature = "keepkey",
39 feature = "ledger",
40 feature = "trezor"
41))]
42pub mod hid;
43#[cfg(feature = "jade")]
44pub mod jade;
45#[cfg(feature = "keepkey")]
46pub mod keepkey;
47#[cfg(feature = "ledger")]
48pub mod ledger;
49#[cfg(any(feature = "jade", feature = "specter"))]
50mod serial;
51#[cfg(feature = "specter")]
52pub mod specter;
53#[cfg(feature = "trezor")]
54pub mod trezor;
55#[cfg(target_os = "linux")]
56pub mod udev;
57#[cfg(any(feature = "keepkey", feature = "trezor"))]
58pub mod webusb;
59
60pub use bhwi_async::Info;
61pub use bhwi_async::device::{
62 Device, DeviceCandidate, DeviceManager, DeviceScan, DeviceSelector, DeviceSource, DeviceType,
63 HostInteractionFactory, NoUsableDevice, PairingCodePrompt, ScanEntry, SelectError,
64 SkippedDevice, can_sign_taproot, classify_error, is_user_cancelled, networks_string, no_device,
65 reports_device_info,
66};
67pub use error::{NativeError, NativeResult};
68
69#[derive(Default)]
77pub struct NativeSource {
78 excluded: Vec<DeviceType>,
79}
80
81impl NativeSource {
82 pub fn excluding(excluded: impl IntoIterator<Item = DeviceType>) -> Self {
84 Self {
85 excluded: excluded.into_iter().collect(),
86 }
87 }
88
89 pub fn is_supported(device_type: DeviceType) -> bool {
93 match device_type {
94 DeviceType::BitBox02 => cfg!(feature = "bitbox"),
95 DeviceType::Coldcard => cfg!(feature = "coldcard"),
96 DeviceType::Jade => cfg!(feature = "jade"),
97 DeviceType::KeepKey => cfg!(feature = "keepkey"),
98 DeviceType::Ledger => cfg!(feature = "ledger"),
99 DeviceType::Specter => cfg!(feature = "specter"),
100 DeviceType::Trezor => cfg!(feature = "trezor"),
101 }
102 }
103
104 fn walked(&self, selector: &DeviceSelector) -> Vec<DeviceType> {
105 match selector.device_type {
106 Some(device_type) => vec![device_type],
107 None => DeviceType::ALL
108 .into_iter()
109 .filter(|d| Self::is_supported(*d) && !self.excluded.contains(d))
110 .collect(),
111 }
112 }
113
114 async fn list_device_type(
115 device_type: DeviceType,
116 selector: &DeviceSelector,
117 ) -> NativeResult<Vec<DeviceCandidate>> {
118 Ok(match device_type {
119 #[cfg(feature = "bitbox")]
120 DeviceType::BitBox02 => BitBoxDevice::list(selector).await?,
121 #[cfg(feature = "ledger")]
122 DeviceType::Ledger => LedgerDevice::list(selector).await?,
123 #[cfg(feature = "coldcard")]
124 DeviceType::Coldcard => ColdcardDevice::list(selector).await?,
125 #[cfg(feature = "jade")]
126 DeviceType::Jade => JadeDevice::list(selector).await?,
127 #[cfg(feature = "keepkey")]
128 DeviceType::KeepKey => KeepKeyDevice::list(selector).await?,
129 #[cfg(feature = "specter")]
130 DeviceType::Specter => SpecterDevice::list(selector).await?,
131 #[cfg(feature = "trezor")]
132 DeviceType::Trezor => TrezorDevice::list(selector).await?,
133 #[allow(unreachable_patterns)]
134 _ => return Err(NativeError::NotCompiled(device_type)),
135 })
136 }
137}
138
139#[async_trait(?Send)]
140impl DeviceSource for NativeSource {
141 type Error = NativeError;
142
143 async fn list(&self, selector: &DeviceSelector) -> NativeResult<Vec<DeviceCandidate>> {
144 let results = join_all(
145 self.walked(selector)
146 .into_iter()
147 .map(|device_type| Self::list_device_type(device_type, selector)),
148 )
149 .await;
150 collect_candidates(results)
151 }
152
153 async fn open(
154 &self,
155 candidate: &DeviceCandidate,
156 selector: &DeviceSelector,
157 pairing_code: Option<&PairingCodePrompt>,
158 host_interaction: Option<&HostInteractionFactory>,
159 ) -> NativeResult<Device> {
160 match candidate.device_type {
161 #[cfg(feature = "bitbox")]
162 DeviceType::BitBox02 => {
163 BitBoxDevice::open(candidate, selector, pairing_code, host_interaction).await
164 }
165 #[cfg(feature = "ledger")]
166 DeviceType::Ledger => {
167 LedgerDevice::open(candidate, selector, pairing_code, host_interaction).await
168 }
169 #[cfg(feature = "coldcard")]
170 DeviceType::Coldcard => {
171 ColdcardDevice::open(candidate, selector, pairing_code, host_interaction).await
172 }
173 #[cfg(feature = "jade")]
174 DeviceType::Jade => {
175 JadeDevice::open(candidate, selector, pairing_code, host_interaction).await
176 }
177 #[cfg(feature = "keepkey")]
178 DeviceType::KeepKey => {
179 KeepKeyDevice::open(candidate, selector, pairing_code, host_interaction).await
180 }
181 #[cfg(feature = "specter")]
182 DeviceType::Specter => {
183 SpecterDevice::open(candidate, selector, pairing_code, host_interaction).await
184 }
185 #[cfg(feature = "trezor")]
186 DeviceType::Trezor => {
187 TrezorDevice::open(candidate, selector, pairing_code, host_interaction).await
188 }
189 #[allow(unreachable_patterns)]
190 device_type => Err(NativeError::NotCompiled(device_type)),
191 }
192 }
193}
194
195fn collect_candidates(
197 results: Vec<NativeResult<Vec<DeviceCandidate>>>,
198) -> NativeResult<Vec<DeviceCandidate>> {
199 let mut candidates = Vec::new();
200 let mut first_error = None;
201 for result in results {
202 match result {
203 Ok(found) => candidates.extend(found),
204 Err(err) if first_error.is_none() => first_error = Some(err),
205 Err(_) => {}
206 }
207 }
208 if candidates.is_empty()
209 && let Some(err) = first_error
210 {
211 return Err(err);
212 }
213 Ok(candidates)
214}
215
216#[cfg(any(feature = "keepkey", feature = "trezor"))]
218pub(crate) fn uses_backend(selected_path: Option<&str>, prefix: &str) -> bool {
219 selected_path.is_none_or(|path| path.starts_with(prefix))
220}
221
222#[async_trait(?Send)]
227pub trait DeviceEnumerator {
228 async fn list(selector: &DeviceSelector) -> NativeResult<Vec<DeviceCandidate>>;
233
234 async fn open(
241 candidate: &DeviceCandidate,
242 selector: &DeviceSelector,
243 pairing_code: Option<&PairingCodePrompt>,
244 host_interaction: Option<&HostInteractionFactory>,
245 ) -> NativeResult<Device>;
246}
247
248#[cfg(test)]
249mod tests {
250 use super::*;
251
252 fn candidate(path: &str) -> DeviceCandidate {
253 DeviceCandidate {
254 device_type: DeviceType::Coldcard,
255 name: "Coldcard".to_owned(),
256 model: "coldcard".to_owned(),
257 path: path.to_owned(),
258 is_emulated: false,
259 }
260 }
261
262 fn error(message: &'static str) -> NativeError {
263 NativeError::MissingDeviceId(message)
264 }
265
266 #[cfg(feature = "specter")]
267 #[test]
268 fn an_excluded_device_is_skipped_only_when_not_named() {
269 let source = NativeSource::excluding([DeviceType::Specter]);
270 assert!(
271 !source
272 .walked(&DeviceSelector::default())
273 .contains(&DeviceType::Specter)
274 );
275 assert!(
276 NativeSource::default()
277 .walked(&DeviceSelector::default())
278 .contains(&DeviceType::Specter)
279 );
280 let named = DeviceSelector {
281 device_type: Some(DeviceType::Specter),
282 ..DeviceSelector::default()
283 };
284 assert_eq!(source.walked(&named), vec![DeviceType::Specter]);
285 }
286
287 #[test]
288 fn a_list_keeps_what_the_other_buses_found() {
289 let results = vec![
290 Ok(vec![candidate("hid:1")]),
291 Err(error("unrelated")),
292 Ok(vec![candidate("hid:2")]),
293 ];
294
295 let found = collect_candidates(results).expect("one bus failing is tolerated");
296 assert_eq!(found.len(), 2);
297 }
298
299 #[test]
300 fn a_list_that_found_nothing_reports_the_first_failure() {
301 let results = vec![Err(error("first")), Err(error("second"))];
302
303 let err = collect_candidates(results).expect_err("a list that found nothing reports why");
304 assert_eq!(err.to_string(), "first");
305 }
306
307 #[test]
308 fn one_bus_failing_alone_reports_the_failure() {
309 let results = vec![Err(error("path failed"))];
310
311 let err = collect_candidates(results).expect_err("a list that found nothing reports why");
312 assert_eq!(err.to_string(), "path failed");
313 }
314
315 #[test]
316 fn a_list_that_found_something_ignores_the_failure() {
317 let results = vec![Err(error("unrelated")), Ok(vec![candidate("hid:1")])];
318
319 let found = collect_candidates(results).expect("a found device wins over an error");
320 assert_eq!(found.len(), 1);
321 }
322}