bitbox_api/
btc.rs

1//! Functions and methods related to Bitcoin.
2
3use crate::runtime::Runtime;
4
5use crate::error::Error;
6use crate::pb::{self, request::Request, response::Response};
7use crate::Keypath;
8use crate::PairedBitBox;
9
10pub use bitcoin::{
11    bip32::{Fingerprint, Xpub},
12    blockdata::script::witness_version::WitnessVersion,
13    Script,
14};
15
16#[cfg(feature = "wasm")]
17use enum_assoc::Assoc;
18
19#[cfg(feature = "wasm")]
20pub(crate) fn serde_deserialize_simple_type<'de, D>(deserializer: D) -> Result<i32, D::Error>
21where
22    D: serde::Deserializer<'de>,
23{
24    use serde::Deserialize;
25    Ok(pb::btc_script_config::SimpleType::deserialize(deserializer)?.into())
26}
27
28#[cfg(feature = "wasm")]
29pub(crate) fn serde_deserialize_multisig<'de, D>(
30    deserializer: D,
31) -> Result<pb::btc_script_config::Multisig, D::Error>
32where
33    D: serde::Deserializer<'de>,
34{
35    use serde::Deserialize;
36    use std::str::FromStr;
37
38    #[derive(serde::Deserialize)]
39    #[serde(rename_all = "camelCase")]
40    struct Multisig {
41        threshold: u32,
42        xpubs: Vec<String>,
43        our_xpub_index: u32,
44        script_type: pb::btc_script_config::multisig::ScriptType,
45    }
46    let ms = Multisig::deserialize(deserializer)?;
47    let xpubs = ms
48        .xpubs
49        .iter()
50        .map(|s| Xpub::from_str(s.as_str()))
51        .collect::<Result<Vec<Xpub>, _>>()
52        .map_err(serde::de::Error::custom)?;
53    Ok(pb::btc_script_config::Multisig {
54        threshold: ms.threshold,
55        xpubs: xpubs.iter().map(convert_xpub).collect(),
56        our_xpub_index: ms.our_xpub_index,
57        script_type: ms.script_type.into(),
58    })
59}
60
61#[cfg(feature = "wasm")]
62#[derive(serde::Deserialize)]
63pub(crate) struct SerdeScriptConfig(pb::btc_script_config::Config);
64
65#[cfg(feature = "wasm")]
66impl From<SerdeScriptConfig> for pb::BtcScriptConfig {
67    fn from(value: SerdeScriptConfig) -> Self {
68        pb::BtcScriptConfig {
69            config: Some(value.0),
70        }
71    }
72}
73
74#[derive(Clone, Debug, PartialEq)]
75pub struct PrevTxInput {
76    pub prev_out_hash: Vec<u8>,
77    pub prev_out_index: u32,
78    pub signature_script: Vec<u8>,
79    pub sequence: u32,
80}
81
82impl From<&bitcoin::TxIn> for PrevTxInput {
83    fn from(value: &bitcoin::TxIn) -> Self {
84        PrevTxInput {
85            prev_out_hash: (value.previous_output.txid.as_ref() as &[u8]).to_vec(),
86            prev_out_index: value.previous_output.vout,
87            signature_script: value.script_sig.as_bytes().to_vec(),
88            sequence: value.sequence.to_consensus_u32(),
89        }
90    }
91}
92#[derive(Clone, Debug, PartialEq)]
93pub struct PrevTxOutput {
94    pub value: u64,
95    pub pubkey_script: Vec<u8>,
96}
97
98impl From<&bitcoin::TxOut> for PrevTxOutput {
99    fn from(value: &bitcoin::TxOut) -> Self {
100        PrevTxOutput {
101            value: value.value.to_sat(),
102            pubkey_script: value.script_pubkey.as_bytes().to_vec(),
103        }
104    }
105}
106
107#[derive(Clone, Debug, PartialEq)]
108pub struct PrevTx {
109    pub version: u32,
110    pub inputs: Vec<PrevTxInput>,
111    pub outputs: Vec<PrevTxOutput>,
112    pub locktime: u32,
113}
114
115impl From<&bitcoin::Transaction> for PrevTx {
116    fn from(value: &bitcoin::Transaction) -> Self {
117        PrevTx {
118            version: value.version.0 as _,
119            inputs: value.input.iter().map(PrevTxInput::from).collect(),
120            outputs: value.output.iter().map(PrevTxOutput::from).collect(),
121            locktime: value.lock_time.to_consensus_u32(),
122        }
123    }
124}
125
126#[derive(Debug, PartialEq)]
127pub struct TxInput {
128    pub prev_out_hash: Vec<u8>,
129    pub prev_out_index: u32,
130    pub prev_out_value: u64,
131    pub sequence: u32,
132    pub keypath: Keypath,
133    pub script_config_index: u32,
134    // Can be None if all transaction inputs are Taproot.
135    pub prev_tx: Option<PrevTx>,
136}
137
138impl TxInput {
139    fn get_prev_tx(&self) -> Result<&PrevTx, Error> {
140        self.prev_tx.as_ref().ok_or(Error::BtcSign(
141            "input's previous transaction required but missing".into(),
142        ))
143    }
144}
145
146#[derive(Debug, PartialEq)]
147pub struct TxInternalOutput {
148    pub keypath: Keypath,
149    pub value: u64,
150    pub script_config_index: u32,
151}
152
153#[derive(Debug, PartialEq)]
154pub struct Payload {
155    pub data: Vec<u8>,
156    pub output_type: pb::BtcOutputType,
157}
158
159#[derive(thiserror::Error, Debug)]
160pub enum PayloadError {
161    #[error("unrecognized pubkey script")]
162    Unrecognized,
163}
164
165impl Payload {
166    pub fn from_pkscript(pkscript: &[u8]) -> Result<Payload, PayloadError> {
167        let script = Script::from_bytes(pkscript);
168        if script.is_p2pkh() {
169            Ok(Payload {
170                data: pkscript[3..23].to_vec(),
171                output_type: pb::BtcOutputType::P2pkh,
172            })
173        } else if script.is_p2sh() {
174            Ok(Payload {
175                data: pkscript[2..22].to_vec(),
176                output_type: pb::BtcOutputType::P2sh,
177            })
178        } else if script.is_p2wpkh() {
179            Ok(Payload {
180                data: pkscript[2..].to_vec(),
181                output_type: pb::BtcOutputType::P2wpkh,
182            })
183        } else if script.is_p2wsh() {
184            Ok(Payload {
185                data: pkscript[2..].to_vec(),
186                output_type: pb::BtcOutputType::P2wsh,
187            })
188        } else if script.is_p2tr() {
189            Ok(Payload {
190                data: pkscript[2..].to_vec(),
191                output_type: pb::BtcOutputType::P2tr,
192            })
193        } else {
194            Err(PayloadError::Unrecognized)
195        }
196    }
197}
198
199#[derive(Debug, PartialEq)]
200pub struct TxExternalOutput {
201    pub payload: Payload,
202    pub value: u64,
203}
204
205impl TryFrom<&bitcoin::TxOut> for TxExternalOutput {
206    type Error = PsbtError;
207    fn try_from(value: &bitcoin::TxOut) -> Result<Self, Self::Error> {
208        Ok(TxExternalOutput {
209            payload: Payload::from_pkscript(value.script_pubkey.as_bytes())
210                .map_err(|_| PsbtError::UnknownOutputType)?,
211            value: value.value.to_sat(),
212        })
213    }
214}
215
216#[derive(Debug, PartialEq)]
217pub enum TxOutput {
218    Internal(TxInternalOutput),
219    External(TxExternalOutput),
220}
221
222#[derive(Debug, PartialEq)]
223pub struct Transaction {
224    pub script_configs: Vec<pb::BtcScriptConfigWithKeypath>,
225    pub version: u32,
226    pub inputs: Vec<TxInput>,
227    pub outputs: Vec<TxOutput>,
228    pub locktime: u32,
229}
230// See https://github.com/spesmilo/electrum/blob/84dc181b6e7bb20e88ef6b98fb8925c5f645a765/electrum/ecc.py#L521-L523
231#[derive(Debug, PartialEq, serde::Serialize)]
232#[serde(rename_all = "camelCase")]
233pub struct SignMessageSignature {
234    pub sig: Vec<u8>,
235    pub recid: u8,
236    pub electrum_sig65: Vec<u8>,
237}
238
239#[derive(thiserror::Error, Debug)]
240#[cfg_attr(feature = "wasm", derive(Assoc), func(pub const fn js_code(&self) -> &'static str))]
241pub enum PsbtError {
242    #[error("{0}")]
243    #[cfg_attr(feature = "wasm", assoc(js_code = "sign-error"))]
244    SignError(#[from] bitcoin::psbt::SignError),
245    #[error("Taproot pubkeys must be unique across the internal key and all leaf scripts.")]
246    #[cfg_attr(feature = "wasm", assoc(js_code = "key-not-unique"))]
247    KeyNotUnique,
248    #[error("Could not find our key in an input.")]
249    #[cfg_attr(feature = "wasm", assoc(js_code = "key-not-found"))]
250    KeyNotFound,
251    #[error("Unrecognized/unsupported output type.")]
252    #[cfg_attr(feature = "wasm", assoc(js_code = "unknown-output-type"))]
253    UnknownOutputType,
254}
255
256enum OurKey {
257    Segwit(bitcoin::secp256k1::PublicKey, Keypath),
258    TaprootInternal(Keypath),
259    TaprootScript(
260        bitcoin::secp256k1::XOnlyPublicKey,
261        bitcoin::taproot::TapLeafHash,
262        Keypath,
263    ),
264}
265
266impl OurKey {
267    fn keypath(&self) -> Keypath {
268        match self {
269            OurKey::Segwit(_, kp) => kp.clone(),
270            OurKey::TaprootInternal(kp) => kp.clone(),
271            OurKey::TaprootScript(_, _, kp) => kp.clone(),
272        }
273    }
274}
275
276trait PsbtOutputInfo {
277    fn get_bip32_derivation(
278        &self,
279    ) -> &std::collections::BTreeMap<bitcoin::secp256k1::PublicKey, bitcoin::bip32::KeySource>;
280
281    fn get_tap_internal_key(&self) -> Option<&bitcoin::secp256k1::XOnlyPublicKey>;
282    fn get_tap_key_origins(
283        &self,
284    ) -> &std::collections::BTreeMap<
285        bitcoin::secp256k1::XOnlyPublicKey,
286        (
287            Vec<bitcoin::taproot::TapLeafHash>,
288            bitcoin::bip32::KeySource,
289        ),
290    >;
291}
292
293impl PsbtOutputInfo for &bitcoin::psbt::Input {
294    fn get_bip32_derivation(
295        &self,
296    ) -> &std::collections::BTreeMap<bitcoin::secp256k1::PublicKey, bitcoin::bip32::KeySource> {
297        &self.bip32_derivation
298    }
299
300    fn get_tap_internal_key(&self) -> Option<&bitcoin::secp256k1::XOnlyPublicKey> {
301        self.tap_internal_key.as_ref()
302    }
303
304    fn get_tap_key_origins(
305        &self,
306    ) -> &std::collections::BTreeMap<
307        bitcoin::secp256k1::XOnlyPublicKey,
308        (
309            Vec<bitcoin::taproot::TapLeafHash>,
310            bitcoin::bip32::KeySource,
311        ),
312    > {
313        &self.tap_key_origins
314    }
315}
316
317impl PsbtOutputInfo for &bitcoin::psbt::Output {
318    fn get_bip32_derivation(
319        &self,
320    ) -> &std::collections::BTreeMap<bitcoin::secp256k1::PublicKey, bitcoin::bip32::KeySource> {
321        &self.bip32_derivation
322    }
323
324    fn get_tap_internal_key(&self) -> Option<&bitcoin::secp256k1::XOnlyPublicKey> {
325        self.tap_internal_key.as_ref()
326    }
327
328    fn get_tap_key_origins(
329        &self,
330    ) -> &std::collections::BTreeMap<
331        bitcoin::secp256k1::XOnlyPublicKey,
332        (
333            Vec<bitcoin::taproot::TapLeafHash>,
334            bitcoin::bip32::KeySource,
335        ),
336    > {
337        &self.tap_key_origins
338    }
339}
340
341fn find_our_key<T: PsbtOutputInfo>(
342    our_root_fingerprint: &[u8],
343    output_info: T,
344) -> Result<OurKey, PsbtError> {
345    for (xonly, (leaf_hashes, (fingerprint, derivation_path))) in
346        output_info.get_tap_key_origins().iter()
347    {
348        if &fingerprint[..] == our_root_fingerprint {
349            // TODO: check for fingerprint collision
350
351            if let Some(tap_internal_key) = output_info.get_tap_internal_key() {
352                if tap_internal_key == xonly {
353                    if !leaf_hashes.is_empty() {
354                        // TODO change err msg, we don't support the
355                        // same key as internal key and also in a leaf
356                        // script.
357                        return Err(PsbtError::KeyNotUnique);
358                    }
359                    return Ok(OurKey::TaprootInternal(derivation_path.into()));
360                }
361            }
362            if leaf_hashes.len() != 1 {
363                // TODO change err msg, per BIP-388 all pubkeys are
364                // unique, so it can't be in multiple leafs.
365                return Err(PsbtError::KeyNotUnique);
366            }
367            return Ok(OurKey::TaprootScript(
368                *xonly,
369                leaf_hashes[0],
370                derivation_path.into(),
371            ));
372        }
373    }
374    for (pubkey, (fingerprint, derivation_path)) in output_info.get_bip32_derivation().iter() {
375        if &fingerprint[..] == our_root_fingerprint {
376            // TODO: check for fingerprint collision
377            return Ok(OurKey::Segwit(*pubkey, derivation_path.into()));
378        }
379    }
380    Err(PsbtError::KeyNotFound)
381}
382
383fn script_config_from_utxo(
384    output: &bitcoin::TxOut,
385    keypath: Keypath,
386    redeem_script: Option<&bitcoin::ScriptBuf>,
387    _witness_script: Option<&bitcoin::ScriptBuf>,
388) -> Result<pb::BtcScriptConfigWithKeypath, PsbtError> {
389    let keypath = keypath.hardened_prefix();
390    if output.script_pubkey.is_p2wpkh() {
391        return Ok(pb::BtcScriptConfigWithKeypath {
392            script_config: Some(make_script_config_simple(
393                pb::btc_script_config::SimpleType::P2wpkh,
394            )),
395            keypath: keypath.to_vec(),
396        });
397    }
398    let redeem_script_is_p2wpkh = redeem_script.map(|s| s.is_p2wpkh()).unwrap_or(false);
399    if output.script_pubkey.is_p2sh() && redeem_script_is_p2wpkh {
400        return Ok(pb::BtcScriptConfigWithKeypath {
401            script_config: Some(make_script_config_simple(
402                pb::btc_script_config::SimpleType::P2wpkhP2sh,
403            )),
404            keypath: keypath.to_vec(),
405        });
406    }
407    if output.script_pubkey.is_p2tr() {
408        return Ok(pb::BtcScriptConfigWithKeypath {
409            script_config: Some(make_script_config_simple(
410                pb::btc_script_config::SimpleType::P2tr,
411            )),
412            keypath: keypath.to_vec(),
413        });
414    }
415    // Check for segwit multisig (p2wsh or p2wsh-p2sh).
416    let redeem_script_is_p2wsh = redeem_script.map(|s| s.is_p2wsh()).unwrap_or(false);
417    let is_p2wsh_p2sh = output.script_pubkey.is_p2sh() && redeem_script_is_p2wsh;
418    if output.script_pubkey.is_p2wsh() || is_p2wsh_p2sh {
419        todo!();
420    }
421    Err(PsbtError::UnknownOutputType)
422}
423
424impl Transaction {
425    fn from_psbt(
426        our_root_fingerprint: &[u8],
427        psbt: &bitcoin::psbt::Psbt,
428        force_script_config: Option<pb::BtcScriptConfigWithKeypath>,
429    ) -> Result<(Self, Vec<OurKey>), PsbtError> {
430        let mut script_configs: Vec<pb::BtcScriptConfigWithKeypath> = Vec::new();
431        let mut is_script_config_forced = false;
432        if let Some(cfg) = force_script_config {
433            script_configs.push(cfg);
434            is_script_config_forced = true;
435        }
436
437        let mut our_keys: Vec<OurKey> = Vec::new();
438        let mut inputs: Vec<TxInput> = Vec::new();
439
440        let mut add_script_config = |script_config: pb::BtcScriptConfigWithKeypath| -> usize {
441            match script_configs.iter().position(|el| el == &script_config) {
442                Some(pos) => pos,
443                None => {
444                    script_configs.push(script_config);
445                    script_configs.len() - 1
446                }
447            }
448        };
449
450        for (input_index, (tx_input, psbt_input)) in
451            psbt.unsigned_tx.input.iter().zip(&psbt.inputs).enumerate()
452        {
453            let utxo = psbt.spend_utxo(input_index)?;
454            let our_key = find_our_key(our_root_fingerprint, psbt_input)?;
455            let script_config_index = if is_script_config_forced {
456                0
457            } else {
458                add_script_config(script_config_from_utxo(
459                    utxo,
460                    our_key.keypath(),
461                    psbt_input.redeem_script.as_ref(),
462                    psbt_input.witness_script.as_ref(),
463                )?)
464            };
465
466            inputs.push(TxInput {
467                prev_out_hash: (tx_input.previous_output.txid.as_ref() as &[u8]).to_vec(),
468                prev_out_index: tx_input.previous_output.vout,
469                prev_out_value: utxo.value.to_sat(),
470                sequence: tx_input.sequence.to_consensus_u32(),
471                keypath: our_key.keypath(),
472                script_config_index: script_config_index as _,
473                prev_tx: psbt_input.non_witness_utxo.as_ref().map(PrevTx::from),
474            });
475            our_keys.push(our_key);
476        }
477
478        let mut outputs: Vec<TxOutput> = Vec::new();
479        for (tx_output, psbt_output) in psbt.unsigned_tx.output.iter().zip(&psbt.outputs) {
480            let our_key = find_our_key(our_root_fingerprint, psbt_output);
481            // Either change output or a non-change output owned by the BitBox.
482            match our_key {
483                Ok(our_key) => {
484                    let script_config_index = if is_script_config_forced {
485                        0
486                    } else {
487                        add_script_config(script_config_from_utxo(
488                            tx_output,
489                            our_key.keypath(),
490                            psbt_output.redeem_script.as_ref(),
491                            psbt_output.witness_script.as_ref(),
492                        )?)
493                    };
494                    outputs.push(TxOutput::Internal(TxInternalOutput {
495                        keypath: our_key.keypath(),
496                        value: tx_output.value.to_sat(),
497                        script_config_index: script_config_index as _,
498                    }));
499                }
500                Err(_) => {
501                    outputs.push(TxOutput::External(tx_output.try_into()?));
502                }
503            }
504        }
505
506        Ok((
507            Transaction {
508                script_configs,
509                version: psbt.unsigned_tx.version.0 as _,
510                inputs,
511                outputs,
512                locktime: psbt.unsigned_tx.lock_time.to_consensus_u32(),
513            },
514            our_keys,
515        ))
516    }
517}
518
519/// Create a single-sig script config.
520pub fn make_script_config_simple(
521    simple_type: pb::btc_script_config::SimpleType,
522) -> pb::BtcScriptConfig {
523    pb::BtcScriptConfig {
524        config: Some(pb::btc_script_config::Config::SimpleType(
525            simple_type.into(),
526        )),
527    }
528}
529
530#[derive(Clone)]
531#[cfg_attr(
532    feature = "wasm",
533    derive(serde::Deserialize),
534    serde(rename_all = "camelCase")
535)]
536#[derive(PartialEq)]
537pub struct KeyOriginInfo {
538    pub root_fingerprint: Option<bitcoin::bip32::Fingerprint>,
539    pub keypath: Option<Keypath>,
540    pub xpub: bitcoin::bip32::Xpub,
541}
542
543fn convert_xpub(xpub: &bitcoin::bip32::Xpub) -> pb::XPub {
544    pb::XPub {
545        depth: vec![xpub.depth],
546        parent_fingerprint: xpub.parent_fingerprint[..].to_vec(),
547        child_num: xpub.child_number.into(),
548        chain_code: xpub.chain_code[..].to_vec(),
549        public_key: xpub.public_key.serialize().to_vec(),
550    }
551}
552
553impl From<KeyOriginInfo> for pb::KeyOriginInfo {
554    fn from(value: KeyOriginInfo) -> Self {
555        pb::KeyOriginInfo {
556            root_fingerprint: value
557                .root_fingerprint
558                .map_or(vec![], |fp| fp.as_bytes().to_vec()),
559            keypath: value.keypath.map_or(vec![], |kp| kp.to_vec()),
560            xpub: Some(convert_xpub(&value.xpub)),
561        }
562    }
563}
564
565/// Create a multi-sig script config.
566pub fn make_script_config_multisig(
567    threshold: u32,
568    xpubs: &[bitcoin::bip32::Xpub],
569    our_xpub_index: u32,
570    script_type: pb::btc_script_config::multisig::ScriptType,
571) -> pb::BtcScriptConfig {
572    pb::BtcScriptConfig {
573        config: Some(pb::btc_script_config::Config::Multisig(
574            pb::btc_script_config::Multisig {
575                threshold,
576                xpubs: xpubs.iter().map(convert_xpub).collect(),
577                our_xpub_index,
578                script_type: script_type as _,
579            },
580        )),
581    }
582}
583
584/// Create a wallet policy script config according to the wallet policies BIP:
585/// <https://github.com/bitcoin/bips/pull/1389>
586///
587/// At least one of the keys must be ours, i.e. contain our root fingerprint and a keypath to one of
588/// our xpubs.
589pub fn make_script_config_policy(policy: &str, keys: &[KeyOriginInfo]) -> pb::BtcScriptConfig {
590    pb::BtcScriptConfig {
591        config: Some(pb::btc_script_config::Config::Policy(
592            pb::btc_script_config::Policy {
593                policy: policy.into(),
594                keys: keys.iter().cloned().map(pb::KeyOriginInfo::from).collect(),
595            },
596        )),
597    }
598}
599
600fn is_taproot_simple(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
601    matches!(
602        script_config.script_config.as_ref(),
603        Some(pb::BtcScriptConfig {
604            config: Some(pb::btc_script_config::Config::SimpleType(simple_type)),
605        }) if *simple_type == pb::btc_script_config::SimpleType::P2tr as i32
606    )
607}
608
609fn is_taproot_policy(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
610    matches!(
611        script_config.script_config.as_ref(),
612        Some(pb::BtcScriptConfig {
613            config: Some(pb::btc_script_config::Config::Policy(policy)),
614        })  if policy.policy.as_str().starts_with("tr("),
615    )
616}
617
618fn is_schnorr(script_config: &pb::BtcScriptConfigWithKeypath) -> bool {
619    is_taproot_simple(script_config) | is_taproot_policy(script_config)
620}
621
622impl<R: Runtime> PairedBitBox<R> {
623    /// Retrieves an xpub. For non-standard keypaths, a warning is displayed on the BitBox even if
624    /// `display` is false.
625    pub async fn btc_xpub(
626        &self,
627        coin: pb::BtcCoin,
628        keypath: &Keypath,
629        xpub_type: pb::btc_pub_request::XPubType,
630        display: bool,
631    ) -> Result<String, Error> {
632        match self
633            .query_proto(Request::BtcPub(pb::BtcPubRequest {
634                coin: coin as _,
635                keypath: keypath.to_vec(),
636                display,
637                output: Some(pb::btc_pub_request::Output::XpubType(xpub_type as _)),
638            }))
639            .await?
640        {
641            Response::Pub(pb::PubResponse { r#pub }) => Ok(r#pub),
642            _ => Err(Error::UnexpectedResponse),
643        }
644    }
645
646    /// Retrieves multiple xpubs at once. Only standard keypaths are allowed.
647    /// On firmware <v9.24.0, this falls back to calling `btc_xpub()` for each keypath.
648    pub async fn btc_xpubs(
649        &self,
650        coin: pb::BtcCoin,
651        keypaths: &[Keypath],
652        xpub_type: pb::btc_xpubs_request::XPubType,
653    ) -> Result<Vec<String>, Error> {
654        if self.validate_version(">=9.24.0").is_err() {
655            // Fallback to fetching them one-by-one on older firmware.
656            let mut xpubs = Vec::<String>::with_capacity(keypaths.len());
657            for keypath in keypaths {
658                let converted_xpub_type = match xpub_type {
659                    pb::btc_xpubs_request::XPubType::Unknown => return Err(Error::Unknown),
660                    pb::btc_xpubs_request::XPubType::Tpub => pb::btc_pub_request::XPubType::Tpub,
661                    pb::btc_xpubs_request::XPubType::Xpub => pb::btc_pub_request::XPubType::Xpub,
662                };
663                let xpub = self
664                    .btc_xpub(coin, keypath, converted_xpub_type, false)
665                    .await?;
666                xpubs.push(xpub);
667            }
668            return Ok(xpubs);
669        }
670        match self
671            .query_proto_btc(pb::btc_request::Request::Xpubs(pb::BtcXpubsRequest {
672                coin: coin as _,
673                xpub_type: xpub_type as _,
674                keypaths: keypaths.iter().map(|kp| kp.into()).collect(),
675            }))
676            .await?
677        {
678            pb::btc_response::Response::Pubs(pb::PubsResponse { pubs }) => Ok(pubs),
679            _ => Err(Error::UnexpectedResponse),
680        }
681    }
682
683    /// Retrieves a Bitcoin address at the provided keypath.
684    ///
685    /// For the simple script configs (single-sig), the keypath must follow the
686    /// BIP44/BIP49/BIP84/BIP86 conventions.
687    pub async fn btc_address(
688        &self,
689        coin: pb::BtcCoin,
690        keypath: &Keypath,
691        script_config: &pb::BtcScriptConfig,
692        display: bool,
693    ) -> Result<String, Error> {
694        match self
695            .query_proto(Request::BtcPub(pb::BtcPubRequest {
696                coin: coin as _,
697                keypath: keypath.to_vec(),
698                display,
699                output: Some(pb::btc_pub_request::Output::ScriptConfig(
700                    script_config.clone(),
701                )),
702            }))
703            .await?
704        {
705            Response::Pub(pb::PubResponse { r#pub }) => Ok(r#pub),
706            _ => Err(Error::UnexpectedResponse),
707        }
708    }
709
710    async fn query_proto_btc(
711        &self,
712        request: pb::btc_request::Request,
713    ) -> Result<pb::btc_response::Response, Error> {
714        match self
715            .query_proto(Request::Btc(pb::BtcRequest {
716                request: Some(request),
717            }))
718            .await?
719        {
720            Response::Btc(pb::BtcResponse {
721                response: Some(response),
722            }) => Ok(response),
723            _ => Err(Error::UnexpectedResponse),
724        }
725    }
726
727    async fn get_next_response(&self, request: Request) -> Result<pb::BtcSignNextResponse, Error> {
728        match self.query_proto(request).await? {
729            Response::BtcSignNext(next_response) => Ok(next_response),
730            _ => Err(Error::UnexpectedResponse),
731        }
732    }
733
734    async fn get_next_response_nested(
735        &self,
736        request: pb::btc_request::Request,
737    ) -> Result<pb::BtcSignNextResponse, Error> {
738        match self.query_proto_btc(request).await? {
739            pb::btc_response::Response::SignNext(next_response) => Ok(next_response),
740            _ => Err(Error::UnexpectedResponse),
741        }
742    }
743
744    /// Sign a Bitcoin transaction. Returns one 64 byte signature (compact serlization of the R and
745    /// S values) per input.
746    pub async fn btc_sign(
747        &self,
748        coin: pb::BtcCoin,
749        transaction: &Transaction,
750        format_unit: pb::btc_sign_init_request::FormatUnit,
751    ) -> Result<Vec<Vec<u8>>, Error> {
752        self.validate_version(">=9.4.0")?; // anti-klepto since 9.4.0
753        if transaction.script_configs.iter().any(is_taproot_simple) {
754            self.validate_version(">=9.10.0")?; // taproot since 9.10.0
755        }
756
757        let mut sigs: Vec<Vec<u8>> = Vec::new();
758
759        let mut next_response = self
760            .get_next_response(Request::BtcSignInit(pb::BtcSignInitRequest {
761                coin: coin as _,
762                script_configs: transaction.script_configs.clone(),
763                output_script_configs: vec![],
764                version: transaction.version,
765                num_inputs: transaction.inputs.len() as _,
766                num_outputs: transaction.outputs.len() as _,
767                locktime: transaction.locktime,
768                format_unit: format_unit as _,
769                contains_silent_payment_outputs: false,
770            }))
771            .await?;
772
773        let mut is_inputs_pass2 = false;
774        loop {
775            match pb::btc_sign_next_response::Type::try_from(next_response.r#type)
776                .map_err(|_| Error::UnexpectedResponse)?
777            {
778                pb::btc_sign_next_response::Type::Input => {
779                    let input_index: usize = next_response.index as _;
780                    let tx_input: &TxInput = &transaction.inputs[input_index];
781
782                    let input_is_schnorr = is_schnorr(
783                        &transaction.script_configs[tx_input.script_config_index as usize],
784                    );
785                    let perform_antiklepto = is_inputs_pass2 && !input_is_schnorr;
786                    let host_nonce = if perform_antiklepto {
787                        Some(crate::antiklepto::gen_host_nonce()?)
788                    } else {
789                        None
790                    };
791                    next_response = self
792                        .get_next_response(Request::BtcSignInput(pb::BtcSignInputRequest {
793                            prev_out_hash: tx_input.prev_out_hash.clone(),
794                            prev_out_index: tx_input.prev_out_index,
795                            prev_out_value: tx_input.prev_out_value,
796                            sequence: tx_input.sequence,
797                            keypath: tx_input.keypath.to_vec(),
798                            script_config_index: tx_input.script_config_index,
799                            host_nonce_commitment: host_nonce.as_ref().map(|host_nonce| {
800                                pb::AntiKleptoHostNonceCommitment {
801                                    commitment: crate::antiklepto::host_commit(host_nonce).to_vec(),
802                                }
803                            }),
804                        }))
805                        .await?;
806
807                    if let Some(host_nonce) = host_nonce {
808                        if next_response.r#type
809                            != pb::btc_sign_next_response::Type::HostNonce as i32
810                        {
811                            return Err(Error::UnexpectedResponse);
812                        }
813                        if let Some(pb::AntiKleptoSignerCommitment { commitment }) =
814                            next_response.anti_klepto_signer_commitment
815                        {
816                            next_response = self
817                                .get_next_response_nested(
818                                    pb::btc_request::Request::AntikleptoSignature(
819                                        pb::AntiKleptoSignatureRequest {
820                                            host_nonce: host_nonce.to_vec(),
821                                        },
822                                    ),
823                                )
824                                .await?;
825                            if !next_response.has_signature {
826                                return Err(Error::UnexpectedResponse);
827                            }
828                            crate::antiklepto::verify_ecdsa(
829                                &host_nonce,
830                                &commitment,
831                                &next_response.signature,
832                            )?
833                        } else {
834                            return Err(Error::UnexpectedResponse);
835                        }
836                    }
837
838                    if is_inputs_pass2 {
839                        if !next_response.has_signature {
840                            return Err(Error::UnexpectedResponse);
841                        }
842                        sigs.push(next_response.signature.clone());
843                    }
844                    if input_index == transaction.inputs.len() - 1 {
845                        is_inputs_pass2 = true
846                    }
847                }
848                pb::btc_sign_next_response::Type::PrevtxInit => {
849                    let prevtx: &PrevTx =
850                        transaction.inputs[next_response.index as usize].get_prev_tx()?;
851                    next_response = self
852                        .get_next_response_nested(pb::btc_request::Request::PrevtxInit(
853                            pb::BtcPrevTxInitRequest {
854                                version: prevtx.version,
855                                num_inputs: prevtx.inputs.len() as _,
856                                num_outputs: prevtx.outputs.len() as _,
857                                locktime: prevtx.locktime,
858                            },
859                        ))
860                        .await?;
861                }
862                pb::btc_sign_next_response::Type::PrevtxInput => {
863                    let prevtx: &PrevTx =
864                        transaction.inputs[next_response.index as usize].get_prev_tx()?;
865                    let prevtx_input: &PrevTxInput =
866                        &prevtx.inputs[next_response.prev_index as usize];
867                    next_response = self
868                        .get_next_response_nested(pb::btc_request::Request::PrevtxInput(
869                            pb::BtcPrevTxInputRequest {
870                                prev_out_hash: prevtx_input.prev_out_hash.clone(),
871                                prev_out_index: prevtx_input.prev_out_index,
872                                signature_script: prevtx_input.signature_script.clone(),
873                                sequence: prevtx_input.sequence,
874                            },
875                        ))
876                        .await?;
877                }
878                pb::btc_sign_next_response::Type::PrevtxOutput => {
879                    let prevtx: &PrevTx =
880                        transaction.inputs[next_response.index as usize].get_prev_tx()?;
881                    let prevtx_output: &PrevTxOutput =
882                        &prevtx.outputs[next_response.prev_index as usize];
883                    next_response = self
884                        .get_next_response_nested(pb::btc_request::Request::PrevtxOutput(
885                            pb::BtcPrevTxOutputRequest {
886                                value: prevtx_output.value,
887                                pubkey_script: prevtx_output.pubkey_script.clone(),
888                            },
889                        ))
890                        .await?;
891                }
892                pb::btc_sign_next_response::Type::Output => {
893                    let tx_output: &TxOutput = &transaction.outputs[next_response.index as usize];
894                    let request: Request = match tx_output {
895                        TxOutput::Internal(output) => {
896                            Request::BtcSignOutput(pb::BtcSignOutputRequest {
897                                ours: true,
898                                value: output.value,
899                                keypath: output.keypath.to_vec(),
900                                script_config_index: output.script_config_index,
901                                ..Default::default()
902                            })
903                        }
904                        TxOutput::External(output) => {
905                            Request::BtcSignOutput(pb::BtcSignOutputRequest {
906                                ours: false,
907                                value: output.value,
908                                r#type: output.payload.output_type as _,
909                                payload: output.payload.data.clone(),
910                                ..Default::default()
911                            })
912                        }
913                    };
914                    next_response = self.get_next_response(request).await?;
915                }
916                pb::btc_sign_next_response::Type::Done => break,
917                pb::btc_sign_next_response::Type::HostNonce => {
918                    return Err(Error::UnexpectedResponse);
919                }
920                _ => return Err(Error::UnexpectedResponse),
921            }
922        }
923        Ok(sigs)
924    }
925
926    /// Sign a PSBT.
927    ///
928    /// If `force_script_config` is None, we attempt to infer the involved script configs. For the
929    /// simple script config (single sig), we infer the script config from the involved redeem
930    /// scripts and provided derviation paths.
931    ///
932    /// Multisig and policy configs are currently not inferred and must be provided using
933    /// `force_script_config`.
934    pub async fn btc_sign_psbt(
935        &self,
936        coin: pb::BtcCoin,
937        psbt: &mut bitcoin::psbt::Psbt,
938        force_script_config: Option<pb::BtcScriptConfigWithKeypath>,
939        format_unit: pb::btc_sign_init_request::FormatUnit,
940    ) -> Result<(), Error> {
941        // since v9.15.0, the BitBox02 accepts "internal" outputs (ones sent to the BitBox02 with
942        // the keypath) even if the keypath is not a change keypath. PSBTs often contain the key
943        // origin info in outputs even in regular send-to-self outputs.
944        self.validate_version(">=9.15.0")?;
945
946        let our_root_fingerprint = hex::decode(self.root_fingerprint().await?).unwrap();
947        let (transaction, our_keys) =
948            Transaction::from_psbt(&our_root_fingerprint, psbt, force_script_config)?;
949        let signatures = self.btc_sign(coin, &transaction, format_unit).await?;
950        for (psbt_input, (signature, our_key)) in
951            psbt.inputs.iter_mut().zip(signatures.iter().zip(our_keys))
952        {
953            match our_key {
954                OurKey::Segwit(pubkey, _) => {
955                    psbt_input.partial_sigs.insert(
956                        bitcoin::PublicKey::new(pubkey),
957                        bitcoin::ecdsa::Signature {
958                            signature: bitcoin::secp256k1::ecdsa::Signature::from_compact(
959                                signature,
960                            )
961                            .map_err(|_| Error::InvalidSignature)?,
962                            sighash_type: bitcoin::sighash::EcdsaSighashType::All,
963                        },
964                    );
965                }
966                OurKey::TaprootInternal(_) => {
967                    psbt_input.tap_key_sig = Some(
968                        bitcoin::taproot::Signature::from_slice(signature)
969                            .map_err(|_| Error::InvalidSignature)?,
970                    );
971                }
972                OurKey::TaprootScript(xonly, leaf_hash, _) => {
973                    let sig = bitcoin::taproot::Signature::from_slice(signature)
974                        .map_err(|_| Error::InvalidSignature)?;
975                    psbt_input.tap_script_sigs.insert((xonly, leaf_hash), sig);
976                }
977            }
978        }
979        Ok(())
980    }
981
982    /// Sign a message.
983    pub async fn btc_sign_message(
984        &self,
985        coin: pb::BtcCoin,
986        script_config: pb::BtcScriptConfigWithKeypath,
987        msg: &[u8],
988    ) -> Result<SignMessageSignature, Error> {
989        self.validate_version(">=9.5.0")?;
990
991        let host_nonce = crate::antiklepto::gen_host_nonce()?;
992        let request = pb::BtcSignMessageRequest {
993            coin: coin as _,
994            script_config: Some(script_config),
995            msg: msg.to_vec(),
996            host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
997                commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
998            }),
999        };
1000
1001        let response = self
1002            .query_proto_btc(pb::btc_request::Request::SignMessage(request))
1003            .await?;
1004        let signer_commitment = match response {
1005            pb::btc_response::Response::AntikleptoSignerCommitment(
1006                pb::AntiKleptoSignerCommitment { commitment },
1007            ) => commitment,
1008            _ => return Err(Error::UnexpectedResponse),
1009        };
1010
1011        let request = pb::AntiKleptoSignatureRequest {
1012            host_nonce: host_nonce.to_vec(),
1013        };
1014
1015        let response = self
1016            .query_proto_btc(pb::btc_request::Request::AntikleptoSignature(request))
1017            .await?;
1018        let signature = match response {
1019            pb::btc_response::Response::SignMessage(pb::BtcSignMessageResponse { signature }) => {
1020                signature
1021            }
1022            _ => return Err(Error::UnexpectedResponse),
1023        };
1024        crate::antiklepto::verify_ecdsa(&host_nonce, &signer_commitment, &signature)?;
1025
1026        let sig = signature[..64].to_vec();
1027        let recid = signature[64];
1028        let compressed: u8 = 4; // BitBox02 uses only compressed pubkeys
1029        let sig65: u8 = 27 + compressed + recid;
1030        let mut electrum_sig65 = vec![sig65];
1031        electrum_sig65.extend_from_slice(&sig);
1032        Ok(SignMessageSignature {
1033            sig,
1034            recid,
1035            electrum_sig65,
1036        })
1037    }
1038
1039    /// Before a multisig or policy script config can be used to display receive addresses or sign
1040    /// transactions, it must be registered on the device. This function checks if the script config
1041    /// was already registered.
1042    ///
1043    /// `keypath_account` must be set if the script config is multisig, and can be `None` if it is a
1044    /// policy.
1045    pub async fn btc_is_script_config_registered(
1046        &self,
1047        coin: pb::BtcCoin,
1048        script_config: &pb::BtcScriptConfig,
1049        keypath_account: Option<&Keypath>,
1050    ) -> Result<bool, Error> {
1051        match self
1052            .query_proto_btc(pb::btc_request::Request::IsScriptConfigRegistered(
1053                pb::BtcIsScriptConfigRegisteredRequest {
1054                    registration: Some(pb::BtcScriptConfigRegistration {
1055                        coin: coin as _,
1056                        script_config: Some(script_config.clone()),
1057                        keypath: keypath_account.map_or(vec![], |kp| kp.to_vec()),
1058                    }),
1059                },
1060            ))
1061            .await?
1062        {
1063            pb::btc_response::Response::IsScriptConfigRegistered(response) => {
1064                Ok(response.is_registered)
1065            }
1066            _ => Err(Error::UnexpectedResponse),
1067        }
1068    }
1069
1070    /// Before a multisig or policy script config can be used to display receive addresses or sign
1071    /// transcations, it must be registered on the device.
1072    ///
1073    /// If no name is provided, the user will be asked to enter it on the device instead.  If
1074    /// provided, it must be non-empty, smaller or equal to 30 chars, consist only of printable
1075    /// ASCII characters, and contain no whitespace other than spaces.
1076    ///
1077    ///
1078    /// `keypath_account` must be set if the script config is multisig, and can be `None` if it is a
1079    /// policy.
1080    pub async fn btc_register_script_config(
1081        &self,
1082        coin: pb::BtcCoin,
1083        script_config: &pb::BtcScriptConfig,
1084        keypath_account: Option<&Keypath>,
1085        xpub_type: pb::btc_register_script_config_request::XPubType,
1086        name: Option<&str>,
1087    ) -> Result<(), Error> {
1088        match self
1089            .query_proto_btc(pb::btc_request::Request::RegisterScriptConfig(
1090                pb::BtcRegisterScriptConfigRequest {
1091                    registration: Some(pb::BtcScriptConfigRegistration {
1092                        coin: coin as _,
1093                        script_config: Some(script_config.clone()),
1094                        keypath: keypath_account.map_or(vec![], |kp| kp.to_vec()),
1095                    }),
1096                    name: name.unwrap_or("").into(),
1097                    xpub_type: xpub_type as _,
1098                },
1099            ))
1100            .await?
1101        {
1102            pb::btc_response::Response::Success(_) => Ok(()),
1103            _ => Err(Error::UnexpectedResponse),
1104        }
1105    }
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use super::*;
1111    use crate::keypath::HARDENED;
1112
1113    #[test]
1114    fn test_payload_from_pkscript() {
1115        use std::str::FromStr;
1116        // P2PKH
1117        let addr = bitcoin::Address::from_str("1AMZK8xzHJWsuRErpGZTiW4jKz8fdfLUGE")
1118            .unwrap()
1119            .assume_checked();
1120        let pkscript = addr.script_pubkey().into_bytes();
1121        assert_eq!(
1122            Payload::from_pkscript(&pkscript).unwrap(),
1123            Payload {
1124                data: pkscript[3..23].to_vec(),
1125                output_type: pb::BtcOutputType::P2pkh,
1126            }
1127        );
1128
1129        // P2SH
1130        let addr = bitcoin::Address::from_str("3JFL8CgtV4ZtMFYeP5LgV4JppLkHw5Gw9T")
1131            .unwrap()
1132            .assume_checked();
1133        let pkscript = addr.script_pubkey().into_bytes();
1134        assert_eq!(
1135            Payload::from_pkscript(&pkscript).unwrap(),
1136            Payload {
1137                data: pkscript[2..22].to_vec(),
1138                output_type: pb::BtcOutputType::P2sh,
1139            }
1140        );
1141
1142        // P2WPKH
1143        let addr = bitcoin::Address::from_str("bc1qkl8ms75cq6ajxtny7e88z3u9hkpkvktt5jwh6u")
1144            .unwrap()
1145            .assume_checked();
1146        let pkscript = addr.script_pubkey().into_bytes();
1147        assert_eq!(
1148            Payload::from_pkscript(&pkscript).unwrap(),
1149            Payload {
1150                data: pkscript[2..].to_vec(),
1151                output_type: pb::BtcOutputType::P2wpkh,
1152            }
1153        );
1154
1155        // P2WSH
1156        let addr = bitcoin::Address::from_str(
1157            "bc1q2fhgukymf0caaqrhfxrdju4wm94wwrch2ukntl5fuc0faz8zm49q0h6ss8",
1158        )
1159        .unwrap()
1160        .assume_checked();
1161        let pkscript = addr.script_pubkey().into_bytes();
1162        assert_eq!(
1163            Payload::from_pkscript(&pkscript).unwrap(),
1164            Payload {
1165                data: pkscript[2..].to_vec(),
1166                output_type: pb::BtcOutputType::P2wsh,
1167            }
1168        );
1169
1170        // P2TR
1171        let addr = bitcoin::Address::from_str(
1172            "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
1173        )
1174        .unwrap()
1175        .assume_checked();
1176        let pkscript = addr.script_pubkey().into_bytes();
1177        assert_eq!(
1178            Payload::from_pkscript(&pkscript).unwrap(),
1179            Payload {
1180                data: pkscript[2..].to_vec(),
1181                output_type: pb::BtcOutputType::P2tr,
1182            }
1183        );
1184    }
1185
1186    // Test that a PSBT containing only p2wpkh inputs is converted correctly to a transaction to be
1187    // signed by the BitBox.
1188    #[test]
1189    fn test_transaction_from_psbt_p2wpkh() {
1190        use std::str::FromStr;
1191
1192        // Based on mnemonic:
1193        // route glue else try obey local kidney future teach unaware pulse exclude.
1194        let psbt_str = "cHNidP8BAHECAAAAAfbXTun4YYxDroWyzRq3jDsWFVlsZ7HUzxiORY/iR4goAAAAAAD9////AuLCAAAAAAAAFgAUg3w5W0zt3AmxRmgA5Q6wZJUDRhUowwAAAAAAABYAFJjQqUoXDcwUEqfExu9pnaSn5XBct0ElAAABAR+ghgEAAAAAABYAFHn03igII+hp819N2Zlb5LnN8atRAQDfAQAAAAABAZ9EJlMJnXF5bFVrb1eFBYrEev3pg35WpvS3RlELsMMrAQAAAAD9////AqCGAQAAAAAAFgAUefTeKAgj6GnzX03ZmVvkuc3xq1EoRs4JAAAAABYAFKG2PzjYjknaA6lmXFqPaSgHwXX9AkgwRQIhAL0v0r3LisQ9KOlGzMhM/xYqUmrv2a5sORRlkX1fqDC8AiB9XqxSNEdb4mPnp7ylF1cAlbAZ7jMhgIxHUXylTww3bwEhA0AEOM0yYEpexPoKE3vT51uxZ+8hk9sOEfBFKOeo6oDDAAAAACIGAyNQfmAT/YLmZaxxfDwClmVNt2BkFnfQu/i8Uc/hHDUiGBKiwYlUAACAAQAAgAAAAIAAAAAAAAAAAAAAIgIDnxFM7Qr9LvJwQDB9GozdTRIe3MYVuHOqT7dU2EuvHrIYEqLBiVQAAIABAACAAAAAgAEAAAAAAAAAAA==";
1195
1196        let expected_transaction = Transaction {
1197            script_configs: vec![pb::BtcScriptConfigWithKeypath {
1198                script_config: Some(pb::BtcScriptConfig {
1199                    config: Some(pb::btc_script_config::Config::SimpleType(
1200                        pb::btc_script_config::SimpleType::P2wpkh as _,
1201                    )),
1202                }),
1203                keypath: vec![84 + HARDENED, 1 + HARDENED, HARDENED],
1204            }],
1205            version: 2,
1206            inputs: vec![TxInput {
1207                prev_out_hash: vec![
1208                    246, 215, 78, 233, 248, 97, 140, 67, 174, 133, 178, 205, 26, 183, 140, 59, 22,
1209                    21, 89, 108, 103, 177, 212, 207, 24, 142, 69, 143, 226, 71, 136, 40,
1210                ],
1211                prev_out_index: 0,
1212                prev_out_value: 100000,
1213                sequence: 4294967293,
1214                keypath: "m/84'/1'/0'/0/0".try_into().unwrap(),
1215                script_config_index: 0,
1216                prev_tx: Some(PrevTx {
1217                    version: 1,
1218                    inputs: vec![PrevTxInput {
1219                        prev_out_hash: vec![
1220                            159, 68, 38, 83, 9, 157, 113, 121, 108, 85, 107, 111, 87, 133, 5, 138,
1221                            196, 122, 253, 233, 131, 126, 86, 166, 244, 183, 70, 81, 11, 176, 195,
1222                            43,
1223                        ],
1224                        prev_out_index: 1,
1225                        signature_script: vec![],
1226                        sequence: 4294967293,
1227                    }],
1228                    outputs: vec![
1229                        PrevTxOutput {
1230                            value: 100000,
1231                            pubkey_script: vec![
1232                                0, 20, 121, 244, 222, 40, 8, 35, 232, 105, 243, 95, 77, 217, 153,
1233                                91, 228, 185, 205, 241, 171, 81,
1234                            ],
1235                        },
1236                        PrevTxOutput {
1237                            value: 164513320,
1238                            pubkey_script: vec![
1239                                0, 20, 161, 182, 63, 56, 216, 142, 73, 218, 3, 169, 102, 92, 90,
1240                                143, 105, 40, 7, 193, 117, 253,
1241                            ],
1242                        },
1243                    ],
1244                    locktime: 0,
1245                }),
1246            }],
1247            outputs: vec![
1248                TxOutput::External(TxExternalOutput {
1249                    payload: Payload {
1250                        data: vec![
1251                            131, 124, 57, 91, 76, 237, 220, 9, 177, 70, 104, 0, 229, 14, 176, 100,
1252                            149, 3, 70, 21,
1253                        ],
1254                        output_type: pb::BtcOutputType::P2wpkh,
1255                    },
1256                    value: 49890,
1257                }),
1258                TxOutput::Internal(TxInternalOutput {
1259                    keypath: "m/84'/1'/0'/1/0".try_into().unwrap(),
1260                    value: 49960,
1261                    script_config_index: 0,
1262                }),
1263            ],
1264            locktime: 2441655,
1265        };
1266        let our_root_fingerprint = hex::decode("12a2c189").unwrap();
1267        let psbt = bitcoin::psbt::Psbt::from_str(psbt_str).unwrap();
1268        let (transaction, _our_keys) =
1269            Transaction::from_psbt(&our_root_fingerprint, &psbt, None).unwrap();
1270        assert_eq!(transaction, expected_transaction);
1271    }
1272}