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 let _api_call = self.begin_api_call().await;
690 self.btc_xpub_inner(coin, keypath, xpub_type, display).await
691 }
692
693 async fn btc_xpub_inner(
694 &self,
695 coin: pb::BtcCoin,
696 keypath: &Keypath,
697 xpub_type: pb::btc_pub_request::XPubType,
698 display: bool,
699 ) -> Result<String, Error> {
700 match self
701 .query_proto(Request::BtcPub(pb::BtcPubRequest {
702 coin: coin as _,
703 keypath: keypath.to_vec(),
704 display,
705 output: Some(pb::btc_pub_request::Output::XpubType(xpub_type as _)),
706 }))
707 .await?
708 {
709 Response::Pub(pb::PubResponse { r#pub }) => Ok(r#pub),
710 _ => Err(Error::UnexpectedResponse),
711 }
712 }
713
714 pub async fn btc_xpubs(
717 &self,
718 coin: pb::BtcCoin,
719 keypaths: &[Keypath],
720 xpub_type: pb::btc_xpubs_request::XPubType,
721 ) -> Result<Vec<String>, Error> {
722 let _api_call = self.begin_api_call().await;
723 if self.validate_version(">=9.24.0").is_err() {
724 let mut xpubs = Vec::<String>::with_capacity(keypaths.len());
726 for keypath in keypaths {
727 let converted_xpub_type = match xpub_type {
728 pb::btc_xpubs_request::XPubType::Unknown => return Err(Error::Unknown),
729 pb::btc_xpubs_request::XPubType::Tpub => pb::btc_pub_request::XPubType::Tpub,
730 pb::btc_xpubs_request::XPubType::Xpub => pb::btc_pub_request::XPubType::Xpub,
731 };
732 let xpub = self
733 .btc_xpub_inner(coin, keypath, converted_xpub_type, false)
734 .await?;
735 xpubs.push(xpub);
736 }
737 return Ok(xpubs);
738 }
739 match self
740 .query_proto_btc(pb::btc_request::Request::Xpubs(pb::BtcXpubsRequest {
741 coin: coin as _,
742 xpub_type: xpub_type as _,
743 keypaths: keypaths.iter().map(|kp| kp.into()).collect(),
744 }))
745 .await?
746 {
747 pb::btc_response::Response::Pubs(pb::PubsResponse { pubs }) => Ok(pubs),
748 _ => Err(Error::UnexpectedResponse),
749 }
750 }
751
752 pub async fn btc_address(
757 &self,
758 coin: pb::BtcCoin,
759 keypath: &Keypath,
760 script_config: &pb::BtcScriptConfig,
761 display: bool,
762 ) -> Result<String, Error> {
763 let _api_call = self.begin_api_call().await;
764 match self
765 .query_proto(Request::BtcPub(pb::BtcPubRequest {
766 coin: coin as _,
767 keypath: keypath.to_vec(),
768 display,
769 output: Some(pb::btc_pub_request::Output::ScriptConfig(
770 script_config.clone(),
771 )),
772 }))
773 .await?
774 {
775 Response::Pub(pb::PubResponse { r#pub }) => Ok(r#pub),
776 _ => Err(Error::UnexpectedResponse),
777 }
778 }
779
780 async fn query_proto_btc(
781 &self,
782 request: pb::btc_request::Request,
783 ) -> Result<pb::btc_response::Response, Error> {
784 match self
785 .query_proto(Request::Btc(pb::BtcRequest {
786 request: Some(request),
787 }))
788 .await?
789 {
790 Response::Btc(pb::BtcResponse {
791 response: Some(response),
792 }) => Ok(response),
793 _ => Err(Error::UnexpectedResponse),
794 }
795 }
796
797 async fn get_next_response(&self, request: Request) -> Result<pb::BtcSignNextResponse, Error> {
798 match self.query_proto(request).await? {
799 Response::BtcSignNext(next_response) => Ok(next_response),
800 _ => Err(Error::UnexpectedResponse),
801 }
802 }
803
804 async fn get_next_response_nested(
805 &self,
806 request: pb::btc_request::Request,
807 ) -> Result<pb::BtcSignNextResponse, Error> {
808 match self.query_proto_btc(request).await? {
809 pb::btc_response::Response::SignNext(next_response) => Ok(next_response),
810 _ => Err(Error::UnexpectedResponse),
811 }
812 }
813
814 pub async fn btc_sign(
817 &self,
818 coin: pb::BtcCoin,
819 transaction: &Transaction,
820 format_unit: pb::btc_sign_init_request::FormatUnit,
821 ) -> Result<Vec<Vec<u8>>, Error> {
822 let _api_call = self.begin_api_call().await;
823 self.btc_sign_inner(coin, transaction, format_unit).await
824 }
825
826 async fn btc_sign_inner(
827 &self,
828 coin: pb::BtcCoin,
829 transaction: &Transaction,
830 format_unit: pb::btc_sign_init_request::FormatUnit,
831 ) -> Result<Vec<Vec<u8>>, Error> {
832 self.validate_version(">=9.4.0")?; if transaction.script_configs.iter().any(is_taproot_simple) {
834 self.validate_version(">=9.10.0")?; }
836 if transaction.outputs.iter().any(|output| {
837 matches!(
838 output,
839 TxOutput::External(tx_output)
840 if tx_output.payload.output_type == pb::BtcOutputType::OpReturn
841 )
842 }) {
843 self.validate_version(">=9.24.0")?;
844 }
845
846 let mut sigs: Vec<Vec<u8>> = Vec::new();
847
848 let mut next_response = self
849 .get_next_response(Request::BtcSignInit(pb::BtcSignInitRequest {
850 coin: coin as _,
851 script_configs: transaction.script_configs.clone(),
852 output_script_configs: vec![],
853 version: transaction.version,
854 num_inputs: transaction.inputs.len() as _,
855 num_outputs: transaction.outputs.len() as _,
856 locktime: transaction.locktime,
857 format_unit: format_unit as _,
858 contains_silent_payment_outputs: false,
859 }))
860 .await?;
861
862 let mut is_inputs_pass2 = false;
863 loop {
864 match pb::btc_sign_next_response::Type::try_from(next_response.r#type)
865 .map_err(|_| Error::UnexpectedResponse)?
866 {
867 pb::btc_sign_next_response::Type::Input => {
868 let input_index: usize = next_response.index as _;
869 let tx_input: &TxInput = &transaction.inputs[input_index];
870
871 let input_is_schnorr = is_schnorr(
872 &transaction.script_configs[tx_input.script_config_index as usize],
873 );
874 let perform_antiklepto = is_inputs_pass2 && !input_is_schnorr;
875 let host_nonce = if perform_antiklepto {
876 Some(crate::antiklepto::gen_host_nonce()?)
877 } else {
878 None
879 };
880 next_response = self
881 .get_next_response(Request::BtcSignInput(pb::BtcSignInputRequest {
882 prev_out_hash: tx_input.prev_out_hash.clone(),
883 prev_out_index: tx_input.prev_out_index,
884 prev_out_value: tx_input.prev_out_value,
885 sequence: tx_input.sequence,
886 keypath: tx_input.keypath.to_vec(),
887 script_config_index: tx_input.script_config_index,
888 host_nonce_commitment: host_nonce.as_ref().map(|host_nonce| {
889 pb::AntiKleptoHostNonceCommitment {
890 commitment: crate::antiklepto::host_commit(host_nonce).to_vec(),
891 }
892 }),
893 }))
894 .await?;
895
896 if let Some(host_nonce) = host_nonce {
897 if next_response.r#type
898 != pb::btc_sign_next_response::Type::HostNonce as i32
899 {
900 return Err(Error::UnexpectedResponse);
901 }
902 if let Some(pb::AntiKleptoSignerCommitment { commitment }) =
903 next_response.anti_klepto_signer_commitment
904 {
905 next_response = self
906 .get_next_response_nested(
907 pb::btc_request::Request::AntikleptoSignature(
908 pb::AntiKleptoSignatureRequest {
909 host_nonce: host_nonce.to_vec(),
910 },
911 ),
912 )
913 .await?;
914 if !next_response.has_signature {
915 return Err(Error::UnexpectedResponse);
916 }
917 crate::antiklepto::verify_ecdsa(
918 &host_nonce,
919 &commitment,
920 &next_response.signature,
921 )?
922 } else {
923 return Err(Error::UnexpectedResponse);
924 }
925 }
926
927 if is_inputs_pass2 {
928 if !next_response.has_signature {
929 return Err(Error::UnexpectedResponse);
930 }
931 sigs.push(next_response.signature.clone());
932 }
933 if input_index == transaction.inputs.len() - 1 {
934 is_inputs_pass2 = true
935 }
936 }
937 pb::btc_sign_next_response::Type::PrevtxInit => {
938 let prevtx: &PrevTx =
939 transaction.inputs[next_response.index as usize].get_prev_tx()?;
940 next_response = self
941 .get_next_response_nested(pb::btc_request::Request::PrevtxInit(
942 pb::BtcPrevTxInitRequest {
943 version: prevtx.version,
944 num_inputs: prevtx.inputs.len() as _,
945 num_outputs: prevtx.outputs.len() as _,
946 locktime: prevtx.locktime,
947 },
948 ))
949 .await?;
950 }
951 pb::btc_sign_next_response::Type::PrevtxInput => {
952 let prevtx: &PrevTx =
953 transaction.inputs[next_response.index as usize].get_prev_tx()?;
954 let prevtx_input: &PrevTxInput =
955 &prevtx.inputs[next_response.prev_index as usize];
956 next_response = self
957 .get_next_response_nested(pb::btc_request::Request::PrevtxInput(
958 pb::BtcPrevTxInputRequest {
959 prev_out_hash: prevtx_input.prev_out_hash.clone(),
960 prev_out_index: prevtx_input.prev_out_index,
961 signature_script: prevtx_input.signature_script.clone(),
962 sequence: prevtx_input.sequence,
963 },
964 ))
965 .await?;
966 }
967 pb::btc_sign_next_response::Type::PrevtxOutput => {
968 let prevtx: &PrevTx =
969 transaction.inputs[next_response.index as usize].get_prev_tx()?;
970 let prevtx_output: &PrevTxOutput =
971 &prevtx.outputs[next_response.prev_index as usize];
972 next_response = self
973 .get_next_response_nested(pb::btc_request::Request::PrevtxOutput(
974 pb::BtcPrevTxOutputRequest {
975 value: prevtx_output.value,
976 pubkey_script: prevtx_output.pubkey_script.clone(),
977 },
978 ))
979 .await?;
980 }
981 pb::btc_sign_next_response::Type::Output => {
982 let tx_output: &TxOutput = &transaction.outputs[next_response.index as usize];
983 let request: Request = match tx_output {
984 TxOutput::Internal(output) => {
985 Request::BtcSignOutput(pb::BtcSignOutputRequest {
986 ours: true,
987 value: output.value,
988 keypath: output.keypath.to_vec(),
989 script_config_index: output.script_config_index,
990 ..Default::default()
991 })
992 }
993 TxOutput::External(output) => {
994 Request::BtcSignOutput(pb::BtcSignOutputRequest {
995 ours: false,
996 value: output.value,
997 r#type: output.payload.output_type as _,
998 payload: output.payload.data.clone(),
999 ..Default::default()
1000 })
1001 }
1002 };
1003 next_response = self.get_next_response(request).await?;
1004 }
1005 pb::btc_sign_next_response::Type::Done => break,
1006 pb::btc_sign_next_response::Type::HostNonce => {
1007 return Err(Error::UnexpectedResponse);
1008 }
1009 _ => return Err(Error::UnexpectedResponse),
1010 }
1011 }
1012 Ok(sigs)
1013 }
1014
1015 pub async fn btc_sign_psbt(
1024 &self,
1025 coin: pb::BtcCoin,
1026 psbt: &mut bitcoin::psbt::Psbt,
1027 force_script_config: Option<pb::BtcScriptConfigWithKeypath>,
1028 format_unit: pb::btc_sign_init_request::FormatUnit,
1029 ) -> Result<(), Error> {
1030 let _api_call = self.begin_api_call().await;
1031 self.validate_version(">=9.15.0")?;
1035
1036 let our_root_fingerprint = self.root_fingerprint_inner().await?;
1037 let (transaction, our_keys) =
1038 Transaction::from_psbt(&our_root_fingerprint, psbt, force_script_config)?;
1039 let signatures = self.btc_sign_inner(coin, &transaction, format_unit).await?;
1040 for (psbt_input, (signature, our_key)) in
1041 psbt.inputs.iter_mut().zip(signatures.iter().zip(our_keys))
1042 {
1043 match our_key {
1044 OurKey::Segwit(pubkey, _) => {
1045 psbt_input.partial_sigs.insert(
1046 bitcoin::PublicKey::new(pubkey),
1047 bitcoin::ecdsa::Signature {
1048 signature: bitcoin::secp256k1::ecdsa::Signature::from_compact(
1049 signature,
1050 )
1051 .map_err(|_| Error::InvalidSignature)?,
1052 sighash_type: bitcoin::sighash::EcdsaSighashType::All,
1053 },
1054 );
1055 }
1056 OurKey::TaprootInternal(_) => {
1057 psbt_input.tap_key_sig = Some(
1058 bitcoin::taproot::Signature::from_slice(signature)
1059 .map_err(|_| Error::InvalidSignature)?,
1060 );
1061 }
1062 OurKey::TaprootScript(xonly, leaf_hash, _) => {
1063 let sig = bitcoin::taproot::Signature::from_slice(signature)
1064 .map_err(|_| Error::InvalidSignature)?;
1065 psbt_input.tap_script_sigs.insert((xonly, leaf_hash), sig);
1066 }
1067 }
1068 }
1069 Ok(())
1070 }
1071
1072 pub async fn btc_sign_message(
1074 &self,
1075 coin: pb::BtcCoin,
1076 script_config: pb::BtcScriptConfigWithKeypath,
1077 msg: &[u8],
1078 ) -> Result<SignMessageSignature, Error> {
1079 let _api_call = self.begin_api_call().await;
1080 self.validate_version(">=9.5.0")?;
1081
1082 let host_nonce = crate::antiklepto::gen_host_nonce()?;
1083 let request = pb::BtcSignMessageRequest {
1084 coin: coin as _,
1085 script_config: Some(script_config),
1086 msg: msg.to_vec(),
1087 host_nonce_commitment: Some(pb::AntiKleptoHostNonceCommitment {
1088 commitment: crate::antiklepto::host_commit(&host_nonce).to_vec(),
1089 }),
1090 };
1091
1092 let response = self
1093 .query_proto_btc(pb::btc_request::Request::SignMessage(request))
1094 .await?;
1095 let signer_commitment = match response {
1096 pb::btc_response::Response::AntikleptoSignerCommitment(
1097 pb::AntiKleptoSignerCommitment { commitment },
1098 ) => commitment,
1099 _ => return Err(Error::UnexpectedResponse),
1100 };
1101
1102 let request = pb::AntiKleptoSignatureRequest {
1103 host_nonce: host_nonce.to_vec(),
1104 };
1105
1106 let response = self
1107 .query_proto_btc(pb::btc_request::Request::AntikleptoSignature(request))
1108 .await?;
1109 let signature = match response {
1110 pb::btc_response::Response::SignMessage(pb::BtcSignMessageResponse { signature }) => {
1111 signature
1112 }
1113 _ => return Err(Error::UnexpectedResponse),
1114 };
1115 crate::antiklepto::verify_ecdsa(&host_nonce, &signer_commitment, &signature)?;
1116
1117 let sig = signature[..64].to_vec();
1118 let recid = signature[64];
1119 let compressed: u8 = 4; let sig65: u8 = 27 + compressed + recid;
1121 let mut electrum_sig65 = vec![sig65];
1122 electrum_sig65.extend_from_slice(&sig);
1123 Ok(SignMessageSignature {
1124 sig,
1125 recid,
1126 electrum_sig65,
1127 })
1128 }
1129
1130 pub async fn btc_is_script_config_registered(
1137 &self,
1138 coin: pb::BtcCoin,
1139 script_config: &pb::BtcScriptConfig,
1140 keypath_account: Option<&Keypath>,
1141 ) -> Result<bool, Error> {
1142 let _api_call = self.begin_api_call().await;
1143 match self
1144 .query_proto_btc(pb::btc_request::Request::IsScriptConfigRegistered(
1145 pb::BtcIsScriptConfigRegisteredRequest {
1146 registration: Some(pb::BtcScriptConfigRegistration {
1147 coin: coin as _,
1148 script_config: Some(script_config.clone()),
1149 keypath: keypath_account.map_or(vec![], |kp| kp.to_vec()),
1150 }),
1151 },
1152 ))
1153 .await?
1154 {
1155 pb::btc_response::Response::IsScriptConfigRegistered(response) => {
1156 Ok(response.is_registered)
1157 }
1158 _ => Err(Error::UnexpectedResponse),
1159 }
1160 }
1161
1162 pub async fn btc_register_script_config(
1173 &self,
1174 coin: pb::BtcCoin,
1175 script_config: &pb::BtcScriptConfig,
1176 keypath_account: Option<&Keypath>,
1177 xpub_type: pb::btc_register_script_config_request::XPubType,
1178 name: Option<&str>,
1179 ) -> Result<(), Error> {
1180 let _api_call = self.begin_api_call().await;
1181 match self
1182 .query_proto_btc(pb::btc_request::Request::RegisterScriptConfig(
1183 pb::BtcRegisterScriptConfigRequest {
1184 registration: Some(pb::BtcScriptConfigRegistration {
1185 coin: coin as _,
1186 script_config: Some(script_config.clone()),
1187 keypath: keypath_account.map_or(vec![], |kp| kp.to_vec()),
1188 }),
1189 name: name.unwrap_or("").into(),
1190 xpub_type: xpub_type as _,
1191 },
1192 ))
1193 .await?
1194 {
1195 pb::btc_response::Response::Success(_) => Ok(()),
1196 _ => Err(Error::UnexpectedResponse),
1197 }
1198 }
1199}
1200
1201#[cfg(test)]
1202mod tests {
1203 use super::*;
1204 use crate::keypath::HARDENED;
1205
1206 #[test]
1207 fn test_payload_from_pkscript() {
1208 use std::str::FromStr;
1209 let addr = bitcoin::Address::from_str("1AMZK8xzHJWsuRErpGZTiW4jKz8fdfLUGE")
1211 .unwrap()
1212 .assume_checked();
1213 let pkscript = addr.script_pubkey().into_bytes();
1214 assert_eq!(
1215 Payload::from_pkscript(&pkscript).unwrap(),
1216 Payload {
1217 data: pkscript[3..23].to_vec(),
1218 output_type: pb::BtcOutputType::P2pkh,
1219 }
1220 );
1221
1222 let addr = bitcoin::Address::from_str("3JFL8CgtV4ZtMFYeP5LgV4JppLkHw5Gw9T")
1224 .unwrap()
1225 .assume_checked();
1226 let pkscript = addr.script_pubkey().into_bytes();
1227 assert_eq!(
1228 Payload::from_pkscript(&pkscript).unwrap(),
1229 Payload {
1230 data: pkscript[2..22].to_vec(),
1231 output_type: pb::BtcOutputType::P2sh,
1232 }
1233 );
1234
1235 let addr = bitcoin::Address::from_str("bc1qkl8ms75cq6ajxtny7e88z3u9hkpkvktt5jwh6u")
1237 .unwrap()
1238 .assume_checked();
1239 let pkscript = addr.script_pubkey().into_bytes();
1240 assert_eq!(
1241 Payload::from_pkscript(&pkscript).unwrap(),
1242 Payload {
1243 data: pkscript[2..].to_vec(),
1244 output_type: pb::BtcOutputType::P2wpkh,
1245 }
1246 );
1247
1248 let addr = bitcoin::Address::from_str(
1250 "bc1q2fhgukymf0caaqrhfxrdju4wm94wwrch2ukntl5fuc0faz8zm49q0h6ss8",
1251 )
1252 .unwrap()
1253 .assume_checked();
1254 let pkscript = addr.script_pubkey().into_bytes();
1255 assert_eq!(
1256 Payload::from_pkscript(&pkscript).unwrap(),
1257 Payload {
1258 data: pkscript[2..].to_vec(),
1259 output_type: pb::BtcOutputType::P2wsh,
1260 }
1261 );
1262
1263 let addr = bitcoin::Address::from_str(
1265 "bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
1266 )
1267 .unwrap()
1268 .assume_checked();
1269 let pkscript = addr.script_pubkey().into_bytes();
1270 assert_eq!(
1271 Payload::from_pkscript(&pkscript).unwrap(),
1272 Payload {
1273 data: pkscript[2..].to_vec(),
1274 output_type: pb::BtcOutputType::P2tr,
1275 }
1276 );
1277
1278 let pkscript = hex::decode("6a00").unwrap();
1280 assert_eq!(
1281 Payload::from_pkscript(&pkscript).unwrap(),
1282 Payload {
1283 data: Vec::new(),
1284 output_type: pb::BtcOutputType::OpReturn,
1285 }
1286 );
1287
1288 let pkscript = hex::decode("6a03aabbcc").unwrap();
1290 assert_eq!(
1291 Payload::from_pkscript(&pkscript).unwrap(),
1292 Payload {
1293 data: vec![0xaa, 0xbb, 0xcc],
1294 output_type: pb::BtcOutputType::OpReturn,
1295 }
1296 );
1297
1298 let mut pkscript = vec![opcodes::all::OP_RETURN.to_u8(), 0x4c, 0x50];
1300 pkscript.extend(std::iter::repeat_n(0xaa, 80));
1301 assert_eq!(
1302 Payload::from_pkscript(&pkscript).unwrap(),
1303 Payload {
1304 data: vec![0xaa; 80],
1305 output_type: pb::BtcOutputType::OpReturn,
1306 }
1307 );
1308
1309 let pkscript = hex::decode("6a").unwrap();
1311 assert!(matches!(
1312 Payload::from_pkscript(&pkscript),
1313 Err(PayloadError::InvalidOpReturn(
1314 "naked OP_RETURN is not supported"
1315 ))
1316 ));
1317
1318 let pkscript = hex::decode("6a6a").unwrap();
1319 assert!(matches!(
1320 Payload::from_pkscript(&pkscript),
1321 Err(PayloadError::InvalidOpReturn(
1322 "no data push found after OP_RETURN"
1323 ))
1324 ));
1325
1326 let pkscript = hex::decode("6a0000").unwrap();
1327 assert!(matches!(
1328 Payload::from_pkscript(&pkscript),
1329 Err(PayloadError::InvalidOpReturn(
1330 "only one data push supported after OP_RETURN"
1331 ))
1332 ));
1333 }
1334
1335 #[test]
1338 fn test_transaction_from_psbt_p2wpkh() {
1339 use std::str::FromStr;
1340
1341 let psbt_str = "cHNidP8BAHECAAAAAfbXTun4YYxDroWyzRq3jDsWFVlsZ7HUzxiORY/iR4goAAAAAAD9////AuLCAAAAAAAAFgAUg3w5W0zt3AmxRmgA5Q6wZJUDRhUowwAAAAAAABYAFJjQqUoXDcwUEqfExu9pnaSn5XBct0ElAAABAR+ghgEAAAAAABYAFHn03igII+hp819N2Zlb5LnN8atRAQDfAQAAAAABAZ9EJlMJnXF5bFVrb1eFBYrEev3pg35WpvS3RlELsMMrAQAAAAD9////AqCGAQAAAAAAFgAUefTeKAgj6GnzX03ZmVvkuc3xq1EoRs4JAAAAABYAFKG2PzjYjknaA6lmXFqPaSgHwXX9AkgwRQIhAL0v0r3LisQ9KOlGzMhM/xYqUmrv2a5sORRlkX1fqDC8AiB9XqxSNEdb4mPnp7ylF1cAlbAZ7jMhgIxHUXylTww3bwEhA0AEOM0yYEpexPoKE3vT51uxZ+8hk9sOEfBFKOeo6oDDAAAAACIGAyNQfmAT/YLmZaxxfDwClmVNt2BkFnfQu/i8Uc/hHDUiGBKiwYlUAACAAQAAgAAAAIAAAAAAAAAAAAAAIgIDnxFM7Qr9LvJwQDB9GozdTRIe3MYVuHOqT7dU2EuvHrIYEqLBiVQAAIABAACAAAAAgAEAAAAAAAAAAA==";
1344
1345 let expected_transaction = Transaction {
1346 script_configs: vec![pb::BtcScriptConfigWithKeypath {
1347 script_config: Some(pb::BtcScriptConfig {
1348 config: Some(pb::btc_script_config::Config::SimpleType(
1349 pb::btc_script_config::SimpleType::P2wpkh as _,
1350 )),
1351 }),
1352 keypath: vec![84 + HARDENED, 1 + HARDENED, HARDENED],
1353 }],
1354 version: 2,
1355 inputs: vec![TxInput {
1356 prev_out_hash: vec![
1357 246, 215, 78, 233, 248, 97, 140, 67, 174, 133, 178, 205, 26, 183, 140, 59, 22,
1358 21, 89, 108, 103, 177, 212, 207, 24, 142, 69, 143, 226, 71, 136, 40,
1359 ],
1360 prev_out_index: 0,
1361 prev_out_value: 100000,
1362 sequence: 4294967293,
1363 keypath: "m/84'/1'/0'/0/0".try_into().unwrap(),
1364 script_config_index: 0,
1365 prev_tx: Some(PrevTx {
1366 version: 1,
1367 inputs: vec![PrevTxInput {
1368 prev_out_hash: vec![
1369 159, 68, 38, 83, 9, 157, 113, 121, 108, 85, 107, 111, 87, 133, 5, 138,
1370 196, 122, 253, 233, 131, 126, 86, 166, 244, 183, 70, 81, 11, 176, 195,
1371 43,
1372 ],
1373 prev_out_index: 1,
1374 signature_script: vec![],
1375 sequence: 4294967293,
1376 }],
1377 outputs: vec![
1378 PrevTxOutput {
1379 value: 100000,
1380 pubkey_script: vec![
1381 0, 20, 121, 244, 222, 40, 8, 35, 232, 105, 243, 95, 77, 217, 153,
1382 91, 228, 185, 205, 241, 171, 81,
1383 ],
1384 },
1385 PrevTxOutput {
1386 value: 164513320,
1387 pubkey_script: vec![
1388 0, 20, 161, 182, 63, 56, 216, 142, 73, 218, 3, 169, 102, 92, 90,
1389 143, 105, 40, 7, 193, 117, 253,
1390 ],
1391 },
1392 ],
1393 locktime: 0,
1394 }),
1395 }],
1396 outputs: vec![
1397 TxOutput::External(TxExternalOutput {
1398 payload: Payload {
1399 data: vec![
1400 131, 124, 57, 91, 76, 237, 220, 9, 177, 70, 104, 0, 229, 14, 176, 100,
1401 149, 3, 70, 21,
1402 ],
1403 output_type: pb::BtcOutputType::P2wpkh,
1404 },
1405 value: 49890,
1406 }),
1407 TxOutput::Internal(TxInternalOutput {
1408 keypath: "m/84'/1'/0'/1/0".try_into().unwrap(),
1409 value: 49960,
1410 script_config_index: 0,
1411 }),
1412 ],
1413 locktime: 2441655,
1414 };
1415 let our_root_fingerprint = hex::decode("12a2c189").unwrap();
1416 let psbt = bitcoin::psbt::Psbt::from_str(psbt_str).unwrap();
1417 let (transaction, _our_keys) =
1418 Transaction::from_psbt(&our_root_fingerprint, &psbt, None).unwrap();
1419 assert_eq!(transaction, expected_transaction);
1420 }
1421}