lwk/
esplora_client.rs

1use std::sync::{Arc, Mutex};
2
3use lwk_wollet::{
4    asyncr,
5    clients::blocking::{self, BlockchainBackend},
6};
7
8use crate::{BlockHeader, LwkError, Network, Transaction, Txid, Update, Wollet};
9
10/// A blockchain backend implementation based on the
11/// [esplora HTTP API](https://github.com/blockstream/esplora/blob/master/API.md)
12/// But can also use the [waterfalls](https://github.com/RCasatta/waterfalls) endpoint to
13/// speed up the scan if supported by the server.
14#[derive(uniffi::Object, Debug)]
15pub struct EsploraClient {
16    pub(crate) inner: Mutex<blocking::EsploraClient>,
17
18    /// The builder used to create the client, used to create a new client with the same configuration.
19    pub(crate) builder: lwk_wollet::clients::EsploraClientBuilder,
20}
21
22/// A builder for the `EsploraClient`
23#[derive(uniffi::Record)]
24pub struct EsploraClientBuilder {
25    base_url: String,
26    network: Arc<Network>,
27    #[uniffi(default = false)]
28    waterfalls: bool,
29    #[uniffi(default = None)]
30    concurrency: Option<u32>,
31    #[uniffi(default = None)]
32    timeout: Option<u8>,
33    #[uniffi(default = false)]
34    utxo_only: bool,
35}
36
37impl From<EsploraClientBuilder> for lwk_wollet::clients::EsploraClientBuilder {
38    fn from(builder: EsploraClientBuilder) -> Self {
39        let mut result = lwk_wollet::clients::EsploraClientBuilder::new(
40            &builder.base_url,
41            (*builder.network.as_ref()).into(),
42        );
43        if builder.waterfalls {
44            result = result.waterfalls(true);
45        }
46        if let Some(concurrency) = builder.concurrency {
47            result = result.concurrency(concurrency as usize);
48        }
49        if let Some(timeout) = builder.timeout {
50            result = result.timeout(timeout);
51        }
52        if builder.utxo_only {
53            result = result.utxo_only(true);
54        }
55        result
56    }
57}
58
59#[uniffi::export]
60impl EsploraClient {
61    /// Construct an Esplora Client
62    #[uniffi::constructor]
63    pub fn new(url: &str, network: &Network) -> Result<Arc<Self>, LwkError> {
64        let builder = lwk_wollet::clients::EsploraClientBuilder::new(url, network.into());
65        let client = builder.clone().build_blocking()?;
66        Ok(Arc::new(Self {
67            inner: Mutex::new(client),
68            builder,
69        }))
70    }
71
72    /// Construct an Esplora Client using Waterfalls endpoint
73    #[uniffi::constructor]
74    pub fn new_waterfalls(url: &str, network: &Network) -> Result<Arc<Self>, LwkError> {
75        let builder =
76            lwk_wollet::clients::EsploraClientBuilder::new(url, network.into()).waterfalls(true);
77        let client = builder.clone().build_blocking()?;
78        Ok(Arc::new(Self {
79            inner: Mutex::new(client),
80            builder,
81        }))
82    }
83
84    /// Construct an Esplora Client from an `EsploraClientBuilder`
85    #[uniffi::constructor]
86    pub fn from_builder(builder: EsploraClientBuilder) -> Result<Arc<Self>, LwkError> {
87        let builder = lwk_wollet::clients::EsploraClientBuilder::from(builder);
88        let client = builder.clone().build_blocking()?;
89        Ok(Arc::new(Self {
90            inner: Mutex::new(client),
91            builder,
92        }))
93    }
94
95    /// Broadcast a transaction to the network so that a miner can include it in a block.
96    pub fn broadcast(&self, tx: &Transaction) -> Result<Arc<Txid>, LwkError> {
97        Ok(Arc::new(self.inner.lock()?.broadcast(tx.as_ref())?.into()))
98    }
99
100    /// Scan the blockchain for the scripts generated by a watch-only wallet
101    ///
102    /// This method scans both external and internal address chains, stopping after finding
103    /// 20 consecutive unused addresses (the gap limit) as recommended by
104    /// [BIP44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#address-gap-limit).
105    ///
106    /// Returns `Some(Update)` if any changes were found during scanning, or `None` if no changes
107    /// were detected.
108    ///
109    /// To scan beyond the gap limit use `full_scan_to_index()` instead.
110    pub fn full_scan(&self, wollet: &Wollet) -> Result<Option<Arc<Update>>, LwkError> {
111        self.full_scan_to_index(wollet, 0)
112    }
113
114    /// Scan the blockchain for the scripts generated by a watch-only wallet up to a specified derivation index
115    ///
116    /// While `full_scan()` stops after finding 20 consecutive unused addresses (the gap limit),
117    /// this method will scan at least up to the given derivation index. This is useful to prevent
118    /// missing funds in cases where outputs exist beyond the gap limit.
119    ///
120    /// Will scan both external and internal address chains up to the given index for maximum safety,
121    /// even though internal addresses may not need such deep scanning.
122    ///
123    /// If transactions are found beyond the gap limit during this scan, subsequent calls to
124    /// `full_scan()` will automatically scan up to the highest used index, preventing any
125    /// previously-found transactions from being missed.
126    pub fn full_scan_to_index(
127        &self,
128        wollet: &Wollet,
129        index: u32,
130    ) -> Result<Option<Arc<Update>>, LwkError> {
131        let wollet = wollet.inner_wollet()?;
132        let update: Option<lwk_wollet::Update> = self
133            .inner
134            .lock()?
135            .full_scan_to_index(&wollet.state(), index)?;
136        Ok(update.map(Into::into).map(Arc::new))
137    }
138
139    /// See [`BlockchainBackend::tip`]
140    pub fn tip(&self) -> Result<Arc<BlockHeader>, LwkError> {
141        let tip = self.inner.lock()?.tip()?;
142        Ok(Arc::new(tip.into()))
143    }
144}
145
146impl EsploraClient {
147    /// Create a new esplora blocking client with the same connection parameters
148    #[allow(unused)] // TODO remove once lwk_boltz is integrated
149    pub(crate) fn clone_blocking_client(&self) -> Result<blocking::EsploraClient, LwkError> {
150        Ok(self.builder.clone().build_blocking()?)
151    }
152
153    /// Create a new esplora async client with the same connection parameters
154    #[allow(unused)] // TODO remove once lwk_boltz is integrated
155    pub(crate) fn clone_async_client(&self) -> Result<asyncr::EsploraClient, LwkError> {
156        Ok(self.builder.clone().build()?)
157    }
158}