1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::str::FromStr;
4use std::time::Duration;
5
6use simplicityhl::elements::hashes::{Hash, sha256};
7
8use simplicityhl::elements::encode;
9use simplicityhl::elements::{Address, OutPoint, Script, Transaction, Txid};
10
11use serde::Deserialize;
12
13use crate::provider::SimplicityNetwork;
14use crate::transaction::{TxReceipt, UTXO};
15
16use super::core::{DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait};
17use super::error::ProviderError;
18
19#[derive(Debug)]
21pub struct EsploraProvider {
22 pub esplora_url: String,
24 pub network: SimplicityNetwork,
26 pub timeout: Duration,
28}
29
30#[derive(Deserialize)]
31#[allow(dead_code)]
32struct TxStatus {
33 confirmed: bool,
34 block_height: Option<u32>,
35}
36
37#[derive(Deserialize)]
38#[allow(dead_code)]
39struct EsploraBlock {
40 id: String,
41 height: u32,
42 timestamp: u64,
43 tx_count: u32,
44}
45
46#[derive(Clone, Deserialize)]
47#[allow(dead_code)]
48struct UtxoStatus {
49 pub confirmed: bool,
50 pub block_height: Option<u64>,
51 pub block_hash: Option<String>,
52 pub block_time: Option<u64>,
53}
54
55#[derive(Clone, Deserialize)]
56#[allow(dead_code)]
57struct EsploraUtxo {
58 pub txid: String,
59 pub vout: u32,
60 pub value: Option<u64>,
61 pub valuecommitment: Option<String>,
62 pub asset: Option<String>,
63 pub assetcommitment: Option<String>,
64 pub status: UtxoStatus,
65}
66
67impl EsploraProvider {
68 #[must_use]
70 pub fn new(url: String, network: SimplicityNetwork) -> Self {
71 Self {
72 esplora_url: url,
73 network,
74 timeout: Duration::from_secs(DEFAULT_ESPLORA_TIMEOUT_SECS),
75 }
76 }
77
78 fn esplora_utxo_to_outpoint(utxo: &EsploraUtxo) -> Result<OutPoint, ProviderError> {
79 let txid = Txid::from_str(&utxo.txid).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?;
80
81 Ok(OutPoint::new(txid, utxo.vout))
82 }
83
84 fn populate_txouts_from_outpoints(&self, outpoints: &[OutPoint]) -> Result<Vec<UTXO>, ProviderError> {
85 let set: HashSet<_> = outpoints.iter().collect();
86 let mut map = HashMap::new();
87
88 for point in set {
90 let tx = self.fetch_transaction(&point.txid)?;
91 map.insert(point.txid, tx);
92 }
93
94 Ok(outpoints
96 .iter()
97 .map(|point| UTXO {
98 outpoint: *point,
99 txout: map.get(&point.txid).unwrap().output[point.vout as usize].clone(),
100 secrets: None,
101 })
102 .collect())
103 }
104}
105
106impl ProviderTrait for EsploraProvider {
107 fn get_network(&self) -> &SimplicityNetwork {
108 &self.network
109 }
110
111 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
112 fn broadcast_transaction(&self, tx: &Transaction) -> Result<TxReceipt<'_>, ProviderError> {
113 let tx_hex = encode::serialize_hex(tx);
114 let url = format!("{}/tx", self.esplora_url);
115 let timeout_secs = self.timeout.as_secs();
116
117 let response = minreq::post(&url)
118 .with_timeout(timeout_secs)
119 .with_body(tx_hex)
120 .send()
121 .map_err(|e| ProviderError::Request(e.to_string()))?;
122
123 let status = response.status_code;
124 let body = response.as_str().unwrap_or("").trim().to_owned();
125
126 if !(200..300).contains(&status) {
127 return Err(ProviderError::BroadcastRejected {
128 status: status as u16,
129 url: format!("{}/tx", self.esplora_url),
130 message: body,
131 });
132 }
133
134 Txid::from_str(&body)
135 .map_err(|e| ProviderError::InvalidTxid(e.to_string()))
136 .map(|tx_id| TxReceipt::new(self, tx_id))
137 }
138
139 fn wait(&self, txid: &Txid) -> Result<(), ProviderError> {
140 let url = format!("{}/tx/{}/status", self.esplora_url, txid);
141 let timeout_secs = self.timeout.as_secs();
142
143 let confirmation_poll = match self.network {
144 SimplicityNetwork::ElementsRegtest { .. } => Duration::from_millis(100),
145 _ => Duration::from_secs(10),
146 };
147
148 for _ in 1..10 {
150 let response = minreq::get(&url)
151 .with_timeout(timeout_secs)
152 .send()
153 .map_err(|e| ProviderError::Request(e.to_string()))?;
154
155 if response.status_code != 200 {
156 std::thread::sleep(confirmation_poll);
157 continue;
158 }
159
160 let status: TxStatus = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
161
162 if status.confirmed {
163 return Ok(());
164 }
165
166 std::thread::sleep(confirmation_poll);
167 }
168
169 Err(ProviderError::Confirmation())
170 }
171
172 fn fetch_tip_height(&self) -> Result<u32, ProviderError> {
173 let url = format!("{}/blocks/tip/height", self.esplora_url);
174 let timeout_secs = self.timeout.as_secs();
175
176 let response = minreq::get(&url)
177 .with_timeout(timeout_secs)
178 .send()
179 .map_err(|e| ProviderError::Request(e.to_string()))?;
180
181 if response.status_code != 200 {
182 return Err(ProviderError::Request(format!(
183 "HTTP {}: {}",
184 response.status_code, response.reason_phrase
185 )));
186 }
187
188 let body_str = response
189 .as_str()
190 .map_err(|e| ProviderError::Deserialize(e.to_string()))?;
191
192 let height: u32 = body_str
193 .trim()
194 .parse()
195 .map_err(|e: std::num::ParseIntError| ProviderError::Deserialize(e.to_string()))?;
196
197 Ok(height)
198 }
199
200 fn fetch_tip_timestamp(&self) -> Result<u64, ProviderError> {
201 let timeout_secs = self.timeout.as_secs();
202
203 let hash_url = format!("{}/blocks/tip/hash", self.esplora_url);
204 let hash_response = minreq::get(&hash_url)
205 .with_timeout(timeout_secs)
206 .send()
207 .map_err(|e| ProviderError::Request(e.to_string()))?;
208
209 if hash_response.status_code != 200 {
210 return Err(ProviderError::Request(format!(
211 "HTTP {}: {}",
212 hash_response.status_code, hash_response.reason_phrase
213 )));
214 }
215
216 let tip_hash = hash_response
217 .as_str()
218 .map_err(|e| ProviderError::Deserialize(e.to_string()))?
219 .trim();
220
221 let block_url = format!("{}/block/{}", self.esplora_url, tip_hash);
222 let block_response = minreq::get(&block_url)
223 .with_timeout(timeout_secs)
224 .send()
225 .map_err(|e| ProviderError::Request(e.to_string()))?;
226
227 if block_response.status_code != 200 {
228 return Err(ProviderError::Request(format!(
229 "HTTP {}: {}",
230 block_response.status_code, block_response.reason_phrase
231 )));
232 }
233
234 let block: EsploraBlock = block_response
235 .json()
236 .map_err(|e| ProviderError::Deserialize(e.to_string()))?;
237
238 Ok(block.timestamp)
239 }
240
241 fn fetch_transaction(&self, txid: &Txid) -> Result<Transaction, ProviderError> {
242 let url = format!("{}/tx/{}/raw", self.esplora_url, txid);
243 let timeout_secs = self.timeout.as_secs();
244
245 let response = minreq::get(&url)
246 .with_timeout(timeout_secs)
247 .send()
248 .map_err(|e| ProviderError::Request(e.to_string()))?;
249
250 if response.status_code != 200 {
251 return Err(ProviderError::Request(format!(
252 "HTTP {}: {}",
253 response.status_code, response.reason_phrase
254 )));
255 }
256
257 let bytes = response.as_bytes();
258 let tx: Transaction = encode::deserialize(bytes).map_err(|e| ProviderError::Deserialize(e.to_string()))?;
259
260 Ok(tx)
261 }
262
263 fn fetch_address_utxos(&self, address: &Address) -> Result<Vec<UTXO>, ProviderError> {
264 let url = format!("{}/address/{}/utxo", self.esplora_url, address);
265 let timeout_secs = self.timeout.as_secs();
266
267 let response = minreq::get(&url)
268 .with_timeout(timeout_secs)
269 .send()
270 .map_err(|e| ProviderError::Request(e.to_string()))?;
271
272 if response.status_code != 200 {
273 return Err(ProviderError::Request(format!(
274 "HTTP {}: {}",
275 response.status_code, response.reason_phrase
276 )));
277 }
278
279 let utxos: Vec<EsploraUtxo> = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
280 let outpoints = utxos
281 .iter()
282 .map(Self::esplora_utxo_to_outpoint)
283 .collect::<Result<Vec<OutPoint>, ProviderError>>()?;
284
285 self.populate_txouts_from_outpoints(&outpoints)
286 }
287
288 fn fetch_scripthash_utxos(&self, script: &Script) -> Result<Vec<UTXO>, ProviderError> {
289 let hash = sha256::Hash::hash(script.as_bytes());
290 let hash_bytes = hash.to_byte_array();
291 let scripthash = hex::encode(hash_bytes);
292
293 let url = format!("{}/scripthash/{}/utxo", self.esplora_url, scripthash);
294 let timeout_secs = self.timeout.as_secs();
295
296 let response = minreq::get(&url)
297 .with_timeout(timeout_secs)
298 .send()
299 .map_err(|e| ProviderError::Request(e.to_string()))?;
300
301 if response.status_code != 200 {
302 return Err(ProviderError::Request(format!(
303 "HTTP {}: {}",
304 response.status_code, response.reason_phrase
305 )));
306 }
307
308 let utxos: Vec<EsploraUtxo> = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
309 let outpoints = utxos
310 .iter()
311 .map(Self::esplora_utxo_to_outpoint)
312 .collect::<Result<Vec<OutPoint>, ProviderError>>()?;
313
314 self.populate_txouts_from_outpoints(&outpoints)
315 }
316
317 fn fetch_fee_estimates(&self) -> Result<HashMap<String, f64>, ProviderError> {
318 let url = format!("{}/fee-estimates", self.esplora_url);
319 let timeout_secs = self.timeout.as_secs();
320
321 let response = minreq::get(&url)
322 .with_timeout(timeout_secs)
323 .send()
324 .map_err(|e| ProviderError::Request(e.to_string()))?;
325
326 if response.status_code != 200 {
327 return Err(ProviderError::Request(format!(
328 "HTTP {}: {}",
329 response.status_code, response.reason_phrase
330 )));
331 }
332
333 let estimates: HashMap<String, f64> = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
334
335 Ok(estimates)
336 }
337}