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