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