Skip to main content

smplx_sdk/provider/
esplora.rs

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        // filter unique transactions
81        for point in set {
82            let tx = self.fetch_transaction(&point.txid)?;
83            map.insert(point.txid, tx);
84        }
85
86        // populate TxOuts
87        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        // polling needs to be > 1 min on mainnet/testnet
139        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}