1use crate::{bip389, parse_version, AddressScript, DeviceKind, Error as HWIError, HWI};
2use api::btc::make_script_config_simple;
3use async_trait::async_trait;
4use bitbox_api::{
5 btc::KeyOriginInfo,
6 error::{BitBoxError, Error},
7 pb::{self, BtcScriptConfig},
8 usb::UsbError,
9 Keypath, PairedBitBox, PairingBitBox,
10};
11use bitcoin::{
12 bip32::{ChildNumber, DerivationPath, Fingerprint, Xpub},
13 psbt::Psbt,
14};
15use regex::Regex;
16use std::{
17 str::FromStr,
18 sync::{Arc, Mutex},
19};
20
21pub use bitbox_api::{
22 self as api,
23 runtime::Runtime,
24 usb::{get_any_bitbox02, is_bitbox02},
25 ConfigError, NoiseConfig, NoiseConfigData, NoiseConfigNoCache,
26};
27
28#[derive(Clone)]
29struct Cache(Arc<Mutex<Option<NoiseConfigData>>>);
30
31impl bitbox_api::Threading for Cache {}
32
33impl NoiseConfig for Cache {
34 fn read_config(&self) -> Result<NoiseConfigData, ConfigError> {
35 let noise_data = self.0.lock().map_err(|e| ConfigError(e.to_string()))?;
36 if let Some(data) = noise_data.as_ref() {
37 Ok(data.clone())
38 } else {
39 Ok(NoiseConfigData::default())
40 }
41 }
42 fn store_config(&self, data: &NoiseConfigData) -> Result<(), ConfigError> {
43 let mut noise_data = self.0.lock().map_err(|e| ConfigError(e.to_string()))?;
44 *noise_data = Some(data.clone());
45 Ok(())
46 }
47}
48
49pub struct PairingBitbox02WithLocalCache<T: Runtime> {
50 client: PairingBitBox<T>,
51 local_cache: Cache,
52}
53
54impl<T: Runtime> PairingBitbox02WithLocalCache<T> {
55 pub async fn connect(
56 device: hidapi::HidDevice,
57 pairing_data: Option<NoiseConfigData>,
58 ) -> Result<Self, HWIError> {
59 let local_cache = if let Some(data) = pairing_data {
60 Cache(Arc::new(Mutex::new(Some(data))))
61 } else {
62 Cache(Arc::new(Mutex::new(None)))
63 };
64 let bitbox =
65 bitbox_api::BitBox::<T>::from_hid_device(device, Box::new(local_cache.clone())).await?;
66 let pairing_bitbox = bitbox.unlock_and_pair().await?;
67 Ok(PairingBitbox02WithLocalCache {
68 client: pairing_bitbox,
69 local_cache,
70 })
71 }
72
73 pub fn pairing_code(&self) -> Option<String> {
74 self.client.get_pairing_code()
75 }
76
77 pub async fn wait_confirm(self) -> Result<(PairedBitBox<T>, NoiseConfigData), HWIError> {
78 let client = self.client.wait_confirm().await?;
79 let mut cache = self
80 .local_cache
81 .0
82 .lock()
83 .map_err(|e| HWIError::Device(e.to_string()))?;
84 Ok((
85 client,
86 cache
87 .take()
88 .expect("noise config data must be in local cache"),
89 ))
90 }
91}
92
93pub struct PairingBitbox02<T: Runtime> {
94 client: PairingBitBox<T>,
95}
96
97impl<T: Runtime> PairingBitbox02<T> {
98 pub async fn connect(
99 device: hidapi::HidDevice,
100 pairing: Option<Box<dyn NoiseConfig>>,
101 ) -> Result<Self, HWIError> {
102 let noise_config = pairing.unwrap_or_else(|| Box::new(NoiseConfigNoCache {}));
103 let bitbox = bitbox_api::BitBox::<T>::from_hid_device(device, noise_config).await?;
104 let pairing_bitbox = bitbox.unlock_and_pair().await?;
105 Ok(PairingBitbox02 {
106 client: pairing_bitbox,
107 })
108 }
109
110 pub fn pairing_code(&self) -> Option<String> {
111 self.client.get_pairing_code()
112 }
113
114 pub async fn wait_confirm(self) -> Result<PairedBitBox<T>, HWIError> {
115 self.client.wait_confirm().await.map_err(|e| e.into())
116 }
117}
118
119pub struct BitBox02<T: Runtime> {
120 pub network: bitcoin::Network,
121 pub display_xpub: bool,
122 pub client: PairedBitBox<T>,
123 pub policy: Option<Policy>,
124}
125
126impl<T: Runtime> std::fmt::Debug for BitBox02<T> {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("BitBox").finish()
129 }
130}
131
132impl<T: Runtime> BitBox02<T> {
133 pub fn from(paired_bitbox: PairedBitBox<T>) -> Self {
134 BitBox02 {
135 display_xpub: false,
136 network: bitcoin::Network::Bitcoin,
137 client: paired_bitbox,
138 policy: None,
139 }
140 }
141
142 pub fn with_network(mut self, network: bitcoin::Network) -> Self {
143 self.network = network;
144 self
145 }
146
147 pub fn display_xpub(mut self, value: bool) -> Self {
148 self.display_xpub = value;
149 self
150 }
151
152 pub fn with_policy(mut self, policy: &str) -> Result<Self, HWIError> {
153 self.policy = Some(extract_script_config_policy(policy)?);
154 Ok(self)
155 }
156
157 pub async fn is_policy_registered(&self, policy: &str) -> Result<bool, HWIError> {
158 let pb_network = coin_from_network(self.network);
159 let policy = extract_script_config_policy(policy)?;
160 self.client
161 .btc_is_script_config_registered(pb_network, &policy.into(), None)
162 .await
163 .map_err(|e| e.into())
164 }
165}
166
167#[async_trait]
168impl<T: Runtime + Sync + Send> HWI for BitBox02<T> {
169 fn device_kind(&self) -> DeviceKind {
170 DeviceKind::BitBox02
171 }
172
173 async fn get_version(&self) -> Result<super::Version, HWIError> {
174 let info = self
175 .client
176 .device_info()
177 .await
178 .map_err(|e| HWIError::Device(e.to_string()))?;
179 Ok(parse_version(&info.version)?)
180 }
181
182 async fn get_master_fingerprint(&self) -> Result<Fingerprint, HWIError> {
183 let fg = self
184 .client
185 .root_fingerprint()
186 .await
187 .map_err(|e| HWIError::Device(e.to_string()))?;
188 Ok(Fingerprint::from_str(&fg).map_err(|e| HWIError::Device(e.to_string()))?)
189 }
190
191 async fn get_extended_pubkey(&self, path: &DerivationPath) -> Result<Xpub, HWIError> {
192 let fg = self
193 .client
194 .btc_xpub(
195 if self.network == bitcoin::Network::Bitcoin {
196 pb::BtcCoin::Btc
197 } else {
198 pb::BtcCoin::Tbtc
199 },
200 &Keypath::from(path),
201 if self.network == bitcoin::Network::Bitcoin {
202 pb::btc_pub_request::XPubType::Xpub
203 } else {
204 pb::btc_pub_request::XPubType::Tpub
205 },
206 self.display_xpub,
207 )
208 .await
209 .map_err(|e| HWIError::Device(e.to_string()))?;
210 Ok(Xpub::from_str(&fg).map_err(|e| HWIError::Device(e.to_string()))?)
211 }
212
213 async fn display_address(&self, script: &AddressScript) -> Result<(), HWIError> {
214 match script {
215 AddressScript::P2TR(path) => {
216 self.client
217 .btc_address(
218 if self.network == bitcoin::Network::Bitcoin {
219 pb::BtcCoin::Btc
220 } else {
221 pb::BtcCoin::Tbtc
222 },
223 &Keypath::from(path),
224 &make_script_config_simple(pb::btc_script_config::SimpleType::P2tr),
225 true,
226 )
227 .await?;
228 }
229 AddressScript::Miniscript { index, change } => {
230 let policy = self.policy.clone().ok_or_else(|| HWIError::MissingPolicy)?;
231 let fg = self.get_master_fingerprint().await?;
232 let mut path = DerivationPath::master();
233 for (key_index, key) in policy.pubkeys.iter().enumerate() {
234 if Some(fg) == key.master_fingerprint {
235 if let Some(p) = &key.path {
236 path = p.clone();
237 }
238 let (appended_path, wildcard) =
239 extract_first_appended_derivation_with_some_wildcard(
240 key_index,
241 &policy.template,
242 )?;
243 if appended_path.len() >= 2 {
244 path = path.extend(if *change {
245 &appended_path[1]
246 } else {
247 &appended_path[0]
248 });
249 } else if !appended_path.is_empty() {
250 path = path.extend(&appended_path[0]);
251 }
252 if wildcard == bip389::Wildcard::Hardened {
253 let child = ChildNumber::from_hardened_idx(*index)
254 .map_err(|_| HWIError::UnsupportedInput)?;
255 path = path.extend([child]);
256 } else if wildcard == bip389::Wildcard::Unhardened {
257 let child = ChildNumber::from_normal_idx(*index)
258 .map_err(|_| HWIError::UnsupportedInput)?;
259 path = path.extend([child]);
260 }
261 break;
262 }
263 }
264 self.client
265 .btc_address(
266 if self.network == bitcoin::Network::Bitcoin {
267 pb::BtcCoin::Btc
268 } else {
269 pb::BtcCoin::Tbtc
270 },
271 &Keypath::from(&path),
272 &policy.into(),
273 true,
274 )
275 .await?;
276 }
277 }
278 Ok(())
279 }
280
281 async fn register_wallet(
282 &self,
283 name: &str,
284 policy: &str,
285 ) -> Result<Option<[u8; 32]>, HWIError> {
286 let pb_network = coin_from_network(self.network);
287 let policy = extract_script_config_policy(policy)?;
288 if self
289 .client
290 .btc_is_script_config_registered(pb_network, &policy.clone().into(), None)
291 .await?
292 {
293 return Ok(None);
294 }
295 self.client
296 .btc_register_script_config(
297 pb_network,
298 &policy.into(),
299 None,
300 pb::btc_register_script_config_request::XPubType::AutoXpubTpub,
301 Some(name),
302 )
303 .await
304 .map(|_| None)
305 .map_err(|e| e.into())
306 }
307
308 async fn is_wallet_registered(&self, _name: &str, policy: &str) -> Result<bool, HWIError> {
309 let pb_network = coin_from_network(self.network);
310 let policy = extract_script_config_policy(policy)?;
311 self.client
312 .btc_is_script_config_registered(pb_network, &policy.clone().into(), None)
313 .await
314 .map_err(|e| e.into())
315 }
316
317 async fn sign_tx(&self, psbt: &mut Psbt) -> Result<(), HWIError> {
321 let policy: Option<pb::BtcScriptConfigWithKeypath> =
322 if let Some(policy) = self.policy.clone() {
323 let mut path = DerivationPath::master();
324 let fg = self.get_master_fingerprint().await?;
325 for key in &policy.pubkeys {
326 if Some(fg) == key.master_fingerprint {
327 if let Some(p) = &key.path {
328 path = p.clone();
329 break;
330 }
331 }
332 }
333 Some(pb::BtcScriptConfigWithKeypath {
334 script_config: Some(policy.into()),
335 keypath: Keypath::from(&path).to_vec(),
336 })
337 } else {
338 None
339 };
340
341 self.client
342 .btc_sign_psbt(
343 coin_from_network(self.network),
344 psbt,
345 policy,
346 pb::btc_sign_init_request::FormatUnit::Default,
347 )
348 .await?;
349
350 Ok(())
351 }
352}
353
354fn coin_from_network(network: bitcoin::Network) -> pb::BtcCoin {
355 if network == bitcoin::Network::Bitcoin {
356 pb::BtcCoin::Btc
357 } else {
358 pb::BtcCoin::Tbtc
359 }
360}
361
362impl From<UsbError> for HWIError {
363 fn from(value: UsbError) -> Self {
364 HWIError::Device(value.to_string())
365 }
366}
367
368impl From<Error> for HWIError {
369 fn from(e: Error) -> Self {
370 if let Error::BitBox(BitBoxError::UserAbort) = e {
371 HWIError::UserRefused
372 } else {
373 HWIError::Device(e.to_string())
374 }
375 }
376}
377
378impl<T: Runtime + Sync + Send + 'static> From<BitBox02<T>> for Box<dyn HWI + Sync + Send> {
379 fn from(s: BitBox02<T>) -> Box<dyn HWI + Sync + Send> {
380 Box::new(s)
381 }
382}
383
384impl<T: Runtime + Sync + Send + 'static> From<BitBox02<T>> for Box<dyn HWI + Send> {
385 fn from(s: BitBox02<T>) -> Box<dyn HWI + Send> {
386 Box::new(s)
387 }
388}
389
390impl<T: Runtime + Sync + Send + 'static> From<BitBox02<T>>
391 for std::sync::Arc<dyn HWI + Sync + Send>
392{
393 fn from(s: BitBox02<T>) -> std::sync::Arc<dyn HWI + Sync + Send> {
394 std::sync::Arc::new(s)
395 }
396}
397
398pub fn extract_script_config_policy(policy: &str) -> Result<Policy, HWIError> {
399 let re = Regex::new(r"((\[.+?\])?[xyYzZtuUvV]pub[1-9A-HJ-NP-Za-km-z]{79,108})").unwrap();
400 let mut descriptor_template = policy.to_string();
401 let mut pubkeys_str: Vec<&str> = Vec::new();
402 for capture in re.find_iter(policy) {
403 if !pubkeys_str.contains(&capture.as_str()) {
404 pubkeys_str.push(capture.as_str());
405 }
406 }
407
408 let mut pubkeys: Vec<KeyInfo> = Vec::new();
409 for (i, key_str) in pubkeys_str.iter().enumerate() {
410 descriptor_template = descriptor_template.replace(key_str, &format!("@{}", i));
411 let pubkey = if let Ok(key) = Xpub::from_str(key_str) {
412 KeyInfo {
413 path: None,
414 master_fingerprint: None,
415 xpub: key,
416 }
417 } else {
418 let (keysource_str, xpub_str) = key_str
419 .strip_prefix('[')
420 .and_then(|s| s.rsplit_once(']'))
421 .ok_or(HWIError::InvalidParameter(
422 "policy",
423 "Invalid key source".to_string(),
424 ))?;
425 let (f_str, path_str) = keysource_str.split_once('/').unwrap_or((keysource_str, ""));
426 let fingerprint = Fingerprint::from_str(f_str)
427 .map_err(|e| HWIError::InvalidParameter("policy", e.to_string()))?;
428 let derivation_path = if path_str.is_empty() {
429 DerivationPath::master()
430 } else {
431 DerivationPath::from_str(&format!("m/{}", path_str))
432 .map_err(|e| HWIError::InvalidParameter("policy", e.to_string()))?
433 };
434
435 KeyInfo {
436 xpub: Xpub::from_str(xpub_str)
437 .map_err(|e| HWIError::InvalidParameter("policy", e.to_string()))?,
438 path: Some(derivation_path),
439 master_fingerprint: Some(fingerprint),
440 }
441 };
442 pubkeys.push(pubkey);
443 }
444 let descriptor_template =
446 if let Some((descriptor_template, _hash)) = descriptor_template.rsplit_once('#') {
447 descriptor_template
448 } else {
449 &descriptor_template
450 };
451
452 Ok(Policy {
454 template: descriptor_template.to_string(),
455 pubkeys,
456 })
457}
458
459pub fn extract_first_appended_derivation_with_some_wildcard(
460 key_index: usize,
461 template: &str,
462) -> Result<(Vec<DerivationPath>, bip389::Wildcard), HWIError> {
463 let re = Regex::new(r"@\d+/[^,)]+").unwrap();
464 for capture in re.find_iter(template) {
465 if capture.as_str().contains(&format!("@{}", key_index)) {
466 if let Some((_, appended)) = capture.as_str().split_once('/') {
467 let (derivations, wildcard) = bip389::parse_xkey_deriv(appended)?;
468 if wildcard != bip389::Wildcard::None {
469 return Ok((derivations, wildcard));
470 }
471 }
472 }
473 }
474 Ok((Vec::new(), bip389::Wildcard::None))
475}
476
477#[derive(Clone)]
478pub struct Policy {
479 template: String,
480 pubkeys: Vec<KeyInfo>,
481}
482
483impl From<Policy> for BtcScriptConfig {
484 fn from(p: Policy) -> BtcScriptConfig {
485 let keys: Vec<KeyOriginInfo> = p.pubkeys.into_iter().map(|k| k.into()).collect();
486 bitbox_api::btc::make_script_config_policy(&p.template, &keys)
487 }
488}
489
490#[derive(Clone)]
491pub struct KeyInfo {
492 xpub: Xpub,
493 path: Option<DerivationPath>,
494 master_fingerprint: Option<Fingerprint>,
495}
496
497impl From<KeyInfo> for KeyOriginInfo {
498 fn from(info: KeyInfo) -> KeyOriginInfo {
499 KeyOriginInfo {
500 root_fingerprint: info.master_fingerprint,
501 keypath: info.path.as_ref().map(Keypath::from),
502 xpub: info.xpub,
503 }
504 }
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510
511 #[test]
512 fn test_extract_script_config_policy() {
513 let policy = extract_script_config_policy("wsh(or_d(pk([f5acc2fd/49'/1'/0']tpubDCbK3Ysvk8HjcF6mPyrgMu3KgLiaaP19RjKpNezd8GrbAbNg6v5BtWLaCt8FNm6QkLseopKLf5MNYQFtochDTKHdfgG6iqJ8cqnLNAwtXuP/**),and_v(v:pkh(tpubDDtb2WPYwEWw2WWDV7reLV348iJHw2HmhzvPysKKrJw3hYmvrd4jasyoioVPdKGQqjyaBMEvTn1HvHWDSVqQ6amyyxRZ5YjpPBBGjJ8yu8S/**),older(100))))").unwrap();
514 assert_eq!(2, policy.pubkeys.len());
515 assert_eq!(
516 "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/**),older(100))))",
517 policy.template
518 );
519
520 let policy = extract_script_config_policy("wsh(or_d(multi(2,[b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<0;1>/*,[7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<0;1>/*),and_v(v:thresh(2,pkh([b0822927/48'/1'/0'/2']tpubDEvZxV86Br8Knbm9tWcr5Hvmg5cYTYsg92vinqH6Bie6U8ix8CsoN9W11NQygdqVwmHUJpsHXxNsi5gXn36g4xNfLWkMqPuFhRZAmMQ7jjQ/<2;3>/*),a:pkh([7fc39c07/48'/1'/0'/2']tpubDEvjgXtrUuH3Qtkapny9aE8gN847xiXsf9MDM5XueGf9nrvStqAuBSva3ajGyTvtp8Ti55FvVXsgYSXuS1tQkBeopFuodx2hRUDmQbvKxbZ/<2;3>/*),a:pkh([1a1ffd98/48'/1'/0'/2']tpubDFZqzTvGijYb13BC73CkS1er8DrP5YdzMhziN3kWCKUFaW51Yj6ggvf99YpdrkTJy4RT85mxQMHXDiFAKRxzf6BykQgT4pRRBNPshSJJcKo/<0;1>/*)),older(300))))#wp0w3hlw").unwrap();
521 assert_eq!(3, policy.pubkeys.len());
522 assert_eq!(
523 "wsh(or_d(multi(2,@0/<0;1>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/<0;1>/*)),older(300))))",
524 policy.template
525 );
526 }
527
528 #[test]
529 fn test_extract_first_appended_derivation_with_some_wildcard() {
530 let (paths, wildcard) = extract_first_appended_derivation_with_some_wildcard(
531 1,
532 "wsh(or_d(pk(@0/**),and_v(v:pkh(@1/1/**),older(100))))",
533 )
534 .unwrap();
535 assert_eq!(wildcard, bip389::Wildcard::Unhardened);
536 assert_eq!(
537 paths,
538 vec![
539 DerivationPath::from_str("m/1/0").unwrap(),
540 DerivationPath::from_str("m/1/1").unwrap()
541 ],
542 );
543 let (paths, wildcard) = extract_first_appended_derivation_with_some_wildcard(
544 0,
545 "wsh(or_d(multi(2,@0/<8;9>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/2/<3;4;5>/*)),older(300))))",
546 )
547 .unwrap();
548 assert_eq!(wildcard, bip389::Wildcard::Unhardened);
549 assert_eq!(
550 paths,
551 vec![
552 DerivationPath::from_str("m/8").unwrap(),
553 DerivationPath::from_str("m/9").unwrap(),
554 ],
555 );
556 let (paths, wildcard) = extract_first_appended_derivation_with_some_wildcard(
557 1,
558 "wsh(or_d(multi(2,@0/<8;9>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/2/<3;4;5>/*)),older(300))))",
559 )
560 .unwrap();
561 assert_eq!(wildcard, bip389::Wildcard::Unhardened);
562 assert_eq!(
563 paths,
564 vec![
565 DerivationPath::from_str("m/0").unwrap(),
566 DerivationPath::from_str("m/1").unwrap(),
567 ],
568 );
569 let (paths, wildcard) = extract_first_appended_derivation_with_some_wildcard(
570 2,
571 "wsh(or_d(multi(2,@0/<0;1>/*,@1/<0;1>/*),and_v(v:thresh(2,pkh(@0/<2;3>/*),a:pkh(@1/<2;3>/*),a:pkh(@2/2/<3;4;5>/*)),older(300))))",
572 )
573 .unwrap();
574 assert_eq!(wildcard, bip389::Wildcard::Unhardened);
575 assert_eq!(
576 paths,
577 vec![
578 DerivationPath::from_str("m/2/3").unwrap(),
579 DerivationPath::from_str("m/2/4").unwrap(),
580 DerivationPath::from_str("m/2/5").unwrap()
581 ],
582 );
583 }
584}