lwk/
esplora_client.rs

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