spark_rust/spark_test_utils/
faucet.rs1use 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
21use 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 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 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, }))?;
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 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 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 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 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 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}