rgbp/resolvers/
esplora.rs1use core::num::NonZeroU64;
26
27use bpstd::psbt::Utxo;
28use bpstd::{Sats, ScriptPubkey, Terminal, Tx, Txid, UnsignedTx, Vout};
29#[cfg(feature = "async")]
30use esplora::AsyncClient as EsploraAsyncClient;
31#[cfg(not(feature = "async"))]
32use esplora::BlockingClient as EsploraClient;
33use esplora::{Error as EsploraError, TxStatus};
34use rgb::{Outpoint, WitnessStatus};
35
36use super::{Resolver, ResolverError};
37
38#[cfg(not(feature = "async"))]
39pub struct EsploraResolver(EsploraClient);
40
41#[cfg(feature = "async")]
42pub struct EsploraAsyncResolver(EsploraAsyncClient);
43
44#[cfg(not(feature = "async"))]
45impl EsploraResolver {
46 pub fn new(url: &str) -> Result<Self, ResolverError> {
56 let inner = esplora::Builder::new(url).build_blocking()?;
57 let client = Self(inner);
58 Ok(client)
59 }
60}
61
62#[cfg(feature = "async")]
63impl EsploraAsyncResolver {
64 pub fn new(url: &str) -> Result<Self, ResolverError> {
74 let inner = esplora::Builder::new(url).build_async()?;
75 let client = Self(inner);
76 Ok(client)
77 }
78}
79
80fn convert_status(status: TxStatus) -> WitnessStatus {
81 if !status.confirmed {
82 return WitnessStatus::Tentative;
83 }
84 if let Some(height) = status.block_height {
85 match NonZeroU64::new(height as u64) {
86 Some(height) => WitnessStatus::Mined(height),
87 None => WitnessStatus::Genesis,
88 }
89 } else {
90 WitnessStatus::Archived
91 }
92}
93
94#[cfg(not(feature = "async"))]
95impl Resolver for EsploraResolver {
96 fn resolve_tx(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> {
97 let tx = self.0.tx(&txid)?;
98 Ok(tx.map(UnsignedTx::with_sigs_removed))
99 }
100
101 fn resolve_tx_status(&self, txid: Txid) -> Result<WitnessStatus, ResolverError> {
102 let status = self.0.tx_status(&txid)?;
103 Ok(convert_status(status))
104 }
105
106 fn resolve_utxos(
107 &self,
108 iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
109 ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
110 iter.into_iter()
111 .flat_map(|(terminal, spk)| match self.0.scripthash_utxo(&spk) {
112 Err(err) => vec![Err(ResolverError::from(err))],
113 Ok(list) => list
114 .into_iter()
115 .map(|utxo| {
116 Ok(Utxo {
117 outpoint: Outpoint::new(
118 utxo.txid,
119 Vout::from_u32(utxo.vout.value as u32),
120 ),
121 value: Sats::from_sats(utxo.value),
122 terminal,
123 })
124 })
125 .collect::<Vec<_>>(),
126 })
127 }
128
129 fn last_block_height(&self) -> Result<u64, ResolverError> { Ok(self.0.height()? as u64) }
130
131 fn broadcast(&self, tx: &Tx) -> Result<(), ResolverError> {
132 self.0.broadcast(tx)?;
133 Ok(())
134 }
135}
136
137#[cfg(feature = "async")]
138impl Resolver for EsploraAsyncResolver {
139 async fn resolve_tx_async(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> {
140 let tx = self.0.tx(&txid).await?;
141 Ok(tx.map(UnsignedTx::with_sigs_removed))
142 }
143
144 async fn resolve_tx_status_async(&self, txid: Txid) -> Result<WitnessStatus, ResolverError> {
145 let status = self.0.tx_status(&txid).await?;
146 Ok(convert_status(status))
147 }
148
149 async fn resolve_utxos_async(
150 &self,
151 iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
152 ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
153 let mut utxos = Vec::new();
154 for (terminal, spk) in iter {
155 match self.0.scripthash_utxo(&spk).await {
156 Err(err) => utxos.push(Err(ResolverError::from(err))),
157 Ok(list) => utxos.extend(list.into_iter().map(|utxo| {
158 Ok(Utxo {
159 outpoint: Outpoint::new(utxo.txid, Vout::from_u32(utxo.vout.value as u32)),
160 value: Sats::from_sats(utxo.value),
161 terminal,
162 })
163 })),
164 }
165 }
166 utxos.into_iter()
167 }
168
169 async fn last_block_height_async(&self) -> Result<u64, ResolverError> {
170 Ok(self.0.height().await? as u64)
171 }
172
173 async fn broadcast_async(&self, tx: &Tx) -> Result<(), ResolverError> {
174 self.0.broadcast(tx).await?;
175 Ok(())
176 }
177}
178
179impl From<EsploraError> for ResolverError {
180 fn from(err: EsploraError) -> Self {
181 match err {
182 #[cfg(feature = "async")]
183 EsploraError::Reqwest(_) => ResolverError::Connectivity,
184
185 #[cfg(not(feature = "async"))]
186 EsploraError::Minreq(_) => ResolverError::Connectivity,
187
188 EsploraError::InvalidHttpHeaderName(_) | EsploraError::InvalidHttpHeaderValue(_) => {
189 ResolverError::Connectivity
190 }
191
192 EsploraError::StatusCode(_)
193 | EsploraError::HttpResponse { .. }
194 | EsploraError::InvalidServerData
195 | EsploraError::Parsing(_)
196 | EsploraError::BitcoinEncoding
197 | EsploraError::Hex(_) => ResolverError::Protocol,
198
199 EsploraError::TransactionNotFound(_) => ResolverError::Logic,
200 }
201 }
202}