rgbp/resolvers/
mod.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
25#[cfg(feature = "resolver-electrum")]
26mod electrum;
27#[cfg(any(feature = "resolver-esplora", feature = "resolver-esplora-async",))]
28mod esplora;
29// #[cfg(any(feature = "resolver-bitcoinrpc", feature = "resolver-bitcoinrpc-async"))]
30// mod bitcoinrpc;
31
32use core::iter;
33#[cfg(feature = "std")]
34use std::process::exit;
35
36use amplify::IoError;
37use bpstd::psbt::Utxo;
38use bpstd::{ScriptPubkey, Terminal, Tx, Txid, UnsignedTx};
39use rgb::WitnessStatus;
40
41//#[cfg(feature = "resolver-electrum-async")]
42//pub use self::electrum::ElectrumAsyncResolver;
43#[cfg(feature = "resolver-electrum")]
44pub use self::electrum::ElectrumResolver;
45#[cfg(feature = "resolver-esplora-async")]
46pub use self::esplora::EsploraAsyncResolver;
47#[cfg(feature = "resolver-esplora")]
48pub use self::esplora::EsploraResolver;
49
50#[cfg(not(feature = "async"))]
51pub trait Resolver {
52    fn resolve_tx(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError>;
53    fn resolve_tx_status(&self, txid: Txid) -> Result<WitnessStatus, ResolverError>;
54
55    fn resolve_utxos(
56        &self,
57        iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
58    ) -> impl Iterator<Item = Result<Utxo, ResolverError>>;
59
60    fn last_block_height(&self) -> Result<u64, ResolverError>;
61
62    fn broadcast(&self, tx: &Tx) -> Result<(), ResolverError>;
63}
64
65#[cfg(feature = "async")]
66pub trait Resolver {
67    async fn resolve_tx_async(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError>;
68    async fn resolve_tx_status_async(&self, txid: Txid) -> Result<WitnessStatus, ResolverError>;
69
70    async fn resolve_utxos_async(
71        &self,
72        iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
73    ) -> impl Iterator<Item = Result<Utxo, ResolverError>>;
74
75    async fn last_block_height_async(&self) -> Result<u64, ResolverError>;
76
77    async fn broadcast_async(&self, tx: &Tx) -> Result<(), ResolverError>;
78}
79
80#[derive(Default)]
81pub struct MultiResolver {
82    #[cfg(feature = "resolver-electrum")]
83    electrum: Option<ElectrumResolver>,
84    #[cfg(feature = "resolver-esplora")]
85    esplora: Option<EsploraResolver>,
86    // TODO: Implement Bitcoin RPC resolver
87    // #[cfg(feature = "resolver-bitcoinrpc")]
88    // bitcoinrpc: Option<NoResolver>,
89
90    //#[cfg(feature = "resolver-electrum-async")]
91    //electrum: Option<ElectrumAsyncResolver>,
92    #[cfg(feature = "resolver-esplora-async")]
93    esplora: Option<EsploraAsyncResolver>,
94    // #[cfg(feature = "resolver-bitcoinrpc-async")]
95    // bitcoinrpc: Option<NoResolver>,
96}
97
98#[derive(Copy, Clone)]
99pub struct NoResolver;
100
101impl NoResolver {
102    fn call(&self) -> ! {
103        #[cfg(feature = "std")]
104        {
105            eprintln!(
106                "Error: no blockchain indexer specified; use either --esplora or --electrum \
107                 argument"
108            );
109            exit(1);
110        }
111        #[cfg(not(feature = "std"))]
112        panic!(
113            "Error: no blockchain indexer, you need to use one of resolver-* features during the \
114             compilation"
115        );
116    }
117}
118
119#[cfg(not(feature = "async"))]
120impl Resolver for NoResolver {
121    fn resolve_tx(&self, _txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> { self.call() }
122    fn resolve_tx_status(&self, _txid: Txid) -> Result<WitnessStatus, ResolverError> { self.call() }
123    fn resolve_utxos(
124        &self,
125        _iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
126    ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
127        self.call();
128        #[allow(unreachable_code)]
129        iter::empty()
130    }
131    fn last_block_height(&self) -> Result<u64, ResolverError> { self.call() }
132    fn broadcast(&self, _tx: &Tx) -> Result<(), ResolverError> { self.call() }
133}
134
135#[cfg(feature = "async")]
136impl Resolver for NoResolver {
137    async fn resolve_tx_async(&self, _txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> {
138        self.call()
139    }
140    async fn resolve_tx_status_async(&self, _txid: Txid) -> Result<WitnessStatus, ResolverError> {
141        self.call()
142    }
143    async fn resolve_utxos_async(
144        &self,
145        _iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
146    ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
147        self.call();
148        #[allow(unreachable_code)]
149        iter::empty()
150    }
151    async fn last_block_height_async(&self) -> Result<u64, ResolverError> { self.call() }
152    async fn broadcast_async(&self, _tx: &Tx) -> Result<(), ResolverError> { self.call() }
153}
154
155impl MultiResolver {
156    #[cfg(feature = "resolver-electrum")]
157    pub fn new_electrum(url: &str) -> Result<Self, ResolverError> {
158        Ok(Self { electrum: Some(ElectrumResolver::new(url)?), ..default!() })
159    }
160    #[cfg(feature = "resolver-esplora")]
161    pub fn new_esplora(url: &str) -> Result<Self, ResolverError> {
162        Ok(Self { esplora: Some(EsploraResolver::new(url)?), ..default!() })
163    }
164    #[allow(clippy::needless_update)]
165    #[cfg(feature = "resolver-esplora-async")]
166    pub fn new_esplora(url: &str) -> Result<Self, ResolverError> {
167        Ok(Self { esplora: Some(EsploraAsyncResolver::new(url)?), ..default!() })
168    }
169    // #[cfg(feature = "resolver-bitcoinrpc")]
170    // pub fn new_bitcoinrpc(_url: &str) -> Result<Self, ResolverError> { todo!() }
171    pub fn new_absent() -> Result<Self, ResolverError> { Ok(Self::default()) }
172}
173
174#[cfg(not(feature = "async"))]
175impl Resolver for MultiResolver {
176    fn resolve_tx(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> {
177        #[cfg(feature = "resolver-esplora")]
178        if let Some(resolver) = &self.esplora {
179            return resolver.resolve_tx(txid);
180        }
181        #[cfg(feature = "resolver-electrum")]
182        if let Some(resolver) = &self.electrum {
183            return resolver.resolve_tx(txid);
184        }
185        // #[cfg(feature = "resolver-bitcoinrpc")]
186        // if let Some(resolver) = &self.bitcoinrpc {
187        //     return resolver.resolve_tx(txid);
188        // }
189        NoResolver.call()
190    }
191
192    fn resolve_tx_status(&self, txid: Txid) -> Result<WitnessStatus, ResolverError> {
193        #[cfg(feature = "resolver-esplora")]
194        if let Some(resolver) = &self.esplora {
195            return resolver.resolve_tx_status(txid);
196        }
197        #[cfg(feature = "resolver-electrum")]
198        if let Some(resolver) = &self.electrum {
199            return resolver.resolve_tx_status(txid);
200        }
201        // #[cfg(feature = "resolver-bitcoinrpc")]
202        // if let Some(resolver) = &self.bitcoinrpc {
203        //     return resolver.resolve_tx_status(txid);
204        // }
205        NoResolver.call()
206    }
207
208    #[allow(unreachable_code)]
209    fn resolve_utxos(
210        &self,
211        iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
212    ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
213        #[cfg(feature = "resolver-esplora")]
214        if let Some(resolver) = &self.esplora {
215            return resolver.resolve_utxos(iter).collect::<Vec<_>>().into_iter();
216        }
217        #[cfg(feature = "resolver-electrum")]
218        if let Some(resolver) = &self.electrum {
219            return resolver.resolve_utxos(iter).collect::<Vec<_>>().into_iter();
220        }
221        // #[cfg(feature = "resolver-bitcoinrpc")]
222        // if let Some(resolver) = &self.bitcoinrpc {
223        //     return resolver.resolve_utxos(iter).collect::<Vec<_>>().into_iter();
224        // }
225        NoResolver.call();
226        vec![].into_iter()
227    }
228
229    fn last_block_height(&self) -> Result<u64, ResolverError> {
230        #[cfg(feature = "resolver-esplora")]
231        if let Some(resolver) = &self.esplora {
232            return resolver.last_block_height();
233        }
234        #[cfg(feature = "resolver-electrum")]
235        if let Some(resolver) = &self.electrum {
236            return resolver.last_block_height();
237        }
238        // #[cfg(feature = "resolver-bitcoinrpc")]
239        // if let Some(resolver) = &self.bitcoinrpc {
240        //     return resolver.last_block_height();
241        // }
242        NoResolver.call()
243    }
244
245    fn broadcast(&self, tx: &Tx) -> Result<(), ResolverError> {
246        #[cfg(feature = "resolver-esplora")]
247        if let Some(resolver) = &self.esplora {
248            return resolver.broadcast(tx);
249        }
250        #[cfg(feature = "resolver-electrum")]
251        if let Some(resolver) = &self.electrum {
252            return resolver.broadcast(tx);
253        }
254        // #[cfg(feature = "resolver-bitcoinrpc")]
255        // if let Some(resolver) = &self.bitcoinrpc {
256        //     return resolver.broadcast(tx);
257        // }
258        NoResolver.call()
259    }
260}
261
262#[cfg(feature = "async")]
263impl Resolver for MultiResolver {
264    async fn resolve_tx_async(&self, txid: Txid) -> Result<Option<UnsignedTx>, ResolverError> {
265        #[cfg(feature = "resolver-esplora-async")]
266        if let Some(resolver) = &self.esplora {
267            return resolver.resolve_tx_async(txid).await;
268        }
269        // #[cfg(feature = "resolver-electrum-async")]
270        // if let Some(resolver) = &self.electrum {
271        //     return resolver.resolve_tx_async(txid).await;
272        // }
273        // #[cfg(feature = "resolver-bitcoinrpc-async")]
274        // if let Some(resolver) = &self.bitcoinrpc {
275        //     return resolver.resolve_tx_async(txid).await;
276        // }
277        NoResolver.call()
278    }
279
280    async fn resolve_tx_status_async(&self, txid: Txid) -> Result<WitnessStatus, ResolverError> {
281        #[cfg(feature = "resolver-esplora-async")]
282        if let Some(resolver) = &self.esplora {
283            return resolver.resolve_tx_status_async(txid).await;
284        }
285        // #[cfg(feature = "resolver-electrum-async")]
286        // if let Some(resolver) = &self.electrum {
287        //     return resolver.resolve_tx_status_async(txid).await;
288        // }
289        // #[cfg(feature = "resolver-bitcoinrpc-async")]
290        // if let Some(resolver) = &self.bitcoinrpc {
291        //     return resolver.resolve_tx_status_async(txid).await;
292        // }
293        NoResolver.call()
294    }
295
296    #[allow(unreachable_code)]
297    async fn resolve_utxos_async(
298        &self,
299        iter: impl IntoIterator<Item = (Terminal, ScriptPubkey)>,
300    ) -> impl Iterator<Item = Result<Utxo, ResolverError>> {
301        #[cfg(feature = "resolver-esplora-async")]
302        if let Some(resolver) = &self.esplora {
303            return resolver
304                .resolve_utxos_async(iter)
305                .await
306                .collect::<Vec<_>>()
307                .into_iter();
308        }
309        // #[cfg(feature = "resolver-electrum-async")]
310        // if let Some(resolver) = &self.electrum {
311        //     return resolver
312        //         .resolve_utxos_async(iter)
313        //         .await
314        //         .collect::<Vec<_>>()
315        //         .into_iter();
316        // }
317        // #[cfg(feature = "resolver-bitcoinrpc-async")]
318        // if let Some(resolver) = &self.bitcoinrpc {
319        //     return resolver
320        //         .resolve_utxos_async(iter)
321        //         .await
322        //         .collect::<Vec<_>>()
323        //         .into_iter();
324        // }
325        NoResolver.call();
326        vec![].into_iter()
327    }
328
329    async fn last_block_height_async(&self) -> Result<u64, ResolverError> {
330        #[cfg(feature = "resolver-esplora-async")]
331        if let Some(resolver) = &self.esplora {
332            return resolver.last_block_height_async().await;
333        }
334        // #[cfg(feature = "resolver-electrum-async")]
335        // if let Some(resolver) = &self.electrum {
336        //     return resolver.last_block_height_async().await;
337        // }
338        // #[cfg(feature = "resolver-bitcoinrpc-async")]
339        // if let Some(resolver) = &self.bitcoinrpc {
340        //     return resolver.last_block_height_async().await;
341        // }
342        NoResolver.call()
343    }
344
345    async fn broadcast_async(&self, tx: &Tx) -> Result<(), ResolverError> {
346        #[cfg(feature = "resolver-esplora-async")]
347        if let Some(resolver) = &self.esplora {
348            return resolver.broadcast_async(tx).await;
349        }
350        // #[cfg(feature = "resolver-electrum-async")]
351        // if let Some(resolver) = &self.electrum {
352        //     return resolver.broadcast_async(tx).await;
353        // }
354        // #[cfg(feature = "resolver-bitcoinrpc-async")]
355        // if let Some(resolver) = &self.bitcoinrpc {
356        //     return resolver.broadcast_async(tx).await;
357        // }
358        NoResolver.call()
359    }
360}
361
362#[derive(Debug, Display, Error, From)]
363#[display(inner)]
364pub enum ResolverError {
365    Io(IoError),
366
367    /// cannot connect to the indexer server.
368    Connectivity,
369
370    /// internal resolver error on the client side.
371    Local,
372
373    /// indexer uses invalid protocol.
374    Protocol,
375
376    /// invalid caller business logic.
377    Logic,
378
379    /// the indexer server has returned an error "{0}"
380    ServerSide(String),
381}