bdkffi/
electrum.rs

1use crate::bitcoin::{BlockHash, Header, Transaction, Txid};
2use crate::error::ElectrumError;
3use crate::types::KeychainKind;
4use crate::types::Update;
5use crate::types::{FullScanRequest, SyncRequest};
6
7use bdk_electrum::electrum_client::HeaderNotification as BdkHeaderNotification;
8use bdk_electrum::electrum_client::ServerFeaturesRes as BdkServerFeaturesRes;
9use bdk_electrum::BdkElectrumClient as BdkBdkElectrumClient;
10use bdk_wallet::bitcoin::Transaction as BdkTransaction;
11use bdk_wallet::chain::spk_client::FullScanRequest as BdkFullScanRequest;
12use bdk_wallet::chain::spk_client::FullScanResponse as BdkFullScanResponse;
13use bdk_wallet::chain::spk_client::SyncRequest as BdkSyncRequest;
14use bdk_wallet::chain::spk_client::SyncResponse as BdkSyncResponse;
15use bdk_wallet::Update as BdkUpdate;
16
17use bdk_electrum::electrum_client::ElectrumApi;
18use bdk_wallet::bitcoin::hex::{Case, DisplayHex};
19use std::collections::BTreeMap;
20use std::convert::TryFrom;
21use std::sync::Arc;
22use std::time::Duration;
23
24/// Wrapper around an electrum_client::ElectrumApi which includes an internal in-memory transaction
25/// cache to avoid re-fetching already downloaded transactions.
26#[derive(uniffi::Object)]
27pub struct ElectrumClient(BdkBdkElectrumClient<bdk_electrum::electrum_client::Client>);
28
29#[uniffi::export]
30impl ElectrumClient {
31    /// Creates a new bdk client from a electrum_client::ElectrumApi
32    /// Optional: Set the proxy of the builder
33    /// Optional: Set the timeout (in seconds) of the builder
34    /// Optional: Set the retry attempts number of the builder
35    /// Optional: Set whether the server's TLS certificate is validated.
36    #[uniffi::constructor(default(socks5 = None, timeout = None, retry = None, validate_domain = true))]
37    pub fn new(
38        url: String,
39        socks5: Option<String>,
40        timeout: Option<u8>,
41        retry: Option<u8>,
42        validate_domain: bool,
43    ) -> Result<Self, ElectrumError> {
44        let mut config = bdk_electrum::electrum_client::ConfigBuilder::new();
45        config = config.validate_domain(validate_domain);
46        if let Some(timeout) = timeout {
47            config = config.timeout(Some(Duration::from_secs(timeout.into())));
48        }
49        if let Some(retry) = retry {
50            config = config.retry(retry);
51        }
52        if let Some(socks5) = socks5 {
53            config = config.socks5(Some(bdk_electrum::electrum_client::Socks5Config::new(
54                socks5.as_str(),
55            )));
56        }
57        let inner_client =
58            bdk_electrum::electrum_client::Client::from_config(url.as_str(), config.build())?;
59        let client = BdkBdkElectrumClient::new(inner_client);
60        Ok(Self(client))
61    }
62
63    /// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
64    /// returns updates for bdk_chain data structures.
65    ///
66    /// - `request`: struct with data required to perform a spk-based blockchain client
67    ///   full scan, see `FullScanRequest`.
68    /// - `stop_gap`: the full scan for each keychain stops after a gap of script pubkeys with no
69    ///   associated transactions.
70    /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
71    ///   request.
72    /// - `fetch_prev_txouts`: specifies whether we want previous `TxOuts` for fee calculation. Note
73    ///   that this requires additional calls to the Electrum server, but is necessary for
74    ///   calculating the fee on a transaction if your wallet does not own the inputs. Methods like
75    ///   `Wallet.calculate_fee` and `Wallet.calculate_fee_rate` will return a
76    ///   `CalculateFeeError::MissingTxOut` error if those TxOuts are not present in the transaction
77    ///   graph.
78    pub fn full_scan(
79        &self,
80        request: Arc<FullScanRequest>,
81        stop_gap: u64,
82        batch_size: u64,
83        fetch_prev_txouts: bool,
84    ) -> Result<Arc<Update>, ElectrumError> {
85        // using option and take is not ideal but the only way to take full ownership of the request
86        let request: BdkFullScanRequest<KeychainKind> = request
87            .0
88            .lock()
89            .unwrap()
90            .take()
91            .ok_or(ElectrumError::RequestAlreadyConsumed)?;
92
93        let full_scan_result: BdkFullScanResponse<KeychainKind> = self.0.full_scan(
94            request,
95            stop_gap as usize,
96            batch_size as usize,
97            fetch_prev_txouts,
98        )?;
99
100        let update = BdkUpdate {
101            last_active_indices: full_scan_result.last_active_indices,
102            tx_update: full_scan_result.tx_update,
103            chain: full_scan_result.chain_update,
104        };
105
106        Ok(Arc::new(Update(update)))
107    }
108
109    /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified and returns updates for bdk_chain data structures.
110    ///
111    /// - `request`: struct with data required to perform a spk-based blockchain client
112    ///   sync, see `SyncRequest`.
113    /// - `batch_size`: specifies the max number of script pubkeys to request for in a single batch
114    ///   request.
115    /// - `fetch_prev_txouts`: specifies whether we want previous `TxOuts` for fee calculation. Note
116    ///   that this requires additional calls to the Electrum server, but is necessary for
117    ///   calculating the fee on a transaction if your wallet does not own the inputs. Methods like
118    ///   `Wallet.calculate_fee` and `Wallet.calculate_fee_rate` will return a
119    ///   `CalculateFeeError::MissingTxOut` error if those TxOuts are not present in the transaction
120    ///   graph.
121    ///
122    /// If the scripts to sync are unknown, such as when restoring or importing a keychain that may
123    /// include scripts that have been used, use full_scan with the keychain.
124    pub fn sync(
125        &self,
126        request: Arc<SyncRequest>,
127        batch_size: u64,
128        fetch_prev_txouts: bool,
129    ) -> Result<Arc<Update>, ElectrumError> {
130        // using option and take is not ideal but the only way to take full ownership of the request
131        let request: BdkSyncRequest<(KeychainKind, u32)> = request
132            .0
133            .lock()
134            .unwrap()
135            .take()
136            .ok_or(ElectrumError::RequestAlreadyConsumed)?;
137
138        let sync_result: BdkSyncResponse =
139            self.0
140                .sync(request, batch_size as usize, fetch_prev_txouts)?;
141
142        let update = BdkUpdate {
143            last_active_indices: BTreeMap::default(),
144            tx_update: sync_result.tx_update,
145            chain: sync_result.chain_update,
146        };
147
148        Ok(Arc::new(Update(update)))
149    }
150
151    /// Broadcasts a transaction to the network.
152    pub fn transaction_broadcast(&self, tx: &Transaction) -> Result<Arc<Txid>, ElectrumError> {
153        let bdk_transaction: BdkTransaction = tx.into();
154        self.0
155            .transaction_broadcast(&bdk_transaction)
156            .map_err(ElectrumError::from)
157            .map(|txid| Arc::new(Txid(txid)))
158    }
159
160    /// Fetch transaction of given `Txid`.
161    ///
162    /// If it hits the cache it will return the cached version and avoid making the request.
163    pub fn fetch_tx(&self, txid: Arc<Txid>) -> Result<Arc<Transaction>, ElectrumError> {
164        let tx = self.0.fetch_tx(txid.0).map_err(ElectrumError::from)?;
165        Ok(Arc::new(Transaction::from(tx.as_ref().clone())))
166    }
167
168    /// Returns the capabilities of the server.
169    pub fn server_features(&self) -> Result<ServerFeaturesRes, ElectrumError> {
170        let res = self
171            .0
172            .inner
173            .server_features()
174            .map_err(ElectrumError::from)?;
175
176        ServerFeaturesRes::try_from(res)
177    }
178
179    /// Estimates the fee required in bitcoin per kilobyte to confirm a transaction in `number` blocks.
180    pub fn estimate_fee(&self, number: u64) -> Result<f64, ElectrumError> {
181        self.0
182            .inner
183            .estimate_fee(number as usize, None)
184            .map_err(ElectrumError::from)
185    }
186
187    /// Gets the block header for height `height`.
188    pub fn block_header(&self, height: u64) -> Result<Header, ElectrumError> {
189        self.0
190            .inner
191            .block_header(height as usize)
192            .map_err(ElectrumError::from)
193            .map(Header::from)
194    }
195
196    /// Subscribes to notifications for new block headers, by sending a blockchain.headers.subscribe call.
197    pub fn block_headers_subscribe(&self) -> Result<HeaderNotification, ElectrumError> {
198        self.0
199            .inner
200            .block_headers_subscribe()
201            .map_err(ElectrumError::from)
202            .map(HeaderNotification::from)
203    }
204
205    /// Tries to pop one queued notification for a new block header that we might have received.
206    /// Returns `None` if there are no items in the queue.
207    pub fn block_headers_pop(&self) -> Result<Option<HeaderNotification>, ElectrumError> {
208        self.0
209            .inner
210            .block_headers_pop()
211            .map_err(ElectrumError::from)
212            .map(|notification| notification.map(HeaderNotification::from))
213    }
214
215    /// Pings the server.
216    pub fn ping(&self) -> Result<(), ElectrumError> {
217        self.0.inner.ping().map_err(ElectrumError::from)
218    }
219
220    /// Returns the minimum accepted fee by the server’s node in Bitcoin, not Satoshi.
221    pub fn relay_fee(&self) -> Result<f64, ElectrumError> {
222        self.0.inner.relay_fee().map_err(ElectrumError::from)
223    }
224
225    /// Gets the raw bytes of a transaction with txid. Returns an error if not found.
226    pub fn transaction_get_raw(&self, txid: Arc<Txid>) -> Result<Vec<u8>, ElectrumError> {
227        self.0
228            .inner
229            .transaction_get_raw(&txid.0)
230            .map_err(ElectrumError::from)
231    }
232}
233
234/// Response to an ElectrumClient.server_features request.
235#[derive(uniffi::Record)]
236pub struct ServerFeaturesRes {
237    /// Server version reported.
238    pub server_version: String,
239    /// Hash of the genesis block.
240    pub genesis_hash: Arc<BlockHash>,
241    /// Minimum supported version of the protocol.
242    pub protocol_min: String,
243    /// Maximum supported version of the protocol.
244    pub protocol_max: String,
245    /// Hash function used to create the `ScriptHash`.
246    pub hash_function: Option<String>,
247    /// Pruned height of the server.
248    pub pruning: Option<i64>,
249}
250
251impl TryFrom<BdkServerFeaturesRes> for ServerFeaturesRes {
252    type Error = ElectrumError;
253
254    fn try_from(value: BdkServerFeaturesRes) -> Result<ServerFeaturesRes, ElectrumError> {
255        let hash_str = value.genesis_hash.to_hex_string(Case::Lower);
256        let blockhash = hash_str
257            .parse::<bdk_wallet::bitcoin::BlockHash>()
258            .map_err(|err| ElectrumError::InvalidResponse {
259                error_message: format!(
260                    "invalid genesis hash returned by server: {hash_str} ({err})"
261                ),
262            })?;
263
264        Ok(ServerFeaturesRes {
265            server_version: value.server_version,
266            genesis_hash: Arc::new(BlockHash(blockhash)),
267            protocol_min: value.protocol_min,
268            protocol_max: value.protocol_max,
269            hash_function: value.hash_function,
270            pruning: value.pruning,
271        })
272    }
273}
274
275/// Notification of a new block header.
276#[derive(uniffi::Record)]
277pub struct HeaderNotification {
278    /// New block height.
279    pub height: u64,
280    /// Newly added header.
281    pub header: Header,
282}
283
284impl From<BdkHeaderNotification> for HeaderNotification {
285    fn from(value: BdkHeaderNotification) -> HeaderNotification {
286        HeaderNotification {
287            height: value.height as u64,
288            header: value.header.into(),
289        }
290    }
291}