spark_sdk/wallet/utils/
bitcoin.rs1use bitcoin::consensus::deserialize;
2use bitcoin::hashes::Hash;
3use bitcoin::key::TapTweak;
4use bitcoin::secp256k1::{PublicKey, Secp256k1};
5use bitcoin::{Address, Network, ScriptBuf, Transaction, XOnlyPublicKey};
6
7use crate::error::SparkSdkError;
8
9pub(crate) fn p2tr_script_from_pubkey(pubkey: &PublicKey, network: Network) -> ScriptBuf {
11 let secp = Secp256k1::new();
12 let xonly = XOnlyPublicKey::from(*pubkey);
13 let address = Address::p2tr(&secp, xonly, None, network);
14 address.script_pubkey()
15}
16
17pub(crate) fn get_p2tr_address_from_public_key(
18 pubkey: &[u8],
19 network: Network,
20) -> Result<Address, SparkSdkError> {
21 let secp = Secp256k1::new();
22 let pubkey = PublicKey::from_slice(pubkey)?;
23 let xonly = XOnlyPublicKey::from(pubkey);
24
25 let address = Address::p2tr(&secp, xonly, None, network);
26 Ok(address)
27}
28
29pub(crate) fn bitcoin_tx_from_hex(raw_tx_hex: &str) -> Result<Transaction, SparkSdkError> {
31 let tx_bytes = hex::decode(raw_tx_hex).map_err(|e| {
32 SparkSdkError::InvalidBitcoinTransaction(format!("Failed to decode hex: {}", e))
33 })?;
34 bitcoin_tx_from_bytes(&tx_bytes)
35}
36
37pub(crate) fn bitcoin_tx_from_bytes(raw_tx_bytes: &[u8]) -> Result<Transaction, SparkSdkError> {
38 if raw_tx_bytes.len() == 0 {
39 return Err(SparkSdkError::InvalidBitcoinTransaction(
40 "Cannot deserialize Bitcoin transaction: buffer is empty".to_string(),
41 ));
42 }
43
44 let transaction = deserialize(raw_tx_bytes)
45 .map_err(|_| SparkSdkError::InvalidBitcoinTransaction("Invalid transaction".to_string()))?;
46
47 Ok(transaction)
48}
49
50pub(crate) fn sighash_from_tx(
51 tx: &bitcoin::Transaction,
52 input_index: usize,
53 prev_output: &bitcoin::TxOut,
54) -> Result<[u8; 32], SparkSdkError> {
55 let prevouts_arr = [prev_output.clone()];
56 let prev_output_fetcher = bitcoin::sighash::Prevouts::All(&prevouts_arr);
57
58 let sighash = bitcoin::sighash::SighashCache::new(tx)
59 .taproot_key_spend_signature_hash(
60 input_index,
61 &prev_output_fetcher,
62 bitcoin::sighash::TapSighashType::Default,
63 )
64 .map_err(|e| SparkSdkError::InvalidInput(e.to_string()))?;
65
66 Ok(sighash.to_raw_hash().to_byte_array())
67}
68
69pub(crate) fn compute_taproot_key_no_script(
70 pubkey: bitcoin::secp256k1::PublicKey,
71) -> Result<bitcoin::XOnlyPublicKey, SparkSdkError> {
72 let (x_only_pub, _) = pubkey.x_only_public_key();
73
74 let (tweaked_key, _parity) = x_only_pub.tap_tweak(&bitcoin::secp256k1::Secp256k1::new(), None);
76
77 Ok(tweaked_key.to_inner())
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83 use bitcoin::absolute::LockTime;
84 use bitcoin::transaction::Version;
85 use bitcoin::Network;
86 use bitcoin::{Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Witness};
87
88 #[test]
89 fn test_p2tr_address_from_public_key() {
90 let test_vectors = vec![
91 (
92 "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
93 "bc1pmfr3p9j00pfxjh0zmgp99y8zftmd3s5pmedqhyptwy6lm87hf5sspknck9",
94 Network::Bitcoin,
95 ),
96 (
97 "03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41",
98 "tb1p8dlmzllfah294ntwatr8j5uuvcj7yg0dete94ck2krrk0ka2c9qqex96hv",
99 Network::Testnet,
100 ),
101 ];
102
103 for (pubkey_hex, expected_addr, network) in test_vectors {
104 let pubkey = hex::decode(pubkey_hex).unwrap();
105 let addr = get_p2tr_address_from_public_key(&pubkey, network).unwrap();
106 assert_eq!(&addr.to_string(), expected_addr);
107 }
108 }
109
110 #[test]
111 fn test_tx_from_raw_tx_hex() {
112 let raw_tx_hex = "02000000000102dc552c6c0ef5ed0d8cd64bd1d2d1ffd7cf0ec0b5ad8df2a4c6269b59cffcc696010000000000000000603fbd40e86ee82258c57571c557b89a444aabf5b6a05574e6c6848379febe9a00000000000000000002e86905000000000022512024741d89092c5965f35a63802352fa9c7fae4a23d471b9dceb3379e8ff6b7dd1d054080000000000220020aea091435e74e3c1eba0bd964e67a05f300ace9e73efa66fe54767908f3e68800140f607486d87f59af453d62cffe00b6836d8cca2c89a340fab5fe842b20696908c77fd2f64900feb0cbb1c14da3e02271503fc465fcfb1b043c8187dccdd494558014067dff0f0c321fc8abc28bf555acfdfa5ee889b6909b24bc66cedf05e8cc2750a4d95037c3dc9c24f1e502198bade56fef61a2504809f5b2a60a62afeaf8bf52e00000000";
113 let result_hex = bitcoin_tx_from_hex(raw_tx_hex);
114 assert!(result_hex.is_ok());
115
116 let tx_bytes = hex::decode(raw_tx_hex).unwrap();
117 let result_bytes = bitcoin_tx_from_bytes(&tx_bytes);
118 assert!(result_bytes.is_ok());
119
120 assert_eq!(result_hex.unwrap(), result_bytes.unwrap());
121 }
122
123 #[test]
124 fn test_sighash_from_tx() {
125 let prev_tx_hex = "020000000001010cb9feccc0bdaac30304e469c50b4420c13c43d466e13813fcf42a73defd3f010000000000ffffffff018038010000000000225120d21e50e12ae122b4a5662c09b67cec7449c8182913bc06761e8b65f0fa2242f701400536f9b7542799f98739eeb6c6adaeb12d7bd418771bc5c6847f2abd19297bd466153600af26ccf0accb605c11ad667c842c5713832af4b7b11f1bcebe57745900000000";
126 let prev_tx = bitcoin_tx_from_hex(prev_tx_hex).unwrap();
127
128 let tx = Transaction {
129 version: Version::TWO,
130 lock_time: LockTime::ZERO,
131 input: vec![TxIn {
132 previous_output: OutPoint {
133 txid: prev_tx.compute_txid(),
134 vout: 0,
135 },
136 script_sig: ScriptBuf::new(),
137 sequence: bitcoin::Sequence::MAX,
138 witness: Witness::default(),
139 }],
140 output: vec![TxOut {
141 value: Amount::from_sat(70_000),
142 script_pubkey: prev_tx.output[0].script_pubkey.clone(),
143 }],
144 };
145
146 let sighash = sighash_from_tx(&tx, 0, &prev_tx.output[0]).unwrap();
147 assert_eq!(
148 hex::encode(sighash),
149 "8da5e7aa2b03491d7c2f4359ea4968dd58f69adf9af1a2c6881be0295591c293"
150 );
151 }
152}