diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 34cbccf5c..06bbc2b32 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -541,7 +541,11 @@ impl TxGraph { /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// - /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. + /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch + /// update all unconfirmed transactions with the latest `seen_at`, see + /// [`update_last_seen_unconfirmed`]. + /// + /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { let mut update = Self::default(); let (_, _, update_last_seen) = update.txs.entry(txid).or_default(); @@ -549,6 +553,65 @@ impl TxGraph { self.apply_update(update) } + /// Update the last seen time for all unconfirmed transactions. + /// + /// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting + /// the given `seen_at` for every transaction not yet anchored to a confirmed block, + /// and returns the [`ChangeSet`] after applying all updates to `self`. + /// + /// This is useful for keeping track of the latest time a transaction was seen + /// unconfirmed, which is important for evaluating transaction conflicts in the same + /// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for + /// [`try_get_chain_position`]. + /// + /// A normal use of this method is to call it with the current system time. Although + /// block headers contain a timestamp, using the header time would be less effective + /// at tracking mempool transactions, because it can drift from actual clock time, plus + /// we may want to update a transaction's last seen time repeatedly between blocks. + /// + /// # Example + /// + /// ```rust + /// # use bdk_chain::example_utils::*; + /// # use std::time::UNIX_EPOCH; + /// # let tx = tx_from_hex(RAW_TX_1); + /// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]); + /// let now = std::time::SystemTime::now() + /// .duration_since(UNIX_EPOCH) + /// .expect("valid duration") + /// .as_secs(); + /// let changeset = tx_graph.update_last_seen_unconfirmed(now); + /// assert!(!changeset.last_seen.is_empty()); + /// ``` + /// + /// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must + /// by strictly greater than what is currently stored for a transaction to have an effect. + /// To insert a last seen time for a single txid, see [`insert_seen_at`]. + /// + /// [`insert_seen_at`]: Self::insert_seen_at + /// [`try_get_chain_position`]: Self::try_get_chain_position + pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet { + let mut changeset = ChangeSet::default(); + let unanchored_txs: Vec = self + .txs + .iter() + .filter_map( + |(&txid, (_, anchors, _))| { + if anchors.is_empty() { + Some(txid) + } else { + None + } + }, + ) + .collect(); + + for txid in unanchored_txs { + changeset.append(self.insert_seen_at(txid, seen_at)); + } + changeset + } + /// Extends this graph with another so that `self` becomes the union of the two sets of /// transactions. /// diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 37e8c7192..c646d431f 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1048,6 +1048,34 @@ fn test_changeset_last_seen_append() { } } +#[test] +fn update_last_seen_unconfirmed() { + let mut graph = TxGraph::<()>::default(); + let tx = new_tx(0); + let txid = tx.txid(); + + // insert a new tx + // initially we have a last_seen of 0, and no anchors + let _ = graph.insert_tx(tx); + let tx = graph.full_txs().next().unwrap(); + assert_eq!(tx.last_seen_unconfirmed, 0); + assert!(tx.anchors.is_empty()); + + // higher timestamp should update last seen + let changeset = graph.update_last_seen_unconfirmed(2); + assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2); + + // lower timestamp has no effect + let changeset = graph.update_last_seen_unconfirmed(1); + assert!(changeset.last_seen.is_empty()); + + // once anchored, last seen is not updated + let _ = graph.insert_anchor(txid, ()); + let changeset = graph.update_last_seen_unconfirmed(4); + assert!(changeset.is_empty()); + assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2); +} + #[test] fn test_missing_blocks() { /// An anchor implementation for testing, made up of `(the_anchor_block, random_data)`.