bdkffi/
esplora.rs

1use crate::bitcoin::Address;
2use crate::bitcoin::Block;
3use crate::bitcoin::BlockHash;
4use crate::bitcoin::Header;
5use crate::bitcoin::Transaction;
6use crate::bitcoin::Txid;
7use crate::error::EsploraError;
8use crate::types::KeychainKind;
9use crate::types::Tx;
10use crate::types::TxStatus;
11use crate::types::Update;
12use crate::types::{FullScanRequest, MerkleProof, OutputStatus, SyncRequest};
13
14use bdk_esplora::esplora_client::{BlockingClient, Builder};
15use bdk_esplora::EsploraExt;
16use bdk_wallet::bitcoin::Transaction as BdkTransaction;
17use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest;
18use bdk_wallet::chain::spk_client::FullScanResponse as BdkFullScanResponse;
19use bdk_wallet::chain::spk_client::SyncRequest as BdkSyncRequest;
20use bdk_wallet::chain::spk_client::SyncResponse as BdkSyncResponse;
21use bdk_wallet::Update as BdkUpdate;
22
23use std::collections::{BTreeMap, HashMap};
24use std::sync::Arc;
25
26/// Wrapper around an esplora_client::BlockingClient which includes an internal in-memory transaction
27/// cache to avoid re-fetching already downloaded transactions.
28#[derive(uniffi::Object)]
29pub struct EsploraClient(BlockingClient);
30
31#[uniffi::export]
32impl EsploraClient {
33    /// Creates a new bdk client from an esplora_client::BlockingClient.
34    /// Optional: Set the proxy of the builder.
35    #[uniffi::constructor(default(proxy = None))]
36    pub fn new(url: String, proxy: Option<String>) -> Self {
37        let mut builder = Builder::new(url.as_str());
38        if let Some(proxy) = proxy {
39            builder = builder.proxy(proxy.as_str());
40        }
41        Self(builder.build_blocking())
42    }
43
44    /// Scan keychain scripts for transactions against Esplora, returning an update that can be
45    /// applied to the receiving structures.
46    ///
47    /// `request` provides the data required to perform a script-pubkey-based full scan
48    /// (see [`FullScanRequest`]). The full scan for each keychain (`K`) stops after a gap of
49    /// `stop_gap` script pubkeys with no associated transactions. `parallel_requests` specifies
50    /// the maximum number of HTTP requests to make in parallel.
51    pub fn full_scan(
52        &self,
53        request: Arc<FullScanRequest>,
54        stop_gap: u64,
55        parallel_requests: u64,
56    ) -> Result<Arc<Update>, EsploraError> {
57        // using option and take is not ideal but the only way to take full ownership of the request
58        let request: BdkFullScanRequest<KeychainKind> = request
59            .0
60            .lock()
61            .unwrap()
62            .take()
63            .ok_or(EsploraError::RequestAlreadyConsumed)?;
64
65        let result: BdkFullScanResponse<KeychainKind> =
66            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
67                self.0
68                    .full_scan(request, stop_gap as usize, parallel_requests as usize)
69            }))
70            .map_err(|payload| {
71                let error_message = payload
72                    .downcast_ref::<String>()
73                    .map(String::as_str)
74                    .or_else(|| payload.downcast_ref::<&str>().copied())
75                    .unwrap_or("panic in esplora client")
76                    .to_string();
77
78                EsploraError::Parsing { error_message }
79            })??;
80
81        let update = BdkUpdate {
82            last_active_indices: result.last_active_indices,
83            tx_update: result.tx_update,
84            chain: result.chain_update,
85        };
86
87        Ok(Arc::new(Update(update)))
88    }
89
90    /// Sync a set of scripts, txids, and/or outpoints against Esplora.
91    ///
92    /// `request` provides the data required to perform a script-pubkey-based sync (see
93    /// [`SyncRequest`]). `parallel_requests` specifies the maximum number of HTTP requests to make
94    /// in parallel.
95    pub fn sync(
96        &self,
97        request: Arc<SyncRequest>,
98        parallel_requests: u64,
99    ) -> Result<Arc<Update>, EsploraError> {
100        // using option and take is not ideal but the only way to take full ownership of the request
101        let request: BdkSyncRequest<(KeychainKind, u32)> = request
102            .0
103            .lock()
104            .unwrap()
105            .take()
106            .ok_or(EsploraError::RequestAlreadyConsumed)?;
107
108        let result: BdkSyncResponse =
109            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
110                self.0.sync(request, parallel_requests as usize)
111            }))
112            .map_err(|payload| {
113                let error_message = payload
114                    .downcast_ref::<String>()
115                    .map(String::as_str)
116                    .or_else(|| payload.downcast_ref::<&str>().copied())
117                    .unwrap_or("panic in esplora client")
118                    .to_string();
119
120                EsploraError::Parsing { error_message }
121            })??;
122
123        let update = BdkUpdate {
124            last_active_indices: BTreeMap::default(),
125            tx_update: result.tx_update,
126            chain: result.chain_update,
127        };
128
129        Ok(Arc::new(Update(update)))
130    }
131
132    /// Broadcast a [`Transaction`] to Esplora.
133    pub fn broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
134        let bdk_transaction: BdkTransaction = transaction.into();
135        self.0
136            .broadcast(&bdk_transaction)
137            .map_err(EsploraError::from)
138    }
139
140    /// Get a [`Transaction`] option given its [`Txid`].
141    pub fn get_tx(&self, txid: Arc<Txid>) -> Result<Option<Arc<Transaction>>, EsploraError> {
142        let tx_opt = self.0.get_tx(&txid.0)?;
143        Ok(tx_opt.map(|inner| Arc::new(Transaction::from(inner))))
144    }
145
146    /// Get a `Transaction` given its `Txid`.
147    pub fn get_tx_no_opt(&self, txid: Arc<Txid>) -> Result<Arc<Transaction>, EsploraError> {
148        self.0
149            .get_tx_no_opt(&txid.0)
150            .map(Transaction::from)
151            .map(Arc::new)
152            .map_err(EsploraError::from)
153    }
154
155    /// Get the height of the current blockchain tip.
156    pub fn get_height(&self) -> Result<u32, EsploraError> {
157        self.0.get_height().map_err(EsploraError::from)
158    }
159
160    /// Get the `BlockHash` of the current blockchain tip.
161    pub fn get_tip_hash(&self) -> Result<Arc<BlockHash>, EsploraError> {
162        self.0
163            .get_tip_hash()
164            .map(|hash| Arc::new(BlockHash(hash)))
165            .map_err(EsploraError::from)
166    }
167
168    /// Get a map where the key is the confirmation target (in number of
169    /// blocks) and the value is the estimated feerate (in sat/vB).
170    pub fn get_fee_estimates(&self) -> Result<HashMap<u16, f64>, EsploraError> {
171        self.0.get_fee_estimates().map_err(EsploraError::from)
172    }
173
174    /// Get the [`BlockHash`] of a specific block height.
175    pub fn get_block_hash(&self, block_height: u32) -> Result<Arc<BlockHash>, EsploraError> {
176        self.0
177            .get_block_hash(block_height)
178            .map(|hash| Arc::new(BlockHash(hash)))
179            .map_err(EsploraError::from)
180    }
181
182    /// Get a Block given a particular BlockHash.
183    pub fn get_block_by_hash(
184        &self,
185        block_hash: Arc<BlockHash>,
186    ) -> Result<Option<Block>, EsploraError> {
187        self.0
188            .get_block_by_hash(&block_hash.0)
189            .map(|block| block.map(|block| block.into()))
190            .map_err(EsploraError::from)
191    }
192
193    /// Get a `Txid` of a transaction given its index in a block with a given hash.
194    pub fn get_txid_at_block_index(
195        &self,
196        block_hash: Arc<BlockHash>,
197        index: u64,
198    ) -> Result<Option<Arc<Txid>>, EsploraError> {
199        self.0
200            .get_txid_at_block_index(&block_hash.0, index as usize)
201            .map(|txid| txid.map(Txid).map(Arc::new))
202            .map_err(EsploraError::from)
203    }
204
205    /// Get a `Header` given a particular block hash.
206    pub fn get_header_by_hash(&self, block_hash: Arc<BlockHash>) -> Result<Header, EsploraError> {
207        self.0
208            .get_header_by_hash(&block_hash.0)
209            .map(Header::from)
210            .map_err(EsploraError::from)
211    }
212
213    /// Get the status of a [`Transaction`] given its [`Txid`].
214    pub fn get_tx_status(&self, txid: Arc<Txid>) -> Result<TxStatus, EsploraError> {
215        self.0
216            .get_tx_status(&txid.0)
217            .map(TxStatus::from)
218            .map_err(EsploraError::from)
219    }
220
221    /// Get transaction info given its [`Txid`].
222    pub fn get_tx_info(&self, txid: Arc<Txid>) -> Result<Option<Tx>, EsploraError> {
223        self.0
224            .get_tx_info(&txid.0)
225            .map(|tx| tx.map(Tx::from))
226            .map_err(EsploraError::from)
227    }
228
229    /// Get transaction history for the specified address, sorted with newest first.
230    ///
231    /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
232    /// More can be requested by specifying the last txid seen by the previous query.
233    pub fn get_address_txs(
234        &self,
235        address: Arc<Address>,
236        last_seen: Option<Arc<Txid>>,
237    ) -> Result<Vec<Tx>, EsploraError> {
238        let last_seen = last_seen.as_ref().map(|txid| txid.0);
239        let txs = self.0.get_address_txs(&address.as_ref().0, last_seen)?;
240
241        Ok(txs.into_iter().map(Tx::from).collect())
242    }
243
244    /// Get a merkle inclusion proof for a [`Transaction`] with the given
245    /// [`Txid`].
246    pub fn get_merkle_proof(&self, txid: &Txid) -> Result<Option<MerkleProof>, EsploraError> {
247        self.0
248            .get_merkle_proof(&txid.0)
249            .map(|proof| proof.map(MerkleProof::from))
250            .map_err(EsploraError::from)
251    }
252
253    /// Get the spending status of an output given a `Txid` and the output
254    /// index.
255    pub fn get_output_status(
256        &self,
257        txid: Arc<Txid>,
258        vout: u64,
259    ) -> Result<Option<OutputStatus>, EsploraError> {
260        self.0
261            .get_output_status(&txid.0, vout)
262            .map(|status| status.map(OutputStatus::from))
263            .map_err(EsploraError::from)
264    }
265}