|
| 1 | +//! Simple one-liner Electrum sync helper |
| 2 | +//! |
| 3 | +//! This provides a clean API for synchronizing wallets with Electrum servers |
| 4 | +//! while preserving cache and respecting BDK's architectural boundaries. |
| 5 | +
|
| 6 | +use bdk_chain::{keychain_txout::KeychainTxOutIndex, tx_graph::TxGraph}; |
| 7 | +use bdk_core::{ |
| 8 | + collections::BTreeMap, |
| 9 | + spk_client::{FullScanRequest, SyncRequest}, |
| 10 | + CheckPoint, |
| 11 | +}; |
| 12 | +use bdk_electrum::BdkElectrumClient; |
| 13 | +use electrum_client::Client; |
| 14 | + |
| 15 | +/// Result type for Electrum synchronization |
| 16 | +pub type ElectrumSyncResult<K> = (Option<CheckPoint>, TxGraph, Option<BTreeMap<K, u32>>); |
| 17 | + |
| 18 | +/// Simple configuration for Electrum synchronization |
| 19 | +#[derive(Debug, Clone, Copy)] |
| 20 | +pub struct SyncOptions { |
| 21 | + pub fast: bool, |
| 22 | + pub stop_gap: usize, |
| 23 | + pub batch_size: usize, |
| 24 | + pub fetch_prev: bool, |
| 25 | +} |
| 26 | + |
| 27 | +impl Default for SyncOptions { |
| 28 | + fn default() -> Self { |
| 29 | + Self { |
| 30 | + fast: false, |
| 31 | + stop_gap: 25, |
| 32 | + batch_size: 30, |
| 33 | + fetch_prev: false, |
| 34 | + } |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +impl SyncOptions { |
| 39 | + /// Create options for fast sync |
| 40 | + pub fn fast_sync() -> Self { |
| 41 | + Self { |
| 42 | + fast: true, |
| 43 | + ..Default::default() |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + /// Create options for full scan |
| 48 | + pub fn full_scan() -> Self { |
| 49 | + Self::default() |
| 50 | + } |
| 51 | + |
| 52 | + pub fn with_stop_gap(mut self, stop_gap: usize) -> Self { |
| 53 | + self.stop_gap = stop_gap; |
| 54 | + self |
| 55 | + } |
| 56 | + |
| 57 | + pub fn with_batch_size(mut self, batch_size: usize) -> Self { |
| 58 | + self.batch_size = batch_size; |
| 59 | + self |
| 60 | + } |
| 61 | + |
| 62 | + pub fn with_fetch_prev(mut self, fetch_prev: bool) -> Self { |
| 63 | + self.fetch_prev = fetch_prev; |
| 64 | + self |
| 65 | + } |
| 66 | +} |
| 67 | + |
| 68 | +/// Long-lived Electrum sync manager that preserves cache across operations |
| 69 | +/// |
| 70 | +/// This struct holds a persistent connection to an Electrum server, maintaining |
| 71 | +/// transaction and header caches between sync operations for better performance. |
| 72 | +pub struct ElectrumSyncManager { |
| 73 | + client: BdkElectrumClient<Client>, |
| 74 | +} |
| 75 | + |
| 76 | +impl ElectrumSyncManager { |
| 77 | + /// Create a new sync manager with the given Electrum server URL |
| 78 | + pub fn new(url: &str) -> Result<Self, electrum_client::Error> { |
| 79 | + let client = Client::new(url)?; |
| 80 | + Ok(Self { |
| 81 | + client: BdkElectrumClient::new(client), |
| 82 | + }) |
| 83 | + } |
| 84 | + |
| 85 | + /// One-liner synchronization - the main convenience method |
| 86 | + /// |
| 87 | + /// # Example |
| 88 | + /// ```text |
| 89 | + /// let manager = ElectrumSyncManager::new("tcp://electrum.example.com:50001")?; |
| 90 | + /// let result = manager.sync(&wallet, SyncOptions::full_scan())?; |
| 91 | + /// ``` |
| 92 | + pub fn sync<K>( |
| 93 | + &self, |
| 94 | + wallet: &KeychainTxOutIndex<K>, |
| 95 | + options: SyncOptions, |
| 96 | + ) -> Result<ElectrumSyncResult<K>, electrum_client::Error> |
| 97 | + where |
| 98 | + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, |
| 99 | + { |
| 100 | + if options.fast { |
| 101 | + self.fast_sync(wallet, options.batch_size, options.fetch_prev) |
| 102 | + } else { |
| 103 | + self.full_scan( |
| 104 | + wallet, |
| 105 | + options.stop_gap, |
| 106 | + options.batch_size, |
| 107 | + options.fetch_prev, |
| 108 | + ) |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + /// One-liner fast sync (only checks revealed addresses) |
| 113 | + pub fn fast_sync<K>( |
| 114 | + &self, |
| 115 | + wallet: &KeychainTxOutIndex<K>, |
| 116 | + batch_size: usize, |
| 117 | + fetch_prev: bool, |
| 118 | + ) -> Result<ElectrumSyncResult<K>, electrum_client::Error> |
| 119 | + where |
| 120 | + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, |
| 121 | + { |
| 122 | + let request = Self::build_sync_request(wallet); |
| 123 | + let response = self.client.sync(request, batch_size, fetch_prev)?; |
| 124 | + |
| 125 | + Ok((response.chain_update, response.tx_update.into(), None)) |
| 126 | + } |
| 127 | + |
| 128 | + /// One-liner full scan (scans until stop gap is reached) |
| 129 | + pub fn full_scan<K>( |
| 130 | + &self, |
| 131 | + wallet: &KeychainTxOutIndex<K>, |
| 132 | + stop_gap: usize, |
| 133 | + batch_size: usize, |
| 134 | + fetch_prev: bool, |
| 135 | + ) -> Result<ElectrumSyncResult<K>, electrum_client::Error> |
| 136 | + where |
| 137 | + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, |
| 138 | + { |
| 139 | + let request = Self::build_full_scan_request(wallet); |
| 140 | + let response = self |
| 141 | + .client |
| 142 | + .full_scan(request, stop_gap, batch_size, fetch_prev)?; |
| 143 | + |
| 144 | + Ok(( |
| 145 | + response.chain_update, |
| 146 | + response.tx_update.into(), |
| 147 | + Some(response.last_active_indices), |
| 148 | + )) |
| 149 | + } |
| 150 | + |
| 151 | + /// Get a reference to the underlying client for advanced operations |
| 152 | + pub fn client(&self) -> &BdkElectrumClient<Client> { |
| 153 | + &self.client |
| 154 | + } |
| 155 | + |
| 156 | + /// Build a sync request based on revealed addresses. |
| 157 | + fn build_sync_request<K>(wallet: &KeychainTxOutIndex<K>) -> SyncRequest<(K, u32)> |
| 158 | + where |
| 159 | + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, |
| 160 | + { |
| 161 | + let mut builder = SyncRequest::builder(); |
| 162 | + |
| 163 | + for keychain_id in wallet.keychains().map(|(k, _)| k) { |
| 164 | + for (index, spk) in wallet.revealed_keychain_spks(keychain_id.clone()) { |
| 165 | + builder = builder.spks_with_indexes([((keychain_id.clone(), index), spk)]); |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + builder.build() |
| 170 | + } |
| 171 | + |
| 172 | + /// Build a full scan request using unbounded SPK iterators. |
| 173 | + fn build_full_scan_request<K>(wallet: &KeychainTxOutIndex<K>) -> FullScanRequest<K> |
| 174 | + where |
| 175 | + K: Ord + Clone + Send + Sync + std::fmt::Debug + 'static, |
| 176 | + { |
| 177 | + let mut builder = FullScanRequest::builder(); |
| 178 | + |
| 179 | + // Add unbounded script iterators for each keychain |
| 180 | + for (keychain_id, spk_iter) in wallet.all_unbounded_spk_iters() { |
| 181 | + builder = builder.spks_for_keychain(keychain_id, spk_iter); |
| 182 | + } |
| 183 | + |
| 184 | + builder.build() |
| 185 | + } |
| 186 | +} |
0 commit comments