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