1use bitcoin::{OutPoint, Txid};
10use bitcoin_hashes::Hash as BitcoinHash;
11use reqwest::blocking::Client;
12use std::thread;
13use std::time::{Duration, Instant};
14
15use crate::proofs::extract_merkle_proof_from_block;
16use crate::rpc::BitcoinRpc;
17use crate::types::BitcoinInclusionProof;
18
19pub const MEMPOOL_SIGNET_BASE: &str = "https://mempool.space/signet/api";
21
22const MAX_RETRIES: u32 = 3;
24const INITIAL_BACKOFF: Duration = Duration::from_secs(2);
26
27pub struct MempoolSignetRpc {
29 client: Client,
30 base_url: String,
31}
32
33impl MempoolSignetRpc {
34 pub fn new() -> Self {
36 Self::with_url(MEMPOOL_SIGNET_BASE.to_string())
37 }
38
39 pub fn with_url(base_url: String) -> Self {
41 let client = Client::builder()
42 .timeout(Duration::from_secs(30))
43 .build()
44 .expect("Failed to create HTTP client");
45 Self { client, base_url }
46 }
47
48 fn get_with_retry<T: serde::de::DeserializeOwned>(
50 &self,
51 url: &str,
52 ) -> Result<T, Box<dyn std::error::Error + Send + Sync>> {
53 let mut last_err = None;
54 let mut backoff = INITIAL_BACKOFF;
55
56 for attempt in 0..=MAX_RETRIES {
57 if attempt > 0 {
58 log::warn!(
59 "Retry {}/{} for {} after {:?} backoff",
60 attempt,
61 MAX_RETRIES,
62 url,
63 backoff
64 );
65 thread::sleep(backoff);
66 backoff *= 2;
67 }
68
69 match self.client.get(url).send() {
70 Ok(resp) if resp.status().is_success() => {
71 return resp.json::<T>().map_err(|e| e.into());
72 }
73 Ok(resp) => {
74 last_err = Some(format!("HTTP {} at {}", resp.status(), url).into());
75 }
76 Err(e) => {
77 last_err = Some(format!("Network error at {}: {}", url, e).into());
78 }
79 }
80 }
81 Err(last_err.unwrap_or_else(|| "Max retries exceeded".into()))
82 }
83
84 fn get_text_with_retry(
86 &self,
87 url: &str,
88 ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
89 let mut last_err = None;
90 let mut backoff = INITIAL_BACKOFF;
91
92 for attempt in 0..=MAX_RETRIES {
93 if attempt > 0 {
94 thread::sleep(backoff);
95 backoff *= 2;
96 }
97
98 match self.client.get(url).send() {
99 Ok(resp) if resp.status().is_success() => {
100 return resp.text().map_err(|e| e.into());
101 }
102 Ok(resp) => {
103 last_err = Some(format!("HTTP {} at {}", resp.status(), url).into());
104 }
105 Err(e) => {
106 last_err = Some(format!("Network error at {}: {}", url, e).into());
107 }
108 }
109 }
110 Err(last_err.unwrap_or_else(|| "Max retries exceeded".into()))
111 }
112
113 fn post_text_with_retry(
115 &self,
116 url: &str,
117 body: String,
118 ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
119 let mut last_err = None;
120 let mut backoff = INITIAL_BACKOFF;
121
122 for attempt in 0..=MAX_RETRIES {
123 if attempt > 0 {
124 thread::sleep(backoff);
125 backoff *= 2;
126 }
127
128 match self
129 .client
130 .post(url)
131 .header("Content-Type", "text/plain")
132 .body(body.clone())
133 .send()
134 {
135 Ok(resp) if resp.status().is_success() => {
136 return resp.text().map_err(|e| e.into());
137 }
138 Ok(resp) => {
139 let status = resp.status();
140 let error_text = resp.text().unwrap_or_default();
141 last_err = Some(format!("HTTP {} at {}: {}", status, url, error_text).into());
142 }
143 Err(e) => {
144 last_err = Some(format!("Network error at {}: {}", url, e).into());
145 }
146 }
147 }
148 Err(last_err.unwrap_or_else(|| "Max retries exceeded".into()))
149 }
150
151 pub fn get_block_info(
153 &self,
154 block_hash: &str,
155 ) -> Result<BlockInfo, Box<dyn std::error::Error + Send + Sync>> {
156 let url = format!("{}/block/{}", self.base_url, block_hash);
157 self.get_with_retry(&url)
158 }
159
160 pub fn get_tx_status(
162 &self,
163 txid: &str,
164 ) -> Result<TxStatus, Box<dyn std::error::Error + Send + Sync>> {
165 let url = format!("{}/tx/{}/status", self.base_url, txid);
166 self.get_with_retry(&url)
167 }
168
169 pub fn get_tx(&self, txid: &str) -> Result<TxDetail, Box<dyn std::error::Error + Send + Sync>> {
171 let url = format!("{}/tx/{}", self.base_url, txid);
172 self.get_with_retry(&url)
173 }
174
175 pub fn get_tx_hex(
177 &self,
178 txid: &str,
179 ) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
180 let url = format!("{}/tx/{}/hex", self.base_url, txid);
181 self.get_text_with_retry(&url)
182 }
183
184 pub fn get_block_txids(
186 &self,
187 block_hash: &str,
188 ) -> Result<Vec<String>, Box<dyn std::error::Error + Send + Sync>> {
189 let url = format!("{}/block/{}/txids", self.base_url, block_hash);
190 self.get_with_retry(&url)
191 }
192
193 pub fn wait_for_confirmation(
195 &self,
196 txid: [u8; 32],
197 required_confirmations: u64,
198 timeout_secs: u64,
199 ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
200 let txid_hex = hex::encode(txid);
201 let start = Instant::now();
202 let poll_interval = Duration::from_secs(10);
203
204 loop {
205 if start.elapsed() > Duration::from_secs(timeout_secs) {
206 return Err("Timeout waiting for confirmation".into());
207 }
208
209 match self.get_tx_status(&txid_hex) {
210 Ok(status) => {
211 if status.confirmed {
212 let tx_height = status.block_height.unwrap_or(0) as u64;
213 let new_height = self.get_block_count()?;
214 let confirmations = new_height.saturating_sub(tx_height) + 1;
215
216 if confirmations >= required_confirmations {
217 return Ok(confirmations);
218 }
219
220 log::info!(
221 "Tx {} has {} confirmations, waiting for {}...",
222 &txid_hex[..16],
223 confirmations,
224 required_confirmations
225 );
226 }
227 }
228 Err(e) => {
229 log::debug!("Tx {} not found yet: {}", &txid_hex[..16], e);
230 }
231 }
232
233 thread::sleep(poll_interval);
234 }
235 }
236
237 pub fn extract_merkle_proof(
239 &self,
240 txid: [u8; 32],
241 block_hash: [u8; 32],
242 ) -> Result<BitcoinInclusionProof, Box<dyn std::error::Error + Send + Sync>> {
243 let block_hash_hex = hex::encode(block_hash);
244
245 let all_txids_hex = self.get_block_txids(&block_hash_hex)?;
246 let all_txids: Vec<[u8; 32]> = all_txids_hex
247 .iter()
248 .map(|t| {
249 let decoded = hex::decode(t)?;
250 let mut arr = [0u8; 32];
251 arr.copy_from_slice(&decoded);
252 Ok(arr)
253 })
254 .collect::<Result<Vec<_>, Box<dyn std::error::Error + Send + Sync>>>()?;
255
256 let block_info = self.get_block_info(&block_hash_hex)?;
257 let block_height = block_info.height;
258
259 extract_merkle_proof_from_block(txid, &all_txids, block_hash, block_height as u64)
260 .ok_or_else(|| "Failed to extract Merkle proof for txid".into())
261 }
262}
263
264impl Default for MempoolSignetRpc {
265 fn default() -> Self {
266 Self::new()
267 }
268}
269
270impl BitcoinRpc for MempoolSignetRpc {
271 fn get_block_count(&self) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
272 let url = format!("{}/blocks/tip/height", self.base_url);
273 self.get_with_retry(&url)
274 }
275
276 fn get_block_hash(
277 &self,
278 height: u64,
279 ) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
280 let url = format!("{}/block-height/{}", self.base_url, height);
281 let hash_hex: String = self.get_text_with_retry(&url)?;
282 let hash_bytes = hex::decode(hash_hex.trim())?;
283 let mut result = [0u8; 32];
284 result.copy_from_slice(&hash_bytes);
285 Ok(result)
286 }
287
288 fn is_utxo_unspent(
289 &self,
290 txid: [u8; 32],
291 vout: u32,
292 ) -> Result<bool, Box<dyn std::error::Error + Send + Sync>> {
293 let txid_hex = hex::encode(txid);
294 let spend_url = format!("{}/tx/{}/outspend/{}", self.base_url, txid_hex, vout);
295 let spend_status: OutSpendStatus = self.get_with_retry(&spend_url)?;
296 Ok(!spend_status.spent)
297 }
298
299 fn send_raw_transaction(
300 &self,
301 tx_bytes: Vec<u8>,
302 ) -> Result<[u8; 32], Box<dyn std::error::Error + Send + Sync>> {
303 let url = format!("{}/tx", self.base_url);
304 let tx_hex = hex::encode(&tx_bytes);
305
306 let txid_hex = self.post_text_with_retry(&url, tx_hex)?;
307 let txid_bytes = hex::decode(txid_hex.trim())?;
308 let mut result = [0u8; 32];
309 result.copy_from_slice(&txid_bytes);
310 Ok(result)
311 }
312
313 fn get_tx_confirmations(
314 &self,
315 txid: [u8; 32],
316 ) -> Result<u64, Box<dyn std::error::Error + Send + Sync>> {
317 let txid_hex = hex::encode(txid);
318
319 match self.get_tx_status(&txid_hex) {
320 Ok(status) => {
321 if status.confirmed {
322 let current_height = self.get_block_count()?;
323 let tx_height = status.block_height.unwrap_or(0) as u64;
324 Ok(current_height.saturating_sub(tx_height) + 1)
325 } else {
326 Ok(0)
327 }
328 }
329 Err(_) => Ok(0),
330 }
331 }
332}
333
334#[derive(Debug, Clone, serde::Deserialize)]
336pub struct BlockInfo {
337 pub id: String,
338 pub height: u32,
339 pub version: u32,
340 pub timestamp: u64,
341 pub tx_count: u32,
342 pub size: u64,
343 pub weight: u64,
344 pub merkle_root: String,
345}
346
347#[derive(Debug, Clone, serde::Deserialize)]
349pub struct TxStatus {
350 pub confirmed: bool,
351 #[serde(default)]
352 pub block_height: Option<u32>,
353 #[serde(default)]
354 pub block_hash: Option<String>,
355 #[serde(default)]
356 pub block_time: Option<u64>,
357}
358
359#[derive(Debug, Clone, serde::Deserialize)]
361pub struct TxDetail {
362 pub txid: String,
363 pub version: u32,
364 pub locktime: u64,
365 pub vin: Vec<TxInput>,
366 pub vout: Vec<TxOutput>,
367 pub size: u64,
368 pub weight: u64,
369 pub fee: u64,
370}
371
372#[derive(Debug, Clone, serde::Deserialize)]
373pub struct TxInput {
374 pub txid: String,
375 pub vout: u32,
376 pub prevout: Option<TxPrevout>,
377 pub scriptsig: String,
378 pub is_coinbase: bool,
379}
380
381#[derive(Debug, Clone, serde::Deserialize)]
382pub struct TxOutput {
383 pub scriptpubkey: String,
384 pub scriptpubkey_asm: String,
385 pub scriptpubkey_type: String,
386 pub scriptpubkey_address: String,
387 pub value: u64,
388}
389
390#[derive(Debug, Clone, serde::Deserialize)]
391pub struct TxPrevout {
392 pub scriptpubkey: String,
393 pub scriptpubkey_asm: String,
394 pub scriptpubkey_type: String,
395 pub scriptpubkey_address: String,
396 pub value: u64,
397}
398
399#[derive(Debug, Clone, serde::Deserialize)]
401pub struct OutSpendStatus {
402 pub spent: bool,
403 #[serde(default)]
404 pub txid: Option<String>,
405 #[serde(default)]
406 pub vin: Option<u32>,
407 #[serde(default)]
408 pub status: Option<TxStatus>,
409}
410
411pub fn get_address_utxos(
413 rpc: &MempoolSignetRpc,
414 address: &bitcoin::Address,
415) -> Result<Vec<(OutPoint, u64)>, Box<dyn std::error::Error + Send + Sync>> {
416 let url = format!("{}/address/{}/utxo", rpc.base_url, address);
417 let utxos: Vec<AddressUtxo> = rpc.get_with_retry(&url)?;
418
419 let result: Vec<(OutPoint, u64)> = utxos
420 .into_iter()
421 .map(|u| {
422 let mut txid_bytes = hex::decode(&u.txid)?;
423 txid_bytes.reverse();
426 let txid = Txid::from_slice(&txid_bytes).expect("valid txid");
427 let outpoint = OutPoint::new(txid, u.vout);
428 Ok((outpoint, u.value))
429 })
430 .collect::<Result<Vec<_>, Box<dyn std::error::Error + Send + Sync>>>()?;
431
432 Ok(result)
433}
434
435#[derive(Debug, Clone, serde::Deserialize)]
437pub struct AddressUtxo {
438 pub txid: String,
439 pub vout: u32,
440 pub value: u64,
441 pub status: TxStatus,
442}
443
444#[cfg(test)]
445mod tests {
446 use super::*;
447
448 #[test]
449 #[ignore = "requires network"]
450 fn test_get_block_count() {
451 let rpc = MempoolSignetRpc::new();
452 let height = rpc.get_block_count().unwrap();
453 assert!(height > 200_000, "Signet height should be > 200k");
454 println!("Current Signet height: {}", height);
455 }
456
457 #[test]
458 #[ignore = "requires network"]
459 fn test_get_block_hash() {
460 let rpc = MempoolSignetRpc::new();
461 let height = rpc.get_block_count().unwrap();
462 let hash = rpc.get_block_hash(height).unwrap();
463 assert_ne!(hash, [0u8; 32]);
464 println!("Block hash at {}: {}", height, hex::encode(hash));
465 }
466}