]> Untitled Git - bdk/commitdiff
feat(electrum)!: use new sync/full-scan structs for `ElectrumExt`
author志宇 <hello@evanlinjin.me>
Tue, 30 Apr 2024 06:50:21 +0000 (14:50 +0800)
committer志宇 <hello@evanlinjin.me>
Fri, 10 May 2024 06:54:28 +0000 (14:54 +0800)
`ElectrumResultExt` trait is also introduced that adds methods which can
convert the `Anchor` type for the update `TxGraph`.

We also make use of the new `TxCache` fields in
`SyncRequest`/`FullScanRequest`. This way, we can avoid re-fetching full
transactions from Electrum if not needed.

Examples and tests are updated to use the new `ElectrumExt` API.

crates/electrum/Cargo.toml
crates/electrum/src/electrum_ext.rs
crates/electrum/tests/test_electrum.rs
example-crates/example_electrum/src/main.rs
example-crates/wallet_electrum/src/main.rs

index 2f7896f77df1c0ad36ce8345c0166330fb86d186..c3447078185cd49379ea26de3497154be45646cb 100644 (file)
@@ -12,7 +12,7 @@ readme = "README.md"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-bdk_chain = { path = "../chain", version = "0.13.0", default-features = false }
+bdk_chain = { path = "../chain", version = "0.13.0" }
 electrum-client = { version = "0.19" }
 #rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
 
index 7ad2ae270f04b75a729601f4489b12dafe34ee16..af596305205299e21fb98b864b3a52aac56173c3 100644 (file)
@@ -1,28 +1,18 @@
 use bdk_chain::{
-    bitcoin::{OutPoint, ScriptBuf, Txid},
-    collections::{HashMap, HashSet},
+    bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
+    collections::{BTreeMap, HashMap, HashSet},
     local_chain::CheckPoint,
+    spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult, TxCache},
     tx_graph::TxGraph,
-    Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
+    BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
 };
+use core::str::FromStr;
 use electrum_client::{ElectrumApi, Error, HeaderNotification};
-use std::{collections::BTreeMap, fmt::Debug, str::FromStr};
+use std::sync::Arc;
 
 /// We include a chain suffix of a certain length for the purpose of robustness.
 const CHAIN_SUFFIX_LENGTH: u32 = 8;
 
-/// Combination of chain and transactions updates from electrum
-///
-/// We have to update the chain and the txids at the same time since we anchor the txids to
-/// the same chain tip that we check before and after we gather the txids.
-#[derive(Debug)]
-pub struct ElectrumUpdate {
-    /// Chain update
-    pub chain_update: CheckPoint,
-    /// Tracks electrum updates in TxGraph
-    pub graph_update: TxGraph<ConfirmationTimeHeightAnchor>,
-}
-
 /// Trait to extend [`electrum_client::Client`] functionality.
 pub trait ElectrumExt {
     /// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
@@ -35,14 +25,12 @@ pub trait ElectrumExt {
     /// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
     /// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
     /// single batch request.
-    fn full_scan<K: Ord + Clone, A: Anchor>(
+    fn full_scan<K: Ord + Clone>(
         &self,
-        prev_tip: CheckPoint,
-        keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
-        full_txs: Option<&TxGraph<A>>,
+        request: FullScanRequest<K>,
         stop_gap: usize,
         batch_size: usize,
-    ) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
+    ) -> Result<FullScanResult<K, ConfirmationHeightAnchor>, Error>;
 
     /// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
     /// and returns updates for [`bdk_chain`] data structures.
@@ -61,42 +49,33 @@ pub trait ElectrumExt {
     /// may include scripts that have been used, use [`full_scan`] with the keychain.
     ///
     /// [`full_scan`]: ElectrumExt::full_scan
-    fn sync<A: Anchor>(
+    fn sync(
         &self,
-        prev_tip: CheckPoint,
-        misc_spks: impl IntoIterator<Item = ScriptBuf>,
-        full_txs: Option<&TxGraph<A>>,
-        txids: impl IntoIterator<Item = Txid>,
-        outpoints: impl IntoIterator<Item = OutPoint>,
+        request: SyncRequest,
         batch_size: usize,
-    ) -> Result<ElectrumUpdate, Error>;
+    ) -> Result<SyncResult<ConfirmationHeightAnchor>, Error>;
 }
 
 impl<E: ElectrumApi> ElectrumExt for E {
-    fn full_scan<K: Ord + Clone, A: Anchor>(
+    fn full_scan<K: Ord + Clone>(
         &self,
-        prev_tip: CheckPoint,
-        keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
-        full_txs: Option<&TxGraph<A>>,
+        mut request: FullScanRequest<K>,
         stop_gap: usize,
         batch_size: usize,
-    ) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
-        let mut request_spks = keychain_spks
-            .into_iter()
-            .map(|(k, s)| (k, s.into_iter()))
-            .collect::<BTreeMap<K, _>>();
+    ) -> Result<FullScanResult<K, ConfirmationHeightAnchor>, Error> {
+        let mut request_spks = request.spks_by_keychain;
+
+        // We keep track of already-scanned spks just in case a reorg happens and we need to do a
+        // rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
+        // cannot be collected. In addition, we keep track of whether an spk has an active tx
+        // history for determining the `last_active_index`.
+        // * key: (keychain, spk_index) that identifies the spk.
+        // * val: (script_pubkey, has_tx_history).
         let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
 
-        let (electrum_update, keychain_update) = loop {
-            let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
-            let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
-            if let Some(txs) = full_txs {
-                let _ =
-                    tx_graph.apply_update(txs.clone().map_anchors(|a| ConfirmationHeightAnchor {
-                        anchor_block: a.anchor_block(),
-                        confirmation_height: a.confirmation_height_upper_bound(),
-                    }));
-            }
+        let update = loop {
+            let (tip, _) = construct_update_tip(self, request.chain_tip.clone())?;
+            let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
             let cps = tip
                 .iter()
                 .take(10)
@@ -108,7 +87,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
                     scanned_spks.append(&mut populate_with_spks(
                         self,
                         &cps,
-                        &mut tx_graph,
+                        &mut request.tx_cache,
+                        &mut graph_update,
                         &mut scanned_spks
                             .iter()
                             .map(|(i, (spk, _))| (i.clone(), spk.clone())),
@@ -121,7 +101,8 @@ impl<E: ElectrumApi> ElectrumExt for E {
                         populate_with_spks(
                             self,
                             &cps,
-                            &mut tx_graph,
+                            &mut request.tx_cache,
+                            &mut graph_update,
                             keychain_spks,
                             stop_gap,
                             batch_size,
@@ -140,8 +121,6 @@ impl<E: ElectrumApi> ElectrumExt for E {
 
             let chain_update = tip;
 
-            let graph_update = into_confirmation_time_tx_graph(self, &tx_graph)?;
-
             let keychain_update = request_spks
                 .into_keys()
                 .filter_map(|k| {
@@ -153,41 +132,29 @@ impl<E: ElectrumApi> ElectrumExt for E {
                 })
                 .collect::<BTreeMap<_, _>>();
 
-            break (
-                ElectrumUpdate {
-                    chain_update,
-                    graph_update,
-                },
-                keychain_update,
-            );
+            break FullScanResult {
+                graph_update,
+                chain_update,
+                last_active_indices: keychain_update,
+            };
         };
 
-        Ok((electrum_update, keychain_update))
+        Ok(update)
     }
 
-    fn sync<A: Anchor>(
+    fn sync(
         &self,
-        prev_tip: CheckPoint,
-        misc_spks: impl IntoIterator<Item = ScriptBuf>,
-        full_txs: Option<&TxGraph<A>>,
-        txids: impl IntoIterator<Item = Txid>,
-        outpoints: impl IntoIterator<Item = OutPoint>,
+        request: SyncRequest,
         batch_size: usize,
-    ) -> Result<ElectrumUpdate, Error> {
-        let spk_iter = misc_spks
-            .into_iter()
-            .enumerate()
-            .map(|(i, spk)| (i as u32, spk));
-
-        let (mut electrum_update, _) = self.full_scan(
-            prev_tip.clone(),
-            [((), spk_iter)].into(),
-            full_txs,
-            usize::MAX,
-            batch_size,
-        )?;
-
-        let (tip, _) = construct_update_tip(self, prev_tip)?;
+    ) -> Result<SyncResult<ConfirmationHeightAnchor>, Error> {
+        let mut tx_cache = request.tx_cache.clone();
+
+        let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
+            .cache_txs(request.tx_cache)
+            .set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
+        let full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size)?;
+
+        let (tip, _) = construct_update_tip(self, request.chain_tip)?;
         let cps = tip
             .iter()
             .take(10)
@@ -195,16 +162,88 @@ impl<E: ElectrumApi> ElectrumExt for E {
             .collect::<BTreeMap<u32, CheckPoint>>();
 
         let mut tx_graph = TxGraph::<ConfirmationHeightAnchor>::default();
-        populate_with_txids(self, &cps, &mut tx_graph, txids)?;
-        populate_with_outpoints(self, &cps, &mut tx_graph, outpoints)?;
-        let _ = electrum_update
-            .graph_update
-            .apply_update(into_confirmation_time_tx_graph(self, &tx_graph)?);
+        populate_with_txids(self, &cps, &mut tx_cache, &mut tx_graph, request.txids)?;
+        populate_with_outpoints(self, &cps, &mut tx_cache, &mut tx_graph, request.outpoints)?;
 
-        Ok(electrum_update)
+        Ok(SyncResult {
+            chain_update: full_scan_res.chain_update,
+            graph_update: full_scan_res.graph_update,
+        })
     }
 }
 
+/// Trait that extends [`SyncResult`] and [`FullScanResult`] functionality.
+///
+/// Currently, only a single method exists that converts the update [`TxGraph`] to have an anchor
+/// type of [`ConfirmationTimeHeightAnchor`].
+pub trait ElectrumResultExt {
+    /// New result type with a [`TxGraph`] that contains the [`ConfirmationTimeHeightAnchor`].
+    type NewResult;
+
+    /// Convert result type to have an update [`TxGraph`]  that contains the [`ConfirmationTimeHeightAnchor`] .
+    fn try_into_confirmation_time_result(
+        self,
+        client: &impl ElectrumApi,
+    ) -> Result<Self::NewResult, Error>;
+}
+
+impl<K> ElectrumResultExt for FullScanResult<K, ConfirmationHeightAnchor> {
+    type NewResult = FullScanResult<K, ConfirmationTimeHeightAnchor>;
+
+    fn try_into_confirmation_time_result(
+        self,
+        client: &impl ElectrumApi,
+    ) -> Result<Self::NewResult, Error> {
+        Ok(FullScanResult::<K, ConfirmationTimeHeightAnchor> {
+            graph_update: try_into_confirmation_time_result(self.graph_update, client)?,
+            chain_update: self.chain_update,
+            last_active_indices: self.last_active_indices,
+        })
+    }
+}
+
+impl ElectrumResultExt for SyncResult<ConfirmationHeightAnchor> {
+    type NewResult = SyncResult<ConfirmationTimeHeightAnchor>;
+
+    fn try_into_confirmation_time_result(
+        self,
+        client: &impl ElectrumApi,
+    ) -> Result<Self::NewResult, Error> {
+        Ok(SyncResult {
+            graph_update: try_into_confirmation_time_result(self.graph_update, client)?,
+            chain_update: self.chain_update,
+        })
+    }
+}
+
+fn try_into_confirmation_time_result(
+    graph_update: TxGraph<ConfirmationHeightAnchor>,
+    client: &impl ElectrumApi,
+) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
+    let relevant_heights = graph_update
+        .all_anchors()
+        .iter()
+        .map(|(a, _)| a.confirmation_height)
+        .collect::<HashSet<_>>();
+
+    let height_to_time = relevant_heights
+        .clone()
+        .into_iter()
+        .zip(
+            client
+                .batch_block_header(relevant_heights)?
+                .into_iter()
+                .map(|bh| bh.time as u64),
+        )
+        .collect::<HashMap<u32, u64>>();
+
+    Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
+        anchor_block: a.anchor_block,
+        confirmation_height: a.confirmation_height,
+        confirmation_time: height_to_time[&a.confirmation_height],
+    }))
+}
+
 /// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
 fn construct_update_tip(
     client: &impl ElectrumApi,
@@ -323,6 +362,7 @@ fn determine_tx_anchor(
 fn populate_with_outpoints(
     client: &impl ElectrumApi,
     cps: &BTreeMap<u32, CheckPoint>,
+    tx_cache: &mut TxCache,
     tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
     outpoints: impl IntoIterator<Item = OutPoint>,
 ) -> Result<(), Error> {
@@ -358,9 +398,9 @@ fn populate_with_outpoints(
                 let res_tx = match tx_graph.get_tx(res.tx_hash) {
                     Some(tx) => tx,
                     None => {
-                        let res_tx = client.transaction_get(&res.tx_hash)?;
-                        let _ = tx_graph.insert_tx(res_tx);
-                        tx_graph.get_tx(res.tx_hash).expect("just inserted")
+                        let res_tx = fetch_tx(client, tx_cache, res.tx_hash)?;
+                        let _ = tx_graph.insert_tx(Arc::clone(&res_tx));
+                        res_tx
                     }
                 };
                 has_spending = res_tx
@@ -383,11 +423,12 @@ fn populate_with_outpoints(
 fn populate_with_txids(
     client: &impl ElectrumApi,
     cps: &BTreeMap<u32, CheckPoint>,
-    tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
+    tx_cache: &mut TxCache,
+    graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
     txids: impl IntoIterator<Item = Txid>,
 ) -> Result<(), Error> {
     for txid in txids {
-        let tx = match client.transaction_get(&txid) {
+        let tx = match fetch_tx(client, tx_cache, txid) {
             Ok(tx) => tx,
             Err(electrum_client::Error::Protocol(_)) => continue,
             Err(other_err) => return Err(other_err),
@@ -408,20 +449,36 @@ fn populate_with_txids(
             None => continue,
         };
 
-        if tx_graph.get_tx(txid).is_none() {
-            let _ = tx_graph.insert_tx(tx);
+        if graph_update.get_tx(txid).is_none() {
+            // TODO: We need to be able to insert an `Arc` of a transaction.
+            let _ = graph_update.insert_tx(tx);
         }
         if let Some(anchor) = anchor {
-            let _ = tx_graph.insert_anchor(txid, anchor);
+            let _ = graph_update.insert_anchor(txid, anchor);
         }
     }
     Ok(())
 }
 
+fn fetch_tx<C: ElectrumApi>(
+    client: &C,
+    tx_cache: &mut TxCache,
+    txid: Txid,
+) -> Result<Arc<Transaction>, Error> {
+    use bdk_chain::collections::hash_map::Entry;
+    Ok(match tx_cache.entry(txid) {
+        Entry::Occupied(entry) => entry.get().clone(),
+        Entry::Vacant(entry) => entry
+            .insert(Arc::new(client.transaction_get(&txid)?))
+            .clone(),
+    })
+}
+
 fn populate_with_spks<I: Ord + Clone>(
     client: &impl ElectrumApi,
     cps: &BTreeMap<u32, CheckPoint>,
-    tx_graph: &mut TxGraph<ConfirmationHeightAnchor>,
+    tx_cache: &mut TxCache,
+    graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
     spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
     stop_gap: usize,
     batch_size: usize,
@@ -453,51 +510,12 @@ fn populate_with_spks<I: Ord + Clone>(
                 unused_spk_count = 0;
             }
 
-            for tx in spk_history {
-                let mut update = TxGraph::<ConfirmationHeightAnchor>::default();
-
-                if tx_graph.get_tx(tx.tx_hash).is_none() {
-                    let full_tx = client.transaction_get(&tx.tx_hash)?;
-                    update = TxGraph::<ConfirmationHeightAnchor>::new([full_tx]);
+            for tx_res in spk_history {
+                let _ = graph_update.insert_tx(fetch_tx(client, tx_cache, tx_res.tx_hash)?);
+                if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
+                    let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
                 }
-
-                if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
-                    let _ = update.insert_anchor(tx.tx_hash, anchor);
-                }
-
-                let _ = tx_graph.apply_update(update);
             }
         }
     }
 }
-
-fn into_confirmation_time_tx_graph(
-    client: &impl ElectrumApi,
-    tx_graph: &TxGraph<ConfirmationHeightAnchor>,
-) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
-    let relevant_heights = tx_graph
-        .all_anchors()
-        .iter()
-        .map(|(a, _)| a.confirmation_height)
-        .collect::<HashSet<_>>();
-
-    let height_to_time = relevant_heights
-        .clone()
-        .into_iter()
-        .zip(
-            client
-                .batch_block_header(relevant_heights)?
-                .into_iter()
-                .map(|bh| bh.time as u64),
-        )
-        .collect::<HashMap<u32, u64>>();
-
-    let new_graph = tx_graph
-        .clone()
-        .map_anchors(|a| ConfirmationTimeHeightAnchor {
-            anchor_block: a.anchor_block,
-            confirmation_height: a.confirmation_height,
-            confirmation_time: height_to_time[&a.confirmation_height],
-        });
-    Ok(new_graph)
-}
index b48ee34cbd2562df8bf070da6a12d6120fd360b1..aa4b87933cc4e48be6cca1d54fafb392cc250101 100644 (file)
@@ -2,9 +2,10 @@ use bdk_chain::{
     bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
     keychain::Balance,
     local_chain::LocalChain,
+    spk_client::SyncRequest,
     ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
 };
-use bdk_electrum::{ElectrumExt, ElectrumUpdate};
+use bdk_electrum::{ElectrumExt, ElectrumResultExt};
 use bdk_testenv::{anyhow, anyhow::Result, bitcoincore_rpc::RpcApi, TestEnv};
 
 fn get_balance(
@@ -60,22 +61,18 @@ fn scan_detects_confirmed_tx() -> Result<()> {
 
     // Sync up to tip.
     env.wait_until_electrum_sees_block()?;
-    let ElectrumUpdate {
-        chain_update,
-        graph_update,
-    } = client.sync::<ConfirmationTimeHeightAnchor>(
-        recv_chain.tip(),
-        [spk_to_track],
-        Some(recv_graph.graph()),
-        None,
-        None,
-        5,
-    )?;
+    let update = client
+        .sync(
+            SyncRequest::from_chain_tip(recv_chain.tip())
+                .chain_spks(core::iter::once(spk_to_track)),
+            5,
+        )?
+        .try_into_confirmation_time_result(&client)?;
 
     let _ = recv_chain
-        .apply_update(chain_update)
+        .apply_update(update.chain_update)
         .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
-    let _ = recv_graph.apply_update(graph_update);
+    let _ = recv_graph.apply_update(update.graph_update);
 
     // Check to see if tx is confirmed.
     assert_eq!(
@@ -131,25 +128,20 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
 
     // Sync up to tip.
     env.wait_until_electrum_sees_block()?;
-    let ElectrumUpdate {
-        chain_update,
-        graph_update,
-    } = client.sync::<ConfirmationTimeHeightAnchor>(
-        recv_chain.tip(),
-        [spk_to_track.clone()],
-        Some(recv_graph.graph()),
-        None,
-        None,
-        5,
-    )?;
+    let update = client
+        .sync(
+            SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
+            5,
+        )?
+        .try_into_confirmation_time_result(&client)?;
 
     let _ = recv_chain
-        .apply_update(chain_update)
+        .apply_update(update.chain_update)
         .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
-    let _ = recv_graph.apply_update(graph_update.clone());
+    let _ = recv_graph.apply_update(update.graph_update.clone());
 
     // Retain a snapshot of all anchors before reorg process.
-    let initial_anchors = graph_update.all_anchors();
+    let initial_anchors = update.graph_update.all_anchors();
 
     // Check if initial balance is correct.
     assert_eq!(
@@ -166,27 +158,22 @@ fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
         env.reorg_empty_blocks(depth)?;
 
         env.wait_until_electrum_sees_block()?;
-        let ElectrumUpdate {
-            chain_update,
-            graph_update,
-        } = client.sync::<ConfirmationTimeHeightAnchor>(
-            recv_chain.tip(),
-            [spk_to_track.clone()],
-            Some(recv_graph.graph()),
-            None,
-            None,
-            5,
-        )?;
+        let update = client
+            .sync(
+                SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
+                5,
+            )?
+            .try_into_confirmation_time_result(&client)?;
 
         let _ = recv_chain
-            .apply_update(chain_update)
+            .apply_update(update.chain_update)
             .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
 
         // Check to see if a new anchor is added during current reorg.
-        if !initial_anchors.is_superset(graph_update.all_anchors()) {
+        if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
             println!("New anchor added at reorg depth {}", depth);
         }
-        let _ = recv_graph.apply_update(graph_update);
+        let _ = recv_graph.apply_update(update.graph_update);
 
         assert_eq!(
             get_balance(&recv_chain, &recv_graph)?,
index a482e1b31b4a1c45dac2414eb006f089133096b9..d321ded7ea76dd06f112c5f90c5e75b72b340fd0 100644 (file)
@@ -1,19 +1,20 @@
 use std::{
-    collections::BTreeMap,
     io::{self, Write},
     sync::Mutex,
 };
 
 use bdk_chain::{
-    bitcoin::{constants::genesis_block, Address, Network, OutPoint, Txid},
+    bitcoin::{constants::genesis_block, Address, Network, Txid},
+    collections::BTreeSet,
     indexed_tx_graph::{self, IndexedTxGraph},
     keychain,
     local_chain::{self, LocalChain},
+    spk_client::{FullScanRequest, SyncRequest},
     Append, ConfirmationHeightAnchor,
 };
 use bdk_electrum::{
     electrum_client::{self, Client, ElectrumApi},
-    ElectrumExt, ElectrumUpdate,
+    ElectrumExt,
 };
 use example_cli::{
     anyhow::{self, Context},
@@ -147,48 +148,55 @@ fn main() -> anyhow::Result<()> {
 
     let client = electrum_cmd.electrum_args().client(args.network)?;
 
-    let response = match electrum_cmd.clone() {
+    let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() {
         ElectrumCommands::Scan {
             stop_gap,
             scan_options,
             ..
         } => {
-            let (keychain_spks, tip) = {
+            let request = {
                 let graph = &*graph.lock().unwrap();
                 let chain = &*chain.lock().unwrap();
 
-                let keychain_spks = graph
-                    .index
-                    .all_unbounded_spk_iters()
-                    .into_iter()
-                    .map(|(keychain, iter)| {
-                        let mut first = true;
-                        let spk_iter = iter.inspect(move |(i, _)| {
-                            if first {
-                                eprint!("\nscanning {}: ", keychain);
-                                first = false;
+                FullScanRequest::from_chain_tip(chain.tip())
+                    .cache_graph_txs(graph.graph())
+                    .set_spks_for_keychain(
+                        Keychain::External,
+                        graph
+                            .index
+                            .unbounded_spk_iter(&Keychain::External)
+                            .into_iter()
+                            .flatten(),
+                    )
+                    .set_spks_for_keychain(
+                        Keychain::Internal,
+                        graph
+                            .index
+                            .unbounded_spk_iter(&Keychain::Internal)
+                            .into_iter()
+                            .flatten(),
+                    )
+                    .inspect_spks_for_all_keychains({
+                        let mut once = BTreeSet::new();
+                        move |k, spk_i, _| {
+                            if once.insert(k) {
+                                eprint!("\nScanning {}: ", k);
+                            } else {
+                                eprint!("{} ", spk_i);
                             }
-
-                            eprint!("{} ", i);
                             let _ = io::stdout().flush();
-                        });
-                        (keychain, spk_iter)
+                        }
                     })
-                    .collect::<BTreeMap<_, _>>();
-
-                let tip = chain.tip();
-                (keychain_spks, tip)
             };
 
-            client
-                .full_scan::<_, ConfirmationHeightAnchor>(
-                    tip,
-                    keychain_spks,
-                    Some(graph.lock().unwrap().graph()),
-                    stop_gap,
-                    scan_options.batch_size,
-                )
-                .context("scanning the blockchain")?
+            let res = client
+                .full_scan::<_>(request, stop_gap, scan_options.batch_size)
+                .context("scanning the blockchain")?;
+            (
+                res.chain_update,
+                res.graph_update,
+                Some(res.last_active_indices),
+            )
         }
         ElectrumCommands::Sync {
             mut unused_spks,
@@ -201,7 +209,6 @@ fn main() -> anyhow::Result<()> {
             // Get a short lock on the tracker to get the spks we're interested in
             let graph = graph.lock().unwrap();
             let chain = chain.lock().unwrap();
-            let chain_tip = chain.tip().block_id();
 
             if !(all_spks || unused_spks || utxos || unconfirmed) {
                 unused_spks = true;
@@ -211,18 +218,20 @@ fn main() -> anyhow::Result<()> {
                 unused_spks = false;
             }
 
-            let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
-                Box::new(core::iter::empty());
+            let chain_tip = chain.tip();
+            let mut request =
+                SyncRequest::from_chain_tip(chain_tip.clone()).cache_graph_txs(graph.graph());
+
             if all_spks {
                 let all_spks = graph
                     .index
                     .revealed_spks(..)
                     .map(|(k, i, spk)| (k.to_owned(), i, spk.to_owned()))
                     .collect::<Vec<_>>();
-                spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
-                    eprintln!("scanning {}:{}", k, i);
+                request = request.chain_spks(all_spks.into_iter().map(|(k, spk_i, spk)| {
+                    eprintln!("scanning {}: {}", k, spk_i);
                     spk
-                })));
+                }));
             }
             if unused_spks {
                 let unused_spks = graph
@@ -230,82 +239,61 @@ fn main() -> anyhow::Result<()> {
                     .unused_spks()
                     .map(|(k, i, spk)| (k, i, spk.to_owned()))
                     .collect::<Vec<_>>();
-                spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
-                    eprintln!(
-                        "Checking if address {} {}:{} has been used",
-                        Address::from_script(&spk, args.network).unwrap(),
-                        k,
-                        i,
-                    );
-                    spk
-                })));
+                request =
+                    request.chain_spks(unused_spks.into_iter().map(move |(k, spk_i, spk)| {
+                        eprintln!(
+                            "Checking if address {} {}:{} has been used",
+                            Address::from_script(&spk, args.network).unwrap(),
+                            k,
+                            spk_i,
+                        );
+                        spk
+                    }));
             }
 
-            let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
-
             if utxos {
                 let init_outpoints = graph.index.outpoints();
 
                 let utxos = graph
                     .graph()
-                    .filter_chain_unspents(&*chain, chain_tip, init_outpoints)
+                    .filter_chain_unspents(&*chain, chain_tip.block_id(), init_outpoints)
                     .map(|(_, utxo)| utxo)
                     .collect::<Vec<_>>();
-
-                outpoints = Box::new(
-                    utxos
-                        .into_iter()
-                        .inspect(|utxo| {
-                            eprintln!(
-                                "Checking if outpoint {} (value: {}) has been spent",
-                                utxo.outpoint, utxo.txout.value
-                            );
-                        })
-                        .map(|utxo| utxo.outpoint),
-                );
+                request = request.chain_outpoints(utxos.into_iter().map(|utxo| {
+                    eprintln!(
+                        "Checking if outpoint {} (value: {}) has been spent",
+                        utxo.outpoint, utxo.txout.value
+                    );
+                    utxo.outpoint
+                }));
             };
 
-            let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
-
             if unconfirmed {
                 let unconfirmed_txids = graph
                     .graph()
-                    .list_chain_txs(&*chain, chain_tip)
+                    .list_chain_txs(&*chain, chain_tip.block_id())
                     .filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
                     .map(|canonical_tx| canonical_tx.tx_node.txid)
                     .collect::<Vec<Txid>>();
 
-                txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
-                    eprintln!("Checking if {} is confirmed yet", txid);
-                }));
+                request = request.chain_txids(
+                    unconfirmed_txids
+                        .into_iter()
+                        .inspect(|txid| eprintln!("Checking if {} is confirmed yet", txid)),
+                );
             }
 
-            let electrum_update = client
-                .sync::<ConfirmationHeightAnchor>(
-                    chain.tip(),
-                    spks,
-                    Some(graph.graph()),
-                    txids,
-                    outpoints,
-                    scan_options.batch_size,
-                )
+            let res = client
+                .sync(request, scan_options.batch_size)
                 .context("scanning the blockchain")?;
 
             // drop lock on graph and chain
             drop((graph, chain));
 
-            (electrum_update, BTreeMap::new())
+            (res.chain_update, res.graph_update, None)
         }
     };
 
-    let (
-        ElectrumUpdate {
-            chain_update,
-            mut graph_update,
-        },
-        keychain_update,
-    ) = response;
-
     let now = std::time::UNIX_EPOCH
         .elapsed()
         .expect("must get time")
@@ -316,26 +304,17 @@ fn main() -> anyhow::Result<()> {
         let mut chain = chain.lock().unwrap();
         let mut graph = graph.lock().unwrap();
 
-        let chain = chain.apply_update(chain_update)?;
-
-        let indexed_tx_graph = {
-            let mut changeset =
-                indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
-            let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update);
-            changeset.append(indexed_tx_graph::ChangeSet {
-                indexer,
-                ..Default::default()
-            });
-            changeset.append(graph.apply_update(graph_update.map_anchors(|a| {
-                ConfirmationHeightAnchor {
-                    anchor_block: a.anchor_block,
-                    confirmation_height: a.confirmation_height,
-                }
-            })));
-            changeset
-        };
-
-        (chain, indexed_tx_graph)
+        let chain_changeset = chain.apply_update(chain_update)?;
+
+        let mut indexed_tx_graph_changeset =
+            indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
+        if let Some(keychain_update) = keychain_update {
+            let (_, keychain_changeset) = graph.index.reveal_to_target_multi(&keychain_update);
+            indexed_tx_graph_changeset.append(keychain_changeset.into());
+        }
+        indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
+
+        (chain_changeset, indexed_tx_graph_changeset)
     };
 
     let mut db = db.lock().unwrap();
index 8b4cc559285814181d12f772567d126f11eabd5b..bdbf32120207449ec891c9b76ad8dc3e6a385463 100644 (file)
@@ -3,17 +3,16 @@ const SEND_AMOUNT: Amount = Amount::from_sat(5000);
 const STOP_GAP: usize = 50;
 const BATCH_SIZE: usize = 5;
 
-use std::io::Write;
 use std::str::FromStr;
 
 use bdk::bitcoin::{Address, Amount};
-use bdk::chain::ConfirmationTimeHeightAnchor;
-use bdk::wallet::Update;
+use bdk::chain::collections::HashSet;
 use bdk::{bitcoin::Network, Wallet};
 use bdk::{KeychainKind, SignOptions};
+use bdk_electrum::ElectrumResultExt;
 use bdk_electrum::{
     electrum_client::{self, ElectrumApi},
-    ElectrumExt, ElectrumUpdate,
+    ElectrumExt,
 };
 use bdk_file_store::Store;
 
@@ -39,48 +38,24 @@ fn main() -> Result<(), anyhow::Error> {
     print!("Syncing...");
     let client = electrum_client::Client::new("ssl://electrum.blockstream.info:60002")?;
 
-    let prev_tip = wallet.latest_checkpoint();
-    let keychain_spks = wallet
-        .all_unbounded_spk_iters()
-        .into_iter()
-        .map(|(k, k_spks)| {
-            let mut once = Some(());
-            let mut stdout = std::io::stdout();
-            let k_spks = k_spks
-                .inspect(move |(spk_i, _)| match once.take() {
-                    Some(_) => print!("\nScanning keychain [{:?}]", k),
-                    None => print!(" {:<3}", spk_i),
-                })
-                .inspect(move |_| stdout.flush().expect("must flush"));
-            (k, k_spks)
-        })
-        .collect();
-
-    let (
-        ElectrumUpdate {
-            chain_update,
-            mut graph_update,
-        },
-        keychain_update,
-    ) = client.full_scan::<_, ConfirmationTimeHeightAnchor>(
-        prev_tip,
-        keychain_spks,
-        Some(wallet.as_ref()),
-        STOP_GAP,
-        BATCH_SIZE,
-    )?;
+    let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
+        let mut once = HashSet::<KeychainKind>::new();
+        move |k, spk_i, _| match once.insert(k) {
+            true => print!("\nScanning keychain [{:?}]", k),
+            false => print!(" {:<3}", spk_i),
+        }
+    });
 
-    println!();
+    let mut update = client
+        .full_scan(request, STOP_GAP, BATCH_SIZE)?
+        .try_into_confirmation_time_result(&client)?;
 
     let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
-    let _ = graph_update.update_last_seen_unconfirmed(now);
-
-    let wallet_update = Update {
-        last_active_indices: keychain_update,
-        graph: graph_update,
-        chain: Some(chain_update),
-    };
-    wallet.apply_update(wallet_update)?;
+    let _ = update.graph_update.update_last_seen_unconfirmed(now);
+
+    println!();
+
+    wallet.apply_update(update)?;
     wallet.commit()?;
 
     let balance = wallet.get_balance();