diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 92a08a9ef..d24b1b307 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -91,13 +91,10 @@ where /// Apply an `update` directly. /// /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`]. - pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { - let graph = self.graph.apply_update(update); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { - tx_graph: graph, - indexer, - } + pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { + let tx_graph = self.graph.apply_update(update); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } } /// Insert a floating `txout` of given `outpoint`. diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 567a8f0a9..e31b431dd 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -3,7 +3,7 @@ use crate::{ alloc::{boxed::Box, collections::VecDeque, vec::Vec}, collections::BTreeMap, local_chain::CheckPoint, - ConfirmationBlockTime, Indexed, TxGraph, + ConfirmationBlockTime, Indexed, }; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; @@ -345,8 +345,8 @@ impl SyncRequest { #[must_use] #[derive(Debug)] pub struct SyncResult { - /// The update to apply to the receiving [`TxGraph`]. - pub graph_update: TxGraph, + /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). + pub graph_update: crate::tx_graph::Update, /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). pub chain_update: Option, } @@ -497,8 +497,8 @@ impl FullScanRequest { #[derive(Debug)] pub struct FullScanResult { /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). - pub graph_update: TxGraph, - /// The update to apply to the receiving [`TxGraph`]. + pub graph_update: crate::tx_graph::Update, + /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). pub chain_update: Option, /// Last active indices for the corresponding keychains (`K`). pub last_active_indices: BTreeMap, diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 9ab1268b3..ba894fa93 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -70,13 +70,17 @@ //! //! ``` //! # use bdk_chain::{Merge, BlockId}; -//! # use bdk_chain::tx_graph::TxGraph; +//! # use bdk_chain::tx_graph::{self, TxGraph}; //! # use bdk_chain::example_utils::*; //! # use bitcoin::Transaction; +//! # use std::sync::Arc; //! # let tx_a = tx_from_hex(RAW_TX_1); //! # let tx_b = tx_from_hex(RAW_TX_2); //! let mut graph: TxGraph = TxGraph::default(); -//! let update = TxGraph::new(vec![tx_a, tx_b]); +//! +//! let mut update = tx_graph::Update::default(); +//! update.txs.push(Arc::new(tx_a)); +//! update.txs.push(Arc::new(tx_b)); //! //! // apply the update graph //! let changeset = graph.apply_update(update.clone()); @@ -101,6 +105,79 @@ use core::{ ops::{Deref, RangeInclusive}, }; +/// Data object used to update the [`TxGraph`] with. +#[derive(Debug, Clone)] +pub struct Update { + /// Full transactions. + pub txs: Vec>, + /// Floating txouts. + pub txouts: BTreeMap, + /// Transaction anchors. + pub anchors: BTreeSet<(A, Txid)>, + /// Seen at times for transactions. + pub seen_ats: HashMap, +} + +impl Default for Update { + fn default() -> Self { + Self { + txs: Default::default(), + txouts: Default::default(), + anchors: Default::default(), + seen_ats: Default::default(), + } + } +} + +impl From> for Update { + fn from(graph: TxGraph) -> Self { + Self { + txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(), + txouts: graph + .floating_txouts() + .map(|(op, txo)| (op, txo.clone())) + .collect(), + anchors: graph.anchors, + seen_ats: graph.last_seen.into_iter().collect(), + } + } +} + +impl From> for TxGraph { + fn from(update: Update) -> Self { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(update); + graph + } +} + +impl Update { + /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions. + pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) { + let seen_ats = &mut self.seen_ats; + let anchors = &self.anchors; + let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| { + for (_, anchor_txid) in anchors { + if txid == anchor_txid { + return false; + } + } + true + }); + for txid in unanchored_txids { + seen_ats.insert(txid, seen_at); + } + } + + /// Extend this update with `other`. + pub fn extend(&mut self, other: Update) { + self.txs.extend(other.txs); + self.txouts.extend(other.txouts); + self.anchors.extend(other.anchors); + self.seen_ats.extend(other.seen_ats); + } +} + /// A graph of transactions and spends. /// /// See the [module-level documentation] for more. @@ -690,19 +767,19 @@ impl TxGraph { /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). - pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { + pub fn apply_update(&mut self, update: Update) -> ChangeSet { let mut changeset = ChangeSet::::default(); - for tx_node in update.full_txs() { - changeset.merge(self.insert_tx(tx_node.tx)); + for tx in update.txs { + changeset.merge(self.insert_tx(tx)); } - for (outpoint, txout) in update.floating_txouts() { - changeset.merge(self.insert_txout(outpoint, txout.clone())); + for (outpoint, txout) in update.txouts { + changeset.merge(self.insert_txout(outpoint, txout)); } - for (anchor, txid) in &update.anchors { - changeset.merge(self.insert_anchor(*txid, anchor.clone())); + for (anchor, txid) in update.anchors { + changeset.merge(self.insert_anchor(txid, anchor)); } - for (&txid, &last_seen) in &update.last_seen { - changeset.merge(self.insert_seen_at(txid, last_seen)); + for (txid, seen_at) in update.seen_ats { + changeset.merge(self.insert_seen_at(txid, seen_at)); } changeset } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 3ffa82439..c6399f53b 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,7 +2,7 @@ #[macro_use] mod common; -use bdk_chain::tx_graph::CalculateFeeError; +use bdk_chain::tx_graph::{self, CalculateFeeError}; use bdk_chain::{ collections::*, local_chain::LocalChain, @@ -49,7 +49,7 @@ fn insert_txouts() { )]; // One full transaction to be included in the update - let update_txs = Transaction { + let update_tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![TxIn { @@ -63,17 +63,17 @@ fn insert_txouts() { }; // Conf anchor used to mark the full transaction as confirmed. - let conf_anchor = ChainPosition::Confirmed(BlockId { + let conf_anchor = BlockId { height: 100, hash: h!("random blockhash"), - }); + }; - // Unconfirmed anchor to mark the partial transactions as unconfirmed - let unconf_anchor = ChainPosition::::Unconfirmed(1000000); + // Unconfirmed seen_at timestamp to mark the partial transactions as unconfirmed. + let unconf_seen_at = 1000000_u64; // Make the original graph let mut graph = { - let mut graph = TxGraph::>::default(); + let mut graph = TxGraph::::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -88,57 +88,21 @@ fn insert_txouts() { // Make the update graph let update = { - let mut graph = TxGraph::default(); + let mut update = tx_graph::Update::default(); for (outpoint, txout) in &update_ops { - // Insert partials transactions - assert_eq!( - graph.insert_txout(*outpoint, txout.clone()), - ChangeSet { - txouts: [(*outpoint, txout.clone())].into(), - ..Default::default() - } - ); + // Insert partials transactions. + update.txouts.insert(*outpoint, txout.clone()); // Mark them unconfirmed. - assert_eq!( - graph.insert_anchor(outpoint.txid, unconf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(unconf_anchor, outpoint.txid)].into(), - last_seen: [].into() - } - ); - // Mark them last seen at. - assert_eq!( - graph.insert_seen_at(outpoint.txid, 1000000), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [].into(), - last_seen: [(outpoint.txid, 1000000)].into() - } - ); + update.seen_ats.insert(outpoint.txid, unconf_seen_at); } - // Insert the full transaction - assert_eq!( - graph.insert_tx(update_txs.clone()), - ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), - ..Default::default() - } - ); + // Insert the full transaction. + update.txs.push(update_tx.clone().into()); // Mark it as confirmed. - assert_eq!( - graph.insert_anchor(update_txs.compute_txid(), conf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(conf_anchor, update_txs.compute_txid())].into(), - last_seen: [].into() - } - ); - graph + update + .anchors + .insert((conf_anchor, update_tx.compute_txid())); + update }; // Check the resulting addition. @@ -147,13 +111,9 @@ fn insert_txouts() { assert_eq!( changeset, ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), + txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.clone().into(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] - .into(), + anchors: [(conf_anchor, update_tx.compute_txid()),].into(), last_seen: [(h!("tx2"), 1000000)].into() } ); @@ -188,7 +148,7 @@ fn insert_txouts() { assert_eq!( graph - .tx_outputs(update_txs.compute_txid()) + .tx_outputs(update_tx.compute_txid()) .expect("should exists"), [( 0u32, @@ -204,13 +164,9 @@ fn insert_txouts() { assert_eq!( graph.initial_changeset(), ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), + txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] - .into(), + anchors: [(conf_anchor, update_tx.compute_txid()),].into(), last_seen: [(h!("tx2"), 1000000)].into() } ); diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 1458e2bd9..571660756 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -3,12 +3,12 @@ use bdk_chain::{ collections::{BTreeMap, HashMap}, local_chain::CheckPoint, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph::TxGraph, + tx_graph::{self, TxGraph}, Anchor, BlockId, ConfirmationBlockTime, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; use std::{ - collections::BTreeSet, + collections::HashSet, sync::{Arc, Mutex}, }; @@ -138,7 +138,7 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = TxGraph::::default(); + let mut graph_update = tx_graph::Update::::default(); let mut last_active_indices = BTreeMap::::default(); for keychain in request.keychains() { let spks = request.iter_spks(keychain.clone()); @@ -158,7 +158,7 @@ impl BdkElectrumClient { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.all_anchors(), + graph_update.anchors.iter().cloned(), )?), _ => None, }; @@ -205,7 +205,7 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = TxGraph::::default(); + let mut graph_update = tx_graph::Update::::default(); self.populate_with_spks( &mut graph_update, request @@ -227,7 +227,7 @@ impl BdkElectrumClient { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.all_anchors(), + graph_update.anchors.iter().cloned(), )?), None => None, }; @@ -245,7 +245,7 @@ impl BdkElectrumClient { /// also included. fn populate_with_spks( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, mut spks: impl Iterator, stop_gap: usize, batch_size: usize, @@ -278,7 +278,7 @@ impl BdkElectrumClient { } for tx_res in spk_history { - let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?); + graph_update.txs.push(self.fetch_tx(tx_res.tx_hash)?); self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?; } } @@ -291,7 +291,7 @@ impl BdkElectrumClient { /// included. Anchors of the aforementioned transactions are included. fn populate_with_outpoints( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -314,7 +314,7 @@ impl BdkElectrumClient { if !has_residing && res.tx_hash == op_txid { has_residing = true; - let _ = graph_update.insert_tx(Arc::clone(&op_tx)); + graph_update.txs.push(Arc::clone(&op_tx)); self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } @@ -328,7 +328,7 @@ impl BdkElectrumClient { if !has_spending { continue; } - let _ = graph_update.insert_tx(Arc::clone(&res_tx)); + graph_update.txs.push(Arc::clone(&res_tx)); self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } } @@ -339,7 +339,7 @@ impl BdkElectrumClient { /// Populate the `graph_update` with transactions/anchors of the provided `txids`. fn populate_with_txids( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, txids: impl IntoIterator, ) -> Result<(), Error> { for txid in txids { @@ -366,7 +366,7 @@ impl BdkElectrumClient { self.validate_merkle_for_anchor(graph_update, txid, r.height)?; } - let _ = graph_update.insert_tx(tx); + graph_update.txs.push(tx); } Ok(()) } @@ -375,7 +375,7 @@ impl BdkElectrumClient { // An anchor is inserted if the transaction is validated to be in a confirmed block. fn validate_merkle_for_anchor( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, txid: Txid, confirmation_height: i32, ) -> Result<(), Error> { @@ -402,8 +402,7 @@ impl BdkElectrumClient { } if is_confirmed_tx { - let _ = graph_update.insert_anchor( - txid, + graph_update.anchors.insert(( ConfirmationBlockTime { confirmation_time: header.time as u64, block_id: BlockId { @@ -411,7 +410,8 @@ impl BdkElectrumClient { hash: header.block_hash(), }, }, - ); + txid, + )); } } Ok(()) @@ -421,17 +421,18 @@ impl BdkElectrumClient { // which we do not have by default. This data is needed to calculate the transaction fee. fn fetch_prev_txout( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, ) -> Result<(), Error> { - let full_txs: Vec> = - graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); - for tx in full_txs { - for vin in &tx.input { - let outpoint = vin.previous_output; - let vout = outpoint.vout; - let prev_tx = self.fetch_tx(outpoint.txid)?; - let txout = prev_tx.output[vout as usize].clone(); - let _ = graph_update.insert_txout(outpoint, txout); + let mut no_dup = HashSet::::new(); + for tx in &graph_update.txs { + if no_dup.insert(tx.compute_txid()) { + for vin in &tx.input { + let outpoint = vin.previous_output; + let vout = outpoint.vout; + let prev_tx = self.fetch_tx(outpoint.txid)?; + let txout = prev_tx.output[vout as usize].clone(); + let _ = graph_update.txouts.insert(outpoint, txout); + } } } Ok(()) @@ -516,7 +517,7 @@ fn fetch_tip_and_latest_blocks( fn chain_update( mut tip: CheckPoint, latest_blocks: &BTreeMap, - anchors: &BTreeSet<(A, Txid)>, + anchors: impl Iterator, ) -> Result { for anchor in anchors { let height = anchor.0.anchor_block().height; diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 63e91081b..e8b054d33 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -1,9 +1,9 @@ use bdk_chain::{ - bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResult}, spk_txout::SpkTxOutIndex, - Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, + Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -49,7 +49,7 @@ where .elapsed() .expect("must get time") .as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); if let Some(chain_update) = update.chain_update.clone() { let _ = chain @@ -128,18 +128,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -151,12 +156,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); - + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions", + ); Ok(()) } @@ -216,7 +224,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1, false)? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -227,10 +235,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -259,8 +267,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -273,8 +282,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); @@ -475,13 +485,12 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Retain a snapshot of all anchors before reorg process. - let initial_anchors = update.graph_update.all_anchors(); - let anchors: Vec<_> = initial_anchors.iter().cloned().collect(); - assert_eq!(anchors.len(), REORG_COUNT); + let initial_anchors = update.graph_update.anchors.clone(); + assert_eq!(initial_anchors.len(), REORG_COUNT); for i in 0..REORG_COUNT { - let (anchor, txid) = anchors[i]; + let (anchor, txid) = initial_anchors.iter().nth(i).unwrap(); assert_eq!(anchor.block_id.hash, hashes[i]); - assert_eq!(txid, txids[i]); + assert_eq!(*txid, txids[i]); } // Check if initial balance is correct. @@ -507,7 +516,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Check that no new anchors are added during current reorg. - assert!(initial_anchors.is_superset(update.graph_update.all_anchors())); + assert!(initial_anchors.is_superset(&update.graph_update.anchors)); assert_eq!( get_balance(&recv_chain, &recv_graph)?, diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 066b91e17..f3c8e966a 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use async_trait::async_trait; use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; @@ -6,10 +6,9 @@ use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, collections::BTreeMap, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, ConfirmationBlockTime, }; -use bdk_chain::{Anchor, Indexed}; -use esplora_client::{Tx, TxStatus}; +use bdk_chain::{tx_graph, Anchor, Indexed}; use futures::{stream::FuturesOrdered, TryStreamExt}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -72,23 +71,29 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = TxGraph::default(); + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in keychains { let keychain_spks = request.iter_spks(keychain.clone()); - let (tx_graph, last_active_index) = - fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests) - .await?; - let _ = graph_update.apply_update(tx_graph); + let (update, last_active_index) = fetch_txs_with_keychain_spks( + self, + &mut inserted_txs, + keychain_spks, + stop_gap, + parallel_requests, + ) + .await?; + graph_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } } let chain_update = match (chain_tip, latest_blocks) { - (Some(chain_tip), Some(latest_blocks)) => Some( - chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?, - ), + (Some(chain_tip), Some(latest_blocks)) => { + Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + } _ => None, }; @@ -113,20 +118,40 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = TxGraph::::default(); - let _ = graph_update - .apply_update(fetch_txs_with_spks(self, request.iter_spks(), parallel_requests).await?); - let _ = graph_update.apply_update( - fetch_txs_with_txids(self, request.iter_txids(), parallel_requests).await?, + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); + graph_update.extend( + fetch_txs_with_spks( + self, + &mut inserted_txs, + request.iter_spks(), + parallel_requests, + ) + .await?, + ); + graph_update.extend( + fetch_txs_with_txids( + self, + &mut inserted_txs, + request.iter_txids(), + parallel_requests, + ) + .await?, ); - let _ = graph_update.apply_update( - fetch_txs_with_outpoints(self, request.iter_outpoints(), parallel_requests).await?, + graph_update.extend( + fetch_txs_with_outpoints( + self, + &mut inserted_txs, + request.iter_outpoints(), + parallel_requests, + ) + .await?, ); let chain_update = match (chain_tip, latest_blocks) { - (Some(chain_tip), Some(latest_blocks)) => Some( - chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?, - ), + (Some(chain_tip), Some(latest_blocks)) => { + Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + } _ => None, }; @@ -252,13 +277,14 @@ async fn chain_update( /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_keychain_spks> + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, Option), Error> { +) -> Result<(tx_graph::Update, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut tx_graph = TxGraph::default(); + let mut update = tx_graph::Update::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -294,9 +320,11 @@ async fn fetch_txs_with_keychain_spks> + S last_active_index = Some(index); } for tx in txs { - let _ = tx_graph.insert_tx(tx.to_tx()); - insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status); - insert_prevouts(&mut tx_graph, tx.vin); + if inserted_txs.insert(tx.txid) { + update.txs.push(tx.to_tx().into()); + } + insert_anchor_from_status(&mut update, tx.txid, tx.status); + insert_prevouts(&mut update, tx.vin); } } @@ -311,7 +339,7 @@ async fn fetch_txs_with_keychain_spks> + S } } - Ok((tx_graph, last_active_index)) + Ok((update, last_active_index)) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks` @@ -324,20 +352,22 @@ async fn fetch_txs_with_keychain_spks> + S /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_spks + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { fetch_txs_with_keychain_spks( client, + inserted_txs, spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)), usize::MAX, parallel_requests, ) .await - .map(|(tx_graph, _)| tx_graph) + .map(|(update, _)| update) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids` @@ -348,39 +378,27 @@ where /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_txids + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { - enum EsploraResp { - TxStatus(TxStatus), - Tx(Option), - } - - let mut tx_graph = TxGraph::default(); - let mut txids = txids.into_iter(); + let mut update = tx_graph::Update::::default(); + // Only fetch for non-inserted txs. + let mut txids = txids + .into_iter() + .filter(|txid| !inserted_txs.contains(txid)) + .collect::>() + .into_iter(); loop { let handles = txids .by_ref() .take(parallel_requests) .map(|txid| { let client = client.clone(); - let tx_already_exists = tx_graph.get_tx(txid).is_some(); - async move { - if tx_already_exists { - client - .get_tx_status(&txid) - .await - .map(|s| (txid, EsploraResp::TxStatus(s))) - } else { - client - .get_tx_info(&txid) - .await - .map(|t| (txid, EsploraResp::Tx(t))) - } - } + async move { client.get_tx_info(&txid).await.map(|t| (txid, t)) } }) .collect::>(); @@ -388,21 +406,17 @@ where break; } - for (txid, resp) in handles.try_collect::>().await? { - match resp { - EsploraResp::TxStatus(status) => { - insert_anchor_from_status(&mut tx_graph, txid, status); - } - EsploraResp::Tx(Some(tx_info)) => { - let _ = tx_graph.insert_tx(tx_info.to_tx()); - insert_anchor_from_status(&mut tx_graph, txid, tx_info.status); - insert_prevouts(&mut tx_graph, tx_info.vin); + for (txid, tx_info) in handles.try_collect::>().await? { + if let Some(tx_info) = tx_info { + if inserted_txs.insert(txid) { + update.txs.push(tx_info.to_tx().into()); } - _ => continue, + insert_anchor_from_status(&mut update, txid, tx_info.status); + insert_prevouts(&mut update, tx_info.vin); } } } - Ok(tx_graph) + Ok(update) } /// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided @@ -413,22 +427,27 @@ where /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_outpoints + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { let outpoints = outpoints.into_iter().collect::>(); + let mut update = tx_graph::Update::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). - let mut tx_graph = fetch_txs_with_txids( - client, - outpoints.iter().copied().map(|op| op.txid), - parallel_requests, - ) - .await?; + update.extend( + fetch_txs_with_txids( + client, + inserted_txs, + outpoints.iter().copied().map(|op| op.txid), + parallel_requests, + ) + .await?, + ); // get outpoint spend-statuses let mut outpoints = outpoints.into_iter(); @@ -452,18 +471,18 @@ where Some(txid) => txid, None => continue, }; - if tx_graph.get_tx(spend_txid).is_none() { + if !inserted_txs.contains(&spend_txid) { missing_txs.push(spend_txid); } if let Some(spend_status) = op_status.status { - insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status); + insert_anchor_from_status(&mut update, spend_txid, spend_status); } } } - let _ = - tx_graph.apply_update(fetch_txs_with_txids(client, missing_txs, parallel_requests).await?); - Ok(tx_graph) + update + .extend(fetch_txs_with_txids(client, inserted_txs, missing_txs, parallel_requests).await?); + Ok(update) } #[cfg(test)] diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 6e3e25afe..62f0d351e 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use std::thread::JoinHandle; use bdk_chain::collections::BTreeMap; @@ -6,10 +6,10 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, ConfirmationBlockTime, }; -use bdk_chain::{Anchor, Indexed}; -use esplora_client::{OutputStatus, Tx, TxStatus}; +use bdk_chain::{tx_graph, Anchor, Indexed}; +use esplora_client::{OutputStatus, Tx}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -66,13 +66,19 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = TxGraph::default(); + let mut graph_update = tx_graph::Update::default(); + let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in request.keychains() { let keychain_spks = request.iter_spks(keychain.clone()); - let (tx_graph, last_active_index) = - fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests)?; - let _ = graph_update.apply_update(tx_graph); + let (update, last_active_index) = fetch_txs_with_keychain_spks( + self, + &mut inserted_txs, + keychain_spks, + stop_gap, + parallel_requests, + )?; + graph_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } @@ -83,7 +89,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - graph_update.all_anchors(), + &graph_update.anchors, )?), _ => None, }; @@ -109,19 +115,23 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = TxGraph::default(); - let _ = graph_update.apply_update(fetch_txs_with_spks( + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); + graph_update.extend(fetch_txs_with_spks( self, + &mut inserted_txs, request.iter_spks(), parallel_requests, )?); - let _ = graph_update.apply_update(fetch_txs_with_txids( + graph_update.extend(fetch_txs_with_txids( self, + &mut inserted_txs, request.iter_txids(), parallel_requests, )?); - let _ = graph_update.apply_update(fetch_txs_with_outpoints( + graph_update.extend(fetch_txs_with_outpoints( self, + &mut inserted_txs, request.iter_outpoints(), parallel_requests, )?); @@ -131,7 +141,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - graph_update.all_anchors(), + &graph_update.anchors, )?), _ => None, }; @@ -244,13 +254,14 @@ fn chain_update( fn fetch_txs_with_keychain_spks>>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, Option), Error> { +) -> Result<(tx_graph::Update, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut tx_graph = TxGraph::default(); + let mut update = tx_graph::Update::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -289,9 +300,11 @@ fn fetch_txs_with_keychain_spks>>( last_active_index = Some(index); } for tx in txs { - let _ = tx_graph.insert_tx(tx.to_tx()); - insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status); - insert_prevouts(&mut tx_graph, tx.vin); + if inserted_txs.insert(tx.txid) { + update.txs.push(tx.to_tx().into()); + } + insert_anchor_from_status(&mut update, tx.txid, tx.status); + insert_prevouts(&mut update, tx.vin); } } @@ -306,7 +319,7 @@ fn fetch_txs_with_keychain_spks>>( } } - Ok((tx_graph, last_active_index)) + Ok((update, last_active_index)) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks` @@ -319,16 +332,18 @@ fn fetch_txs_with_keychain_spks>>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_spks>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { fetch_txs_with_keychain_spks( client, + inserted_txs, spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)), usize::MAX, parallel_requests, ) - .map(|(tx_graph, _)| tx_graph) + .map(|(update, _)| update) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids` @@ -339,59 +354,48 @@ fn fetch_txs_with_spks>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_txids>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> { - enum EsploraResp { - TxStatus(TxStatus), - Tx(Option), - } - - let mut tx_graph = TxGraph::default(); - let mut txids = txids.into_iter(); +) -> Result, Error> { + let mut update = tx_graph::Update::::default(); + // Only fetch for non-inserted txs. + let mut txids = txids + .into_iter() + .filter(|txid| !inserted_txs.contains(txid)) + .collect::>() + .into_iter(); loop { let handles = txids .by_ref() .take(parallel_requests) .map(|txid| { let client = client.clone(); - let tx_already_exists = tx_graph.get_tx(txid).is_some(); std::thread::spawn(move || { - if tx_already_exists { - client - .get_tx_status(&txid) - .map_err(Box::new) - .map(|s| (txid, EsploraResp::TxStatus(s))) - } else { - client - .get_tx_info(&txid) - .map_err(Box::new) - .map(|t| (txid, EsploraResp::Tx(t))) - } + client + .get_tx_info(&txid) + .map_err(Box::new) + .map(|t| (txid, t)) }) }) - .collect::>>>(); + .collect::), Error>>>>(); if handles.is_empty() { break; } for handle in handles { - let (txid, resp) = handle.join().expect("thread must not panic")?; - match resp { - EsploraResp::TxStatus(status) => { - insert_anchor_from_status(&mut tx_graph, txid, status); + let (txid, tx_info) = handle.join().expect("thread must not panic")?; + if let Some(tx_info) = tx_info { + if inserted_txs.insert(txid) { + update.txs.push(tx_info.to_tx().into()); } - EsploraResp::Tx(Some(tx_info)) => { - let _ = tx_graph.insert_tx(tx_info.to_tx()); - insert_anchor_from_status(&mut tx_graph, txid, tx_info.status); - insert_prevouts(&mut tx_graph, tx_info.vin); - } - _ => continue, + insert_anchor_from_status(&mut update, txid, tx_info.status); + insert_prevouts(&mut update, tx_info.vin); } } } - Ok(tx_graph) + Ok(update) } /// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided @@ -402,18 +406,21 @@ fn fetch_txs_with_txids>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_outpoints>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let outpoints = outpoints.into_iter().collect::>(); + let mut update = tx_graph::Update::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). - let mut tx_graph = fetch_txs_with_txids( + update.extend(fetch_txs_with_txids( client, + inserted_txs, outpoints.iter().map(|op| op.txid), parallel_requests, - )?; + )?); // get outpoint spend-statuses let mut outpoints = outpoints.into_iter(); @@ -442,22 +449,23 @@ fn fetch_txs_with_outpoints>( Some(txid) => txid, None => continue, }; - if tx_graph.get_tx(spend_txid).is_none() { + if !inserted_txs.contains(&spend_txid) { missing_txs.push(spend_txid); } if let Some(spend_status) = op_status.status { - insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status); + insert_anchor_from_status(&mut update, spend_txid, spend_status); } } } } - let _ = tx_graph.apply_update(fetch_txs_with_txids( + update.extend(fetch_txs_with_txids( client, + inserted_txs, missing_txs, parallel_requests, )?); - Ok(tx_graph) + Ok(update) } #[cfg(test)] diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 7db6967b6..9a6e8f1df 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -26,7 +26,7 @@ //! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora use bdk_chain::bitcoin::{Amount, OutPoint, TxOut, Txid}; -use bdk_chain::{BlockId, ConfirmationBlockTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime}; use esplora_client::TxStatus; pub use esplora_client; @@ -42,7 +42,7 @@ mod async_ext; pub use async_ext::*; fn insert_anchor_from_status( - tx_graph: &mut TxGraph, + update: &mut tx_graph::Update, txid: Txid, status: TxStatus, ) { @@ -57,21 +57,21 @@ fn insert_anchor_from_status( block_id: BlockId { height, hash }, confirmation_time: time, }; - let _ = tx_graph.insert_anchor(txid, anchor); + update.anchors.insert((anchor, txid)); } } /// Inserts floating txouts into `tx_graph` using [`Vin`](esplora_client::api::Vin)s returned by /// Esplora. fn insert_prevouts( - tx_graph: &mut TxGraph, + update: &mut tx_graph::Update, esplora_inputs: impl IntoIterator, ) { let prevouts = esplora_inputs .into_iter() .filter_map(|vin| Some((vin.txid, vin.vout, vin.prevout?))); for (prev_txid, prev_vout, prev_txout) in prevouts { - let _ = tx_graph.insert_txout( + update.txouts.insert( OutPoint::new(prev_txid, prev_vout), TxOut { script_pubkey: prev_txout.scriptpubkey, diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 70d464194..7b0ef7fa6 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -1,4 +1,5 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_chain::{ConfirmationBlockTime, TxGraph}; use bdk_esplora::EsploraAsyncExt; use esplora_client::{self, Builder}; use std::collections::{BTreeSet, HashSet}; @@ -6,7 +7,7 @@ use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount, Txid}; +use bdk_chain::bitcoin::{Address, Amount}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; #[tokio::test] @@ -78,18 +79,23 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -101,11 +107,15 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions" + ); Ok(()) } @@ -167,7 +177,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1).await? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -178,10 +188,10 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -212,8 +222,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -226,8 +237,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 818f1f5fb..b3833b899 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -1,4 +1,5 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_chain::{ConfirmationBlockTime, TxGraph}; use bdk_esplora::EsploraExt; use esplora_client::{self, Builder}; use std::collections::{BTreeSet, HashSet}; @@ -6,7 +7,7 @@ use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount, Txid}; +use bdk_chain::bitcoin::{Address, Amount}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; #[test] @@ -78,18 +79,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -101,12 +107,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); - + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions" + ); Ok(()) } @@ -168,7 +177,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1)? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -179,10 +188,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -213,8 +222,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -227,8 +237,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 6dce7503b..386d9d4e3 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -219,13 +219,13 @@ mod test { use bdk_chain::{BlockId, ConfirmationBlockTime}; use bitcoin::hashes::Hash; use bitcoin::{transaction, BlockHash, Network, Transaction}; + use chain::tx_graph; use super::*; use crate::Wallet; fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { use crate::wallet::Update; - use bdk_chain::TxGraph; let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string()) .network(network) .create_wallet_no_persist() @@ -253,11 +253,12 @@ mod test { confirmation_time: 0, block_id, }; - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + anchors: [(anchor, txid)].into_iter().collect(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 47e440c71..0d6cdf184 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -132,7 +132,7 @@ pub struct Update { pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. - pub graph: TxGraph, + pub graph: chain::tx_graph::Update, /// Update for the wallet's internal [`LocalChain`]. /// @@ -2562,7 +2562,7 @@ macro_rules! floating_rate { macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph}; + use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph}; use $crate::{Update, KeychainKind, Wallet}; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; @@ -2590,9 +2590,13 @@ macro_rules! doctest_wallet { confirmation_time: 50_000, block_id, }; - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); - let update = Update { graph, ..Default::default() }; + let update = Update { + graph: tx_graph::Update { + anchors: [(anchor, txid)].into_iter().collect(), + ..Default::default() + }, + ..Default::default() + }; wallet.apply_update(update).unwrap(); wallet }} diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 288560b0a..561a9a5fb 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet}; use bitcoin::{ hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, @@ -218,11 +218,12 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir }) .expect("confirmation height cannot be greater than tip"); - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + anchors: [(anchor, txid)].into(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index c530e779c..243161658 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use anyhow::Context; use assert_matches::assert_matches; -use bdk_chain::COINBASE_MATURITY; +use bdk_chain::{tx_graph, COINBASE_MATURITY}; use bdk_chain::{BlockId, ConfirmationTime}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; @@ -81,11 +81,12 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { use bdk_wallet::Update; - let mut graph = bdk_chain::TxGraph::default(); - let _ = graph.insert_seen_at(txid, seen_at); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + seen_ats: [(txid, seen_at)].into_iter().collect(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 49608fbf1..7212547d6 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -252,7 +252,7 @@ fn main() -> anyhow::Result<()> { .elapsed() .expect("must get time") .as_secs(); - let _ = graph_update.update_last_seen_unconfirmed(now); + graph_update.update_last_seen_unconfirmed(now); let db_changeset = { let mut chain = chain.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index b07a6697d..d188eab76 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -172,7 +172,7 @@ fn main() -> anyhow::Result<()> { // We want to keep track of the latest time a transaction was seen unconfirmed. let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); let mut graph = graph.lock().expect("mutex must not be poisoned"); let mut chain = chain.lock().expect("mutex must not be poisoned"); @@ -269,7 +269,7 @@ fn main() -> anyhow::Result<()> { // Update last seen unconfirmed let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); ( chain diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index f4596ce18..4cc698a00 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -67,7 +67,7 @@ fn main() -> Result<(), anyhow::Error> { let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); println!(); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index f81f8101c..d4dae1f3f 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -61,7 +61,7 @@ async fn main() -> Result<(), anyhow::Error> { .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; wallet.persist(&mut conn)?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index bec395611..9f79d6bf6 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -61,7 +61,7 @@ fn main() -> Result<(), anyhow::Error> { let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() {