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