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::error::ProviderError;
16use super::provider::{DEFAULT_ESPLORA_TIMEOUT_SECS, ProviderTrait};
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(Clone, Deserialize)]
32#[allow(dead_code)]
33struct UtxoStatus {
34    pub confirmed: bool,
35    pub block_height: Option<u64>,
36    pub block_hash: Option<String>,
37    pub block_time: Option<u64>,
38}
39
40#[derive(Clone, Deserialize)]
41#[allow(dead_code)]
42struct EsploraUtxo {
43    pub txid: String,
44    pub vout: u32,
45    pub value: Option<u64>,
46    pub valuecommitment: Option<String>,
47    pub asset: Option<String>,
48    pub assetcommitment: Option<String>,
49    pub status: UtxoStatus,
50}
51
52impl EsploraProvider {
53    pub fn new(url: String, network: SimplicityNetwork) -> Self {
54        Self {
55            esplora_url: url,
56            network: network,
57            timeout: Duration::from_secs(DEFAULT_ESPLORA_TIMEOUT_SECS),
58        }
59    }
60
61    fn esplora_utxo_to_outpoint(&self, utxo: &EsploraUtxo) -> Result<OutPoint, ProviderError> {
62        let txid = Txid::from_str(&utxo.txid).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?;
63
64        Ok(OutPoint::new(txid, utxo.vout))
65    }
66
67    fn populate_txouts_from_outpoints(
68        &self,
69        outpoints: &Vec<OutPoint>,
70    ) -> Result<Vec<(OutPoint, TxOut)>, ProviderError> {
71        let set: HashSet<_> = outpoints.into_iter().collect();
72        let mut map = HashMap::new();
73
74        // filter unique transactions
75        for point in set {
76            let tx = self.fetch_transaction(&point.txid)?;
77            map.insert(point.txid, tx);
78        }
79
80        // populate TxOuts
81        Ok(outpoints
82            .iter()
83            .map(|point| {
84                (
85                    *point,
86                    map.get(&point.txid).unwrap().output[point.vout as usize].clone(),
87                )
88            })
89            .collect())
90    }
91}
92
93impl ProviderTrait for EsploraProvider {
94    fn get_network(&self) -> &SimplicityNetwork {
95        &self.network
96    }
97
98    fn broadcast_transaction(&self, tx: &Transaction) -> Result<Txid, ProviderError> {
99        let tx_hex = encode::serialize_hex(tx);
100        let url = format!("{}/tx", self.esplora_url);
101        let timeout_secs = self.timeout.as_secs();
102
103        let response = minreq::post(&url)
104            .with_timeout(timeout_secs)
105            .with_body(tx_hex)
106            .send()
107            .map_err(|e| ProviderError::Request(e.to_string()))?;
108
109        let status = response.status_code;
110        let body = response.as_str().unwrap_or("").trim().to_owned();
111
112        if !(200..300).contains(&status) {
113            return Err(ProviderError::BroadcastRejected {
114                status: status as u16,
115                url: format!("{}/tx", self.esplora_url),
116                message: body,
117            });
118        }
119
120        Ok(Txid::from_str(&body).map_err(|e| ProviderError::InvalidTxid(e.to_string()))?)
121    }
122
123    fn wait(&self, txid: &Txid) -> Result<(), ProviderError> {
124        let url = format!("{}/tx/{}/status", self.esplora_url, txid);
125        let timeout_secs = self.timeout.as_secs();
126
127        let confirmation_poll = match self.network.clone() {
128            SimplicityNetwork::ElementsRegtest { .. } => Duration::from_millis(100),
129            _ => Duration::from_secs(10),
130        };
131
132        // polling needs to be > 1 min on mainnet/testnet
133        for _ in 1..10 {
134            let response = minreq::get(&url)
135                .with_timeout(timeout_secs)
136                .send()
137                .map_err(|e| ProviderError::Request(e.to_string()))?;
138
139            if response.status_code != 200 {
140                std::thread::sleep(confirmation_poll);
141                continue;
142            }
143
144            let status: TxStatus = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
145
146            if status.confirmed {
147                return Ok(());
148            }
149
150            std::thread::sleep(confirmation_poll);
151        }
152
153        Err(ProviderError::Confirmation())
154    }
155
156    fn fetch_transaction(&self, txid: &Txid) -> Result<Transaction, ProviderError> {
157        let url = format!("{}/tx/{}/raw", self.esplora_url, txid);
158        let timeout_secs = self.timeout.as_secs();
159
160        let response = minreq::get(&url)
161            .with_timeout(timeout_secs)
162            .send()
163            .map_err(|e| ProviderError::Request(e.to_string()))?;
164
165        if response.status_code != 200 {
166            return Err(ProviderError::Request(format!(
167                "HTTP {}: {}",
168                response.status_code, response.reason_phrase
169            )));
170        }
171
172        let bytes = response.as_bytes();
173        let tx: Transaction = encode::deserialize(bytes).map_err(|e| ProviderError::Deserialize(e.to_string()))?;
174
175        Ok(tx)
176    }
177
178    fn fetch_address_utxos(&self, address: &Address) -> Result<Vec<(OutPoint, TxOut)>, ProviderError> {
179        let url = format!("{}/address/{}/utxo", self.esplora_url, address);
180        let timeout_secs = self.timeout.as_secs();
181
182        let response = minreq::get(&url)
183            .with_timeout(timeout_secs)
184            .send()
185            .map_err(|e| ProviderError::Request(e.to_string()))?;
186
187        if response.status_code != 200 {
188            return Err(ProviderError::Request(format!(
189                "HTTP {}: {}",
190                response.status_code, response.reason_phrase
191            )));
192        }
193
194        let utxos: Vec<EsploraUtxo> = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
195        let outpoints = utxos
196            .iter()
197            .map(|utxo| Ok(self.esplora_utxo_to_outpoint(&utxo)?))
198            .collect::<Result<Vec<OutPoint>, ProviderError>>()?;
199
200        Ok(self.populate_txouts_from_outpoints(&outpoints)?)
201    }
202
203    fn fetch_scripthash_utxos(&self, script: &Script) -> Result<Vec<(OutPoint, TxOut)>, ProviderError> {
204        let hash = sha256::Hash::hash(script.as_bytes());
205        let hash_bytes = hash.to_byte_array();
206        let scripthash = hex::encode(hash_bytes);
207
208        let url = format!("{}/scripthash/{}/utxo", self.esplora_url, scripthash);
209        let timeout_secs = self.timeout.as_secs();
210
211        let response = minreq::get(&url)
212            .with_timeout(timeout_secs)
213            .send()
214            .map_err(|e| ProviderError::Request(e.to_string()))?;
215
216        if response.status_code != 200 {
217            return Err(ProviderError::Request(format!(
218                "HTTP {}: {}",
219                response.status_code, response.reason_phrase
220            )));
221        }
222
223        let utxos: Vec<EsploraUtxo> = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
224        let outpoints = utxos
225            .iter()
226            .map(|utxo| Ok(self.esplora_utxo_to_outpoint(&utxo)?))
227            .collect::<Result<Vec<OutPoint>, ProviderError>>()?;
228
229        Ok(self.populate_txouts_from_outpoints(&outpoints)?)
230    }
231
232    fn fetch_fee_estimates(&self) -> Result<HashMap<String, f64>, ProviderError> {
233        let url = format!("{}/fee-estimates", self.esplora_url);
234        let timeout_secs = self.timeout.as_secs();
235
236        let response = minreq::get(&url)
237            .with_timeout(timeout_secs)
238            .send()
239            .map_err(|e| ProviderError::Request(e.to_string()))?;
240
241        if response.status_code != 200 {
242            return Err(ProviderError::Request(format!(
243                "HTTP {}: {}",
244                response.status_code, response.reason_phrase
245            )));
246        }
247
248        let estimates: HashMap<String, f64> = response.json().map_err(|e| ProviderError::Deserialize(e.to_string()))?;
249
250        Ok(estimates)
251    }
252}