diff --git a/crates/electrum/src/electrum_ext.rs b/crates/electrum/src/electrum_ext.rs index e81ef1d3..b7435862 100644 --- a/crates/electrum/src/electrum_ext.rs +++ b/crates/electrum/src/electrum_ext.rs @@ -14,86 +14,63 @@ use std::{ /// We assume that a block of this depth and deeper cannot be reorged. const ASSUME_FINAL_DEPTH: u32 = 8; -/// Represents an update fetched from an Electrum server, but excludes full -/// transactions. +/// Represents a [`TxGraph`] update fetched from an Electrum server, but excludes full transactions. /// /// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to -/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::finalize`] to fetch -/// the full transactions from Electrum and finalize the update. -#[derive(Debug, Clone)] -pub struct ElectrumUpdate { - /// Map of [`Txid`]s to associated [`Anchor`]s. - pub graph_update: HashMap>, - /// The latest chain tip, as seen by the Electrum server. - pub new_tip: local_chain::CheckPoint, - /// Last-used index update for [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex). - pub keychain_update: BTreeMap, -} - -impl ElectrumUpdate { - fn new(new_tip: local_chain::CheckPoint) -> Self { - Self { - new_tip, - graph_update: HashMap::new(), - keychain_update: BTreeMap::new(), - } - } +/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::finalize`] to +/// fetch the full transactions from Electrum and finalize the update. +#[derive(Debug, Default, Clone)] +pub struct IncompleteTxGraph(HashMap>); +impl IncompleteTxGraph { /// Determine the full transactions that are missing from `graph`. /// - /// Refer to [`ElectrumUpdate`]. + /// Refer to [`IncompleteTxGraph`] for more. pub fn missing_full_txs(&self, graph: &TxGraph) -> Vec { - self.graph_update + self.0 .keys() .filter(move |&&txid| graph.as_ref().get_tx(txid).is_none()) .cloned() .collect() } - /// Finalizes update with `missing` txids to fetch from `client`. + /// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`. /// - /// Refer to [`ElectrumUpdate`]. + /// Refer to [`IncompleteTxGraph`] for more. pub fn finalize( self, client: &Client, seen_at: Option, missing: Vec, - ) -> Result<(TxGraph, BTreeMap, local_chain::CheckPoint), Error> { + ) -> Result, Error> { let new_txs = client.batch_transaction_get(&missing)?; - let mut graph_update = TxGraph::::new(new_txs); - for (txid, anchors) in self.graph_update { + let mut graph = TxGraph::::new(new_txs); + for (txid, anchors) in self.0 { if let Some(seen_at) = seen_at { - let _ = graph_update.insert_seen_at(txid, seen_at); + let _ = graph.insert_seen_at(txid, seen_at); } for anchor in anchors { - let _ = graph_update.insert_anchor(txid, anchor); + let _ = graph.insert_anchor(txid, anchor); } } - Ok((graph_update, self.keychain_update, self.new_tip)) + Ok(graph) } } -impl ElectrumUpdate { - /// Finalizes the [`ElectrumUpdate`] with `new_txs` and anchors of type +impl IncompleteTxGraph { + /// Finalizes the [`IncompleteTxGraph`] with `new_txs` and anchors of type /// [`ConfirmationTimeAnchor`]. /// /// **Note:** The confirmation time might not be precisely correct if there has been a reorg. /// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to /// use it. - pub fn finalize_as_confirmation_time( + pub fn finalize_with_confirmation_time( self, client: &Client, seen_at: Option, missing: Vec, - ) -> Result< - ( - TxGraph, - BTreeMap, - local_chain::CheckPoint, - ), - Error, - > { - let (graph, keychain_update, update_tip) = self.finalize(client, seen_at, missing)?; + ) -> Result, Error> { + let graph = self.finalize(client, seen_at, missing)?; let relevant_heights = { let mut visited_heights = HashSet::new(); @@ -117,7 +94,7 @@ impl ElectrumUpdate { .collect::>(); let graph_changeset = { - let old_changeset = TxGraph::default().apply_update(graph.clone()); + let old_changeset = TxGraph::default().apply_update(graph); tx_graph::ChangeSet { txs: old_changeset.txs, txouts: old_changeset.txouts, @@ -139,16 +116,16 @@ impl ElectrumUpdate { } }; - let mut update = TxGraph::default(); - update.apply_changeset(graph_changeset); - - Ok((update, keychain_update, update_tip)) + let mut new_graph = TxGraph::default(); + new_graph.apply_changeset(graph_changeset); + Ok(new_graph) } } /// Trait to extend [`Client`] functionality. pub trait ElectrumExt { - /// Scan the blockchain (via electrum) for the data specified and returns a [`ElectrumUpdate`]. + /// Scan the blockchain (via electrum) for the data specified and returns updates for + /// [`bdk_chain`] data structures. /// /// - `prev_tip`: the most recent blockchain tip present locally /// - `keychain_spks`: keychains that we want to scan transactions for @@ -159,6 +136,7 @@ pub trait ElectrumExt { /// The 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. + #[allow(clippy::type_complexity)] fn scan( &self, prev_tip: Option, @@ -167,7 +145,7 @@ pub trait ElectrumExt { outpoints: impl IntoIterator, stop_gap: usize, batch_size: usize, - ) -> Result, Error>; + ) -> Result<(local_chain::Update, IncompleteTxGraph, BTreeMap), Error>; /// Convenience method to call [`scan`] without requiring a keychain. /// @@ -179,20 +157,22 @@ pub trait ElectrumExt { txids: impl IntoIterator, outpoints: impl IntoIterator, batch_size: usize, - ) -> Result, Error> { + ) -> Result<(local_chain::Update, IncompleteTxGraph), Error> { let spk_iter = misc_spks .into_iter() .enumerate() .map(|(i, spk)| (i as u32, spk)); - self.scan( + let (chain, graph, _) = self.scan( prev_tip, [((), spk_iter)].into(), txids, outpoints, usize::MAX, batch_size, - ) + )?; + + Ok((chain, graph)) } } @@ -205,7 +185,14 @@ impl ElectrumExt for Client { outpoints: impl IntoIterator, stop_gap: usize, batch_size: usize, - ) -> Result, Error> { + ) -> Result< + ( + local_chain::Update, + IncompleteTxGraph, + BTreeMap, + ), + Error, + > { let mut request_spks = keychain_spks .into_iter() .map(|(k, s)| (k, s.into_iter())) @@ -217,9 +204,8 @@ impl ElectrumExt for Client { let update = loop { let (tip, _) = construct_update_tip(self, prev_tip.clone())?; - let mut update = ElectrumUpdate::::new(tip.clone()); - let cps = update - .new_tip + let mut graph_update = IncompleteTxGraph::::default(); + let cps = tip .iter() .take(10) .map(|cp| (cp.height(), cp)) @@ -230,7 +216,7 @@ impl ElectrumExt for Client { scanned_spks.append(&mut populate_with_spks( self, &cps, - &mut update, + &mut graph_update, &mut scanned_spks .iter() .map(|(i, (spk, _))| (i.clone(), spk.clone())), @@ -243,7 +229,7 @@ impl ElectrumExt for Client { populate_with_spks( self, &cps, - &mut update, + &mut graph_update, keychain_spks, stop_gap, batch_size, @@ -254,10 +240,14 @@ impl ElectrumExt for Client { } } - populate_with_txids(self, &cps, &mut update, &mut txids.iter().cloned())?; + populate_with_txids(self, &cps, &mut graph_update, &mut txids.iter().cloned())?; - let _txs = - populate_with_outpoints(self, &cps, &mut update, &mut outpoints.iter().cloned())?; + let _txs = populate_with_outpoints( + self, + &cps, + &mut graph_update, + &mut outpoints.iter().cloned(), + )?; // check for reorgs during scan process let server_blockhash = self.block_header(tip.height() as usize)?.block_hash(); @@ -265,7 +255,12 @@ impl ElectrumExt for Client { continue; // reorg } - update.keychain_update = request_spks + let chain_update = local_chain::Update { + tip, + introduce_older_blocks: true, + }; + + let keychain_update = request_spks .into_keys() .filter_map(|k| { scanned_spks @@ -275,7 +270,8 @@ impl ElectrumExt for Client { .map(|((_, i), _)| (k, *i)) }) .collect::>(); - break update; + + break (chain_update, graph_update, keychain_update); }; Ok(update) @@ -399,10 +395,10 @@ fn determine_tx_anchor( } } -fn populate_with_outpoints( +fn populate_with_outpoints( client: &Client, cps: &BTreeMap, - update: &mut ElectrumUpdate, + graph_update: &mut IncompleteTxGraph, outpoints: &mut impl Iterator, ) -> Result, Error> { let mut full_txs = HashMap::new(); @@ -451,7 +447,7 @@ fn populate_with_outpoints( }; let anchor = determine_tx_anchor(cps, res.height, res.tx_hash); - let tx_entry = update.graph_update.entry(res.tx_hash).or_default(); + let tx_entry = graph_update.0.entry(res.tx_hash).or_default(); if let Some(anchor) = anchor { tx_entry.insert(anchor); } @@ -460,10 +456,10 @@ fn populate_with_outpoints( Ok(full_txs) } -fn populate_with_txids( +fn populate_with_txids( client: &Client, cps: &BTreeMap, - update: &mut ElectrumUpdate, + graph_update: &mut IncompleteTxGraph, txids: &mut impl Iterator, ) -> Result<(), Error> { for txid in txids { @@ -488,7 +484,7 @@ fn populate_with_txids( None => continue, }; - let tx_entry = update.graph_update.entry(txid).or_default(); + let tx_entry = graph_update.0.entry(txid).or_default(); if let Some(anchor) = anchor { tx_entry.insert(anchor); } @@ -496,10 +492,10 @@ fn populate_with_txids( Ok(()) } -fn populate_with_spks( +fn populate_with_spks( client: &Client, cps: &BTreeMap, - update: &mut ElectrumUpdate, + graph_update: &mut IncompleteTxGraph, spks: &mut impl Iterator, stop_gap: usize, batch_size: usize, @@ -532,7 +528,7 @@ fn populate_with_spks( } for tx in spk_history { - let tx_entry = update.graph_update.entry(tx.tx_hash).or_default(); + let tx_entry = graph_update.0.entry(tx.tx_hash).or_default(); if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) { tx_entry.insert(anchor); } diff --git a/crates/electrum/src/lib.rs b/crates/electrum/src/lib.rs index 716c4d3f..09772626 100644 --- a/crates/electrum/src/lib.rs +++ b/crates/electrum/src/lib.rs @@ -1,14 +1,16 @@ //! This crate is used for updating structures of the [`bdk_chain`] crate with data from electrum. //! //! The star of the show is the [`ElectrumExt::scan`] method, which scans for relevant blockchain -//! data (via electrum) and outputs an [`ElectrumUpdate`]. +//! data (via electrum) and outputs updates for [`bdk_chain`] structures as a tuple of form: //! -//! An [`ElectrumUpdate`] only includes `txid`s and no full transactions. The caller is responsible -//! for obtaining full transactions before applying. This can be done with +//! ([`bdk_chain::local_chain::Update`], [`IncompleteTxGraph`], `keychain_update`) +//! +//! An [`IncompleteTxGraph`] only includes `txid`s and no full transactions. The caller is +//! responsible for obtaining full transactions before applying. This can be done with //! these steps: //! //! 1. Determine which full transactions are missing. The method [`missing_full_txs`] of -//! [`ElectrumUpdate`] can be used. +//! [`IncompleteTxGraph`] can be used. //! //! 2. Obtaining the full transactions. To do this via electrum, the method //! [`batch_transaction_get`] can be used. @@ -16,7 +18,7 @@ //! Refer to [`bdk_electrum_example`] for a complete example. //! //! [`ElectrumClient::scan`]: electrum_client::ElectrumClient::scan -//! [`missing_full_txs`]: ElectrumUpdate::missing_full_txs +//! [`missing_full_txs`]: IncompleteTxGraph::missing_full_txs //! [`batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get //! [`bdk_electrum_example`]: https://github.com/LLFourn/bdk_core_staging/tree/master/bdk_electrum_example diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index f8bb10b1..84501358 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -13,7 +13,7 @@ use bdk_chain::{ }; use bdk_electrum::{ electrum_client::{self, ElectrumApi}, - ElectrumExt, ElectrumUpdate, + ElectrumExt, }; use example_cli::{ anyhow::{self, Context}, @@ -251,20 +251,18 @@ fn main() -> anyhow::Result<()> { // drop lock on graph and chain drop((graph, chain)); - let update = client + let (chain_update, graph_update) = client .scan_without_keychain(tip, spks, txids, outpoints, scan_options.batch_size) .context("scanning the blockchain")?; - ElectrumUpdate { - graph_update: update.graph_update, - new_tip: update.new_tip, - keychain_update: BTreeMap::new(), - } + (chain_update, graph_update, BTreeMap::new()) } }; + let (chain_update, incomplete_graph_update, keychain_update) = response; + let missing_txids = { let graph = &*graph.lock().unwrap(); - response.missing_full_txs(graph.graph()) + incomplete_graph_update.missing_full_txs(graph.graph()) }; let now = std::time::UNIX_EPOCH @@ -272,17 +270,13 @@ fn main() -> anyhow::Result<()> { .expect("must get time") .as_secs(); - let (graph_update, keychain_update, update_tip) = - response.finalize(&client, Some(now), missing_txids)?; + let graph_update = incomplete_graph_update.finalize(&client, Some(now), missing_txids)?; let db_changeset = { let mut chain = chain.lock().unwrap(); let mut graph = graph.lock().unwrap(); - let chain = chain.apply_update(local_chain::Update { - tip: update_tip, - introduce_older_blocks: true, - })?; + let chain = chain.apply_update(chain_update)?; let indexed_tx_graph = { let mut changeset = diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 0ea7df48..f723d665 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -7,9 +7,9 @@ use std::io::Write; use std::str::FromStr; use bdk::bitcoin::Address; +use bdk::wallet::WalletUpdate; use bdk::SignOptions; -use bdk::{bitcoin::Network, wallet::WalletUpdate, Wallet}; -use bdk_electrum::bdk_chain::local_chain; +use bdk::{bitcoin::Network, Wallet}; use bdk_electrum::electrum_client::{self, ElectrumApi}; use bdk_electrum::ElectrumExt; use bdk_file_store::Store; @@ -53,21 +53,19 @@ fn main() -> Result<(), Box> { }) .collect(); - let electrum_update = client.scan(prev_tip, keychain_spks, None, None, STOP_GAP, BATCH_SIZE)?; + let (chain_update, incomplete_graph_update, keychain_update) = + client.scan(prev_tip, keychain_spks, None, None, STOP_GAP, BATCH_SIZE)?; println!(); - let missing = electrum_update.missing_full_txs(wallet.as_ref()); - let (graph_update, keychain_update, update_tip) = - electrum_update.finalize_as_confirmation_time(&client, None, missing)?; + let missing = incomplete_graph_update.missing_full_txs(wallet.as_ref()); + let graph_update = + incomplete_graph_update.finalize_with_confirmation_time(&client, None, missing)?; let wallet_update = WalletUpdate { last_active_indices: keychain_update, graph: graph_update, - chain: Some(local_chain::Update { - tip: update_tip, - introduce_older_blocks: true, - }), + chain: Some(chain_update), }; wallet.apply_update(wallet_update)?; wallet.commit()?;