spark_rust/spark_test_utils/
faucet.rs

1use std::rc::Rc;
2
3use bitcoin::{
4    absolute::LockTime,
5    key::{Keypair, Secp256k1, TapTweak},
6    secp256k1::{Message, SecretKey},
7    sighash::{Prevouts, SighashCache},
8    transaction::Version,
9    Address, Amount, Network, OutPoint, ScriptBuf, Sequence, TapSighashType, Transaction, TxIn,
10    TxOut, Witness,
11};
12use rand::thread_rng;
13
14use crate::{
15    error::{CryptoError, NetworkError, SparkSdkError, ValidationError},
16    wallet::{
17        mempool::MempoolClient,
18        utils::bitcoin::{p2tr_address_from_public_key, p2tr_script_from_pubkey},
19    },
20    SparkNetwork,
21};
22
23use crate::spark_test_utils::bitcoind::BitcoindClient;
24
25// Add serde import
26use serde::Deserialize;
27
28#[allow(dead_code)]
29pub struct FaucetCoin {
30    key: SecretKey,
31    outpoint: OutPoint,
32    txout: TxOut,
33}
34
35#[allow(dead_code)]
36impl FaucetCoin {
37    pub fn tx_to_address(
38        &self,
39        p2tr_address: &Address,
40        amount: Amount,
41    ) -> Result<Transaction, SparkSdkError> {
42        let secp = Secp256k1::new();
43
44        let value = self.txout.value;
45        let fees = Amount::from_sat(1000);
46
47        if value - fees < amount {
48            return Err(SparkSdkError::from(ValidationError::InvalidInput {
49                field: format!(
50                    "Amount {amount} is greater than the coin value {value} (incl. fees)"
51                ),
52            }));
53        }
54
55        // Create a second output that spends the rest of the funds to a throwaway address,
56        // otherwise bitcoind will complain about the fees being too high.
57        let throwaway_keypair = Keypair::new(&secp, &mut thread_rng());
58        let throwaway_address =
59            p2tr_address_from_public_key(&throwaway_keypair.public_key(), Network::Regtest);
60
61        self.tx_to_outputs(vec![
62            TxOut {
63                value: amount,
64                script_pubkey: p2tr_address.script_pubkey(),
65            },
66            TxOut {
67                value: value - amount - fees,
68                script_pubkey: throwaway_address.script_pubkey(),
69            },
70        ])
71    }
72
73    pub fn tx_to_outputs(&self, outputs: Vec<TxOut>) -> Result<Transaction, SparkSdkError> {
74        let secp = Secp256k1::new();
75
76        let mut tx = Transaction {
77            version: Version::TWO,
78            lock_time: LockTime::ZERO,
79            input: vec![TxIn {
80                previous_output: self.outpoint,
81                script_sig: ScriptBuf::new(),
82                sequence: Sequence::default(),
83                witness: Witness::new(),
84            }],
85            output: outputs,
86        };
87
88        let mut sighash_cache = SighashCache::new(&mut tx);
89
90        let prevouts_vec = vec![self.txout.clone()];
91        let prevouts = Prevouts::All(&prevouts_vec);
92        let sighash = sighash_cache
93            .taproot_signature_hash(0, &prevouts, None, None, TapSighashType::Default)
94            .map_err(|err| {
95                SparkSdkError::from(ValidationError::InvalidInput {
96                    field: format!("Failed to create taproot signature hash: {err}"),
97                })
98            })?;
99
100        let keypair = Keypair::from_secret_key(&secp, &self.key);
101        let tweaked_keypair = keypair.tap_tweak(&secp, None);
102
103        // Build the Schnorr signature with the tweaked key
104        let msg = Message::from_digest_slice(sighash.as_ref())
105            .map_err(|e| SparkSdkError::from(CryptoError::Secp256k1(e)))?;
106        let schnorr_sig = secp.sign_schnorr_no_aux_rand(&msg, &tweaked_keypair.into());
107
108        tx.input[0].witness.push(schnorr_sig.as_ref());
109        Ok(tx)
110    }
111}
112
113#[allow(dead_code)]
114pub struct BitcoinFaucet {
115    client: Rc<BitcoindClient>,
116    coins: Vec<FaucetCoin>,
117}
118
119#[allow(dead_code)]
120impl BitcoinFaucet {
121    pub fn new(client: Rc<BitcoindClient>) -> Self {
122        Self {
123            client,
124            coins: vec![],
125        }
126    }
127
128    pub fn fund(&mut self) -> Result<FaucetCoin, SparkSdkError> {
129        if self.coins.is_empty() {
130            self.refill()?;
131        }
132
133        let coin =
134            self.coins
135                .pop()
136                .ok_or(SparkSdkError::from(NetworkError::FaucetRequestFailed {
137                    status_code: 0, // Using 0 to indicate internal failure rather than HTTP status code
138                }))?;
139
140        Ok(coin)
141    }
142
143    pub fn refill(&mut self) -> Result<(), SparkSdkError> {
144        let secp = Secp256k1::new();
145        let keypair = Keypair::new(&secp, &mut thread_rng());
146
147        let address = p2tr_address_from_public_key(&keypair.public_key(), Network::Regtest);
148
149        let block_hash = self
150            .client
151            .generate_to_address(1, &address)
152            .map_err(|err| {
153                SparkSdkError::from(NetworkError::BitcoinRpc(format!(
154                    "Failed to generate to address: {}",
155                    err
156                )))
157            })
158            .and_then(|block_hashes| {
159                block_hashes
160                    .first()
161                    .ok_or(SparkSdkError::from(NetworkError::BitcoinRpc(
162                        "Failed to generate to address: block hashes was empty".to_string(),
163                    )))
164                    .cloned()
165            })?;
166
167        let block = self.client.get_block_info(&block_hash).map_err(|err| {
168            SparkSdkError::from(NetworkError::BitcoinRpc(format!(
169                "Failed to get block info for {block_hash}: {err}"
170            )))
171        })?;
172
173        // We don't have all of the transaction data because bitcoin_rpc doesn't have a method for
174        // calling `getblock` with verbosity 2, so we have to fetch the transaction separately.
175        let coin_txid = block
176            .tx
177            .first()
178            .ok_or(SparkSdkError::from(NetworkError::BitcoinRpc(format!(
179                "Failed to get coin txid from block {block_hash}"
180            ))))?;
181
182        let coin_tx = self
183            .client
184            .get_raw_transaction_info(coin_txid, Some(&block_hash))
185            .map_err(|err| {
186                SparkSdkError::from(NetworkError::BitcoinRpc(format!(
187                    "Failed to get raw transaction info for {coin_txid}: {err}"
188                )))
189            })?;
190
191        // Mine 100 blocks to allow the funds to be spendable
192        self.client.mine_blocks(100)?;
193
194        let coin = FaucetCoin {
195            key: keypair.secret_key(),
196            outpoint: OutPoint {
197                txid: *coin_txid,
198                vout: 0,
199            },
200            txout: TxOut {
201                value: coin_tx.vout[0].value,
202                script_pubkey: coin_tx.vout[0].script_pub_key.script().unwrap(),
203            },
204        };
205
206        // Split the coin into 0.1 BTC outputs
207        let mut remaining_value = coin_tx.vout[0].value;
208        let split_amount = Amount::from_sat(10_000_000);
209        let fee_reserve = Amount::from_sat(100_000);
210
211        let mut coin_keys: Vec<Keypair> = vec![];
212        let mut outputs: Vec<TxOut> = vec![];
213
214        while remaining_value > split_amount + fee_reserve {
215            let coin_key = Keypair::new(&secp, &mut thread_rng());
216            let coin_script = p2tr_script_from_pubkey(&coin_key.public_key(), Network::Regtest);
217
218            coin_keys.push(coin_key);
219            outputs.push(TxOut {
220                value: split_amount,
221                script_pubkey: coin_script,
222            });
223
224            remaining_value -= split_amount;
225        }
226
227        let split_tx = coin.tx_to_outputs(outputs)?;
228        let split_txid = self.client.send_raw_transaction(&split_tx).map_err(|err| {
229            SparkSdkError::from(NetworkError::BitcoinRpc(format!(
230                "Failed to send split transaction: {err}"
231            )))
232        })?;
233
234        self.coins.extend(
235            coin_keys
236                .iter()
237                .enumerate()
238                .map(|(i, coin_key)| FaucetCoin {
239                    key: coin_key.secret_key(),
240                    outpoint: OutPoint {
241                        txid: split_txid,
242                        vout: i as u32,
243                    },
244                    txout: split_tx.output[i].clone(),
245                }),
246        );
247
248        Ok(())
249    }
250}
251
252pub struct MempoolFaucet {
253    client: MempoolClient,
254}
255
256impl MempoolFaucet {
257    pub fn new() -> Result<Self, SparkSdkError> {
258        Ok(Self {
259            client: MempoolClient::new()?,
260        })
261    }
262}
263
264#[derive(Deserialize)]
265struct MempoolFaucetResponse {
266    txid: String,
267}
268
269impl MempoolFaucet {
270    pub async fn make_faucet_request(
271        &self,
272        address: &Address,
273        amount: u64,
274    ) -> Result<String, SparkSdkError> {
275        // amount must be less than 100,000 sats
276        if amount > 100_000 {
277            return Err(SparkSdkError::from(ValidationError::InvalidInput {
278                field: "Amount must be less than 100,000".to_string(),
279            }));
280        }
281
282        let res = self
283            .client
284            .request_json::<MempoolFaucetResponse>(
285                SparkNetwork::Regtest,
286                &format!("v1/faucet/{}/{}", address, amount),
287            )
288            .await?;
289
290        Ok(res.txid)
291    }
292}