smplx_sdk/provider/
esplora.rs1use 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 for point in set {
76 let tx = self.fetch_transaction(&point.txid)?;
77 map.insert(point.txid, tx);
78 }
79
80 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 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}