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::UTXO;
15
16use super::core::{DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait};
17use super::error::ProviderError;
18
19pub struct EsploraProvider {
20 pub esplora_url: String,
21 pub network: SimplicityNetwork,
22 pub timeout: Duration,
23}
24
25#[derive(Deserialize)]
26#[allow(dead_code)]
27struct TxStatus {
28 confirmed: bool,
29 block_height: Option<u32>,
30}
31
32#[derive(Deserialize)]
33#[allow(dead_code)]
34struct EsploraBlock {
35 id: String,
36 height: u32,
37 timestamp: u64,
38 tx_count: u32,
39}
40
41#[derive(Clone, Deserialize)]
42#[allow(dead_code)]
43struct UtxoStatus {
44 pub confirmed: bool,
45 pub block_height: Option<u64>,
46 pub block_hash: Option<String>,
47 pub block_time: Option<u64>,
48}
49
50#[derive(Clone, Deserialize)]
51#[allow(dead_code)]
52struct EsploraUtxo {
53 pub txid: String,
54 pub vout: u32,
55 pub value: Option<u64>,
56 pub valuecommitment: Option<String>,
57 pub asset: Option<String>,
58 pub assetcommitment: Option<String>,
59 pub status: UtxoStatus,
60}
61
62impl EsploraProvider {
63 pub fn new(url: String, network: SimplicityNetwork) -> Self {
64 Self {
65 esplora_url: url,
66 network,
67 timeout: Duration::from_secs(DEFAULT_ESPLORA_TIMEOUT_SECS),
68 }
69 }
70
71 fn esplora_utxo_to_outpoint(&self, utxo: &EsploraUtxo) -> Result<OutPoint, ProviderError> {
72 let txid = Txid::from_str(&utxo.txid).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?;
73
74 Ok(OutPoint::new(txid, utxo.vout))
75 }
76
77 fn populate_txouts_from_outpoints(&self, outpoints: &[OutPoint]) -> Result<Vec<UTXO>, ProviderError> {
78 let set: HashSet<_> = outpoints.iter().collect();
79 let mut map = HashMap::new();
80
81 for point in set {
83 let tx = self.fetch_transaction(&point.txid)?;
84 map.insert(point.txid, tx);
85 }
86
87 Ok(outpoints
89 .iter()
90 .map(|point| UTXO {
91 outpoint: *point,
92 txout: map.get(&point.txid).unwrap().output[point.vout as usize].clone(),
93 secrets: None,
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<UTXO>, 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<UTXO>, 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}