diff --git a/Cargo.toml b/Cargo.toml index d505c1a0a..fd09c646f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "crates/testenv", "examples/example_cli", "examples/example_electrum", + "examples/example_electrum_sync", "examples/example_esplora", "examples/example_bitcoind_rpc_polling", ] diff --git a/examples/example_electrum_sync/Cargo.toml b/examples/example_electrum_sync/Cargo.toml new file mode 100644 index 000000000..c579e4f43 --- /dev/null +++ b/examples/example_electrum_sync/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "example_electrum_sync" +version = "0.1.0" +edition = "2021" + +[dependencies] +bdk_core = { path = "../../crates/core" } +bdk_chain = { path = "../../crates/chain" } +bdk_electrum = { path = "../../crates/electrum" } +electrum-client = "0.24.0" +anyhow = "1.0" \ No newline at end of file diff --git a/examples/example_electrum_sync/src/lib.rs b/examples/example_electrum_sync/src/lib.rs new file mode 100644 index 000000000..b34dad6cc --- /dev/null +++ b/examples/example_electrum_sync/src/lib.rs @@ -0,0 +1,186 @@ +//! Simple one-liner Electrum sync helper +//! +//! This provides a clean API for synchronizing wallets with Electrum servers +//! while preserving cache and respecting BDK's architectural boundaries. + +use bdk_chain::{keychain_txout::KeychainTxOutIndex, tx_graph::TxGraph}; +use bdk_core::{ + collections::BTreeMap, + spk_client::{FullScanRequest, SyncRequest}, + CheckPoint, +}; +use bdk_electrum::BdkElectrumClient; +use electrum_client::Client; + +/// Result type for Electrum synchronization +pub type ElectrumSyncResult = (Option, TxGraph, Option>); + +/// Simple configuration for Electrum synchronization +#[derive(Debug, Clone, Copy)] +pub struct SyncOptions { + pub fast: bool, + pub stop_gap: usize, + pub batch_size: usize, + pub fetch_prev: bool, +} + +impl Default for SyncOptions { + fn default() -> Self { + Self { + fast: false, + stop_gap: 25, + batch_size: 30, + fetch_prev: false, + } + } +} + +impl SyncOptions { + /// Create options for fast sync + pub fn fast_sync() -> Self { + Self { + fast: true, + ..Default::default() + } + } + + /// Create options for full scan + pub fn full_scan() -> Self { + Self::default() + } + + pub fn with_stop_gap(mut self, stop_gap: usize) -> Self { + self.stop_gap = stop_gap; + self + } + + pub fn with_batch_size(mut self, batch_size: usize) -> Self { + self.batch_size = batch_size; + self + } + + pub fn with_fetch_prev(mut self, fetch_prev: bool) -> Self { + self.fetch_prev = fetch_prev; + self + } +} + +/// Long-lived Electrum sync manager that preserves cache across operations +/// +/// This struct holds a persistent connection to an Electrum server, maintaining +/// transaction and header caches between sync operations for better performance. +pub struct ElectrumSyncManager { + client: BdkElectrumClient, +} + +impl ElectrumSyncManager { + /// Create a new sync manager with the given Electrum server URL + pub fn new(url: &str) -> Result { + let client = Client::new(url)?; + Ok(Self { + client: BdkElectrumClient::new(client), + }) + } + + /// One-liner synchronization - the main convenience method + /// + /// # Example + /// ```text + /// let manager = ElectrumSyncManager::new("tcp://electrum.example.com:50001")?; + /// let result = manager.sync(&wallet, SyncOptions::full_scan())?; + /// ``` + pub fn sync( + &self, + wallet: &KeychainTxOutIndex, + options: SyncOptions, + ) -> Result, electrum_client::Error> + where + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, + { + if options.fast { + self.fast_sync(wallet, options.batch_size, options.fetch_prev) + } else { + self.full_scan( + wallet, + options.stop_gap, + options.batch_size, + options.fetch_prev, + ) + } + } + + /// One-liner fast sync (only checks revealed addresses) + pub fn fast_sync( + &self, + wallet: &KeychainTxOutIndex, + batch_size: usize, + fetch_prev: bool, + ) -> Result, electrum_client::Error> + where + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, + { + let request = Self::build_sync_request(wallet); + let response = self.client.sync(request, batch_size, fetch_prev)?; + + Ok((response.chain_update, response.tx_update.into(), None)) + } + + /// One-liner full scan (scans until stop gap is reached) + pub fn full_scan( + &self, + wallet: &KeychainTxOutIndex, + stop_gap: usize, + batch_size: usize, + fetch_prev: bool, + ) -> Result, electrum_client::Error> + where + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, + { + let request = Self::build_full_scan_request(wallet); + let response = self + .client + .full_scan(request, stop_gap, batch_size, fetch_prev)?; + + Ok(( + response.chain_update, + response.tx_update.into(), + Some(response.last_active_indices), + )) + } + + /// Get a reference to the underlying client for advanced operations + pub fn client(&self) -> &BdkElectrumClient { + &self.client + } + + /// Build a sync request based on revealed addresses. + fn build_sync_request(wallet: &KeychainTxOutIndex) -> SyncRequest<(K, u32)> + where + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, + { + let mut builder = SyncRequest::builder(); + + for keychain_id in wallet.keychains().map(|(k, _)| k) { + for (index, spk) in wallet.revealed_keychain_spks(keychain_id.clone()) { + builder = builder.spks_with_indexes([((keychain_id.clone(), index), spk)]); + } + } + + builder.build() + } + + /// Build a full scan request using unbounded SPK iterators. + fn build_full_scan_request(wallet: &KeychainTxOutIndex) -> FullScanRequest + where + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, + { + let mut builder = FullScanRequest::builder(); + + // Add unbounded script iterators for each keychain + for (keychain_id, spk_iter) in wallet.all_unbounded_spk_iters() { + builder = builder.spks_for_keychain(keychain_id, spk_iter); + } + + builder.build() + } +} diff --git a/examples/example_electrum_sync/src/main.rs b/examples/example_electrum_sync/src/main.rs new file mode 100644 index 000000000..ef26a7eea --- /dev/null +++ b/examples/example_electrum_sync/src/main.rs @@ -0,0 +1,19 @@ +use bdk_chain::keychain_txout::KeychainTxOutIndex; +use example_electrum_sync::{ElectrumSyncManager, SyncOptions}; + +fn main() -> Result<(), Box> { + println!("Testing ElectrumSyncManager..."); + let wallet: KeychainTxOutIndex<()> = KeychainTxOutIndex::default(); + let manager = ElectrumSyncManager::new("ssl://electrum.blockstream.info:50001")?; + + // Fast sync + let _ = manager.sync(&wallet, SyncOptions::fast_sync())?; + + // Full scan with custom stop gap + let _ = manager.sync(&wallet, SyncOptions::full_scan().with_stop_gap(50))?; + + // Full scan with custom batch size + let _ = manager.sync(&wallet, SyncOptions::full_scan().with_batch_size(100))?; + + Ok(()) +}