rgbp/resolvers/
esplora.rs

1// Wallet Library for RGB smart contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 LNP/BP Laboratories,
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2025 RGB Consortium, Switzerland.
12// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
13// All rights under the above copyrights are reserved.
14//
15// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
16// in compliance with the License. You may obtain a copy of the License at
17//
18//        http://www.apache.org/licenses/LICENSE-2.0
19//
20// Unless required by applicable law or agreed to in writing, software distributed under the License
21// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
22// or implied. See the License for the specific language governing permissions and limitations under
23// the License.
24
25use 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    /// Creates a new Esplora client with the specified URL.
47    ///
48    /// # Arguments
49    ///
50    /// * `url` - The URL of the Esplora server.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if the client fails to connect to the Esplora server.
55    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    /// Creates a new Esplora client with the specified URL.
65    ///
66    /// # Arguments
67    ///
68    /// * `url` - The URL of the Esplora server.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the client fails to connect to the Esplora server.
73    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}