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::{
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
25use 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 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 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, }))?;
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 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 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 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 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}