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            self.0
67                .full_scan(request, stop_gap as usize, parallel_requests as usize)?;
68
69        let update = BdkUpdate {
70            last_active_indices: result.last_active_indices,
71            tx_update: result.tx_update,
72            chain: result.chain_update,
73        };
74
75        Ok(Arc::new(Update(update)))
76    }
77
78    /// Sync a set of scripts, txids, and/or outpoints against Esplora.
79    ///
80    /// `request` provides the data required to perform a script-pubkey-based sync (see
81    /// [`SyncRequest`]). `parallel_requests` specifies the maximum number of HTTP requests to make
82    /// in parallel.
83    pub fn sync(
84        &self,
85        request: Arc<SyncRequest>,
86        parallel_requests: u64,
87    ) -> Result<Arc<Update>, EsploraError> {
88        // using option and take is not ideal but the only way to take full ownership of the request
89        let request: BdkSyncRequest<(KeychainKind, u32)> = request
90            .0
91            .lock()
92            .unwrap()
93            .take()
94            .ok_or(EsploraError::RequestAlreadyConsumed)?;
95
96        let result: BdkSyncResponse = self.0.sync(request, parallel_requests as usize)?;
97
98        let update = BdkUpdate {
99            last_active_indices: BTreeMap::default(),
100            tx_update: result.tx_update,
101            chain: result.chain_update,
102        };
103
104        Ok(Arc::new(Update(update)))
105    }
106
107    /// Broadcast a [`Transaction`] to Esplora.
108    pub fn broadcast(&self, transaction: &Transaction) -> Result<(), EsploraError> {
109        let bdk_transaction: BdkTransaction = transaction.into();
110        self.0
111            .broadcast(&bdk_transaction)
112            .map_err(EsploraError::from)
113    }
114
115    /// Get a [`Transaction`] option given its [`Txid`].
116    pub fn get_tx(&self, txid: Arc<Txid>) -> Result<Option<Arc<Transaction>>, EsploraError> {
117        let tx_opt = self.0.get_tx(&txid.0)?;
118        Ok(tx_opt.map(|inner| Arc::new(Transaction::from(inner))))
119    }
120
121    /// Get a `Transaction` given its `Txid`.
122    pub fn get_tx_no_opt(&self, txid: Arc<Txid>) -> Result<Arc<Transaction>, EsploraError> {
123        self.0
124            .get_tx_no_opt(&txid.0)
125            .map(Transaction::from)
126            .map(Arc::new)
127            .map_err(EsploraError::from)
128    }
129
130    /// Get the height of the current blockchain tip.
131    pub fn get_height(&self) -> Result<u32, EsploraError> {
132        self.0.get_height().map_err(EsploraError::from)
133    }
134
135    /// Get the `BlockHash` of the current blockchain tip.
136    pub fn get_tip_hash(&self) -> Result<Arc<BlockHash>, EsploraError> {
137        self.0
138            .get_tip_hash()
139            .map(|hash| Arc::new(BlockHash(hash)))
140            .map_err(EsploraError::from)
141    }
142
143    /// Get a map where the key is the confirmation target (in number of
144    /// blocks) and the value is the estimated feerate (in sat/vB).
145    pub fn get_fee_estimates(&self) -> Result<HashMap<u16, f64>, EsploraError> {
146        self.0.get_fee_estimates().map_err(EsploraError::from)
147    }
148
149    /// Get the [`BlockHash`] of a specific block height.
150    pub fn get_block_hash(&self, block_height: u32) -> Result<Arc<BlockHash>, EsploraError> {
151        self.0
152            .get_block_hash(block_height)
153            .map(|hash| Arc::new(BlockHash(hash)))
154            .map_err(EsploraError::from)
155    }
156
157    /// Get a Block given a particular BlockHash.
158    pub fn get_block_by_hash(
159        &self,
160        block_hash: Arc<BlockHash>,
161    ) -> Result<Option<Block>, EsploraError> {
162        self.0
163            .get_block_by_hash(&block_hash.0)
164            .map(|block| block.map(|block| block.into()))
165            .map_err(EsploraError::from)
166    }
167
168    /// Get a `Txid` of a transaction given its index in a block with a given hash.
169    pub fn get_txid_at_block_index(
170        &self,
171        block_hash: Arc<BlockHash>,
172        index: u64,
173    ) -> Result<Option<Arc<Txid>>, EsploraError> {
174        self.0
175            .get_txid_at_block_index(&block_hash.0, index as usize)
176            .map(|txid| txid.map(Txid).map(Arc::new))
177            .map_err(EsploraError::from)
178    }
179
180    /// Get a `Header` given a particular block hash.
181    pub fn get_header_by_hash(&self, block_hash: Arc<BlockHash>) -> Result<Header, EsploraError> {
182        self.0
183            .get_header_by_hash(&block_hash.0)
184            .map(Header::from)
185            .map_err(EsploraError::from)
186    }
187
188    /// Get the status of a [`Transaction`] given its [`Txid`].
189    pub fn get_tx_status(&self, txid: Arc<Txid>) -> Result<TxStatus, EsploraError> {
190        self.0
191            .get_tx_status(&txid.0)
192            .map(TxStatus::from)
193            .map_err(EsploraError::from)
194    }
195
196    /// Get transaction info given its [`Txid`].
197    pub fn get_tx_info(&self, txid: Arc<Txid>) -> Result<Option<Tx>, EsploraError> {
198        self.0
199            .get_tx_info(&txid.0)
200            .map(|tx| tx.map(Tx::from))
201            .map_err(EsploraError::from)
202    }
203
204    /// Get transaction history for the specified address, sorted with newest first.
205    ///
206    /// Returns up to 50 mempool transactions plus the first 25 confirmed transactions.
207    /// More can be requested by specifying the last txid seen by the previous query.
208    pub fn get_address_txs(
209        &self,
210        address: Arc<Address>,
211        last_seen: Option<Arc<Txid>>,
212    ) -> Result<Vec<Tx>, EsploraError> {
213        let last_seen = last_seen.as_ref().map(|txid| txid.0);
214        let txs = self.0.get_address_txs(&address.as_ref().0, last_seen)?;
215
216        Ok(txs.into_iter().map(Tx::from).collect())
217    }
218
219    /// Get a merkle inclusion proof for a [`Transaction`] with the given
220    /// [`Txid`].
221    pub fn get_merkle_proof(&self, txid: &Txid) -> Result<Option<MerkleProof>, EsploraError> {
222        self.0
223            .get_merkle_proof(&txid.0)
224            .map(|proof| proof.map(MerkleProof::from))
225            .map_err(EsploraError::from)
226    }
227
228    /// Get the spending status of an output given a `Txid` and the output
229    /// index.
230    pub fn get_output_status(
231        &self,
232        txid: Arc<Txid>,
233        vout: u64,
234    ) -> Result<Option<OutputStatus>, EsploraError> {
235        self.0
236            .get_output_status(&txid.0, vout)
237            .map(|status| status.map(OutputStatus::from))
238            .map_err(EsploraError::from)
239    }
240}