spark_sdk/wallet/utils/
bitcoin.rs

1use 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
9// P2TR functions
10pub(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
29// Transaction functions
30pub(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    // BIP341 taproot tweak with empty script tree
75    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}