async_hwi/
bitbox.rs

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    /// Bitbox and Coldcard sign with the first bip32_derivation that matches its fingerprint.
318    /// It may be useful to user utils::Bip32DerivationFilter to filter already signed derivations
319    /// and derivations collusion in case of multiple spending path per outputs.
320    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    // Do not include the hash in the descriptor template.
445    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(
453    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}