]> Untitled Git - bdk/commitdiff
feat(chain,wallet)!: Transitive `ChainPosition`
author志宇 <hello@evanlinjin.me>
Tue, 19 Nov 2024 07:47:10 +0000 (18:47 +1100)
committer志宇 <hello@evanlinjin.me>
Sat, 30 Nov 2024 23:42:01 +0000 (10:42 +1100)
Change `ChainPosition` to be able to represent transitive anchors and
unconfirm-without-last-seen values.

crates/chain/src/chain_data.rs
crates/chain/src/tx_graph.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_tx_graph.rs
crates/wallet/src/test_utils.rs
crates/wallet/src/wallet/coin_selection.rs
crates/wallet/src/wallet/export.rs
crates/wallet/src/wallet/mod.rs
crates/wallet/src/wallet/tx_builder.rs
crates/wallet/tests/wallet.rs

index efdb7a09935975a6c703c2edfe427237e04b6e55..a4d764c026ed4551e9117c6b87c41a70de7b8585 100644 (file)
@@ -15,16 +15,30 @@ use crate::Anchor;
     ))
 )]
 pub enum ChainPosition<A> {
-    /// The chain data is seen as confirmed, and in anchored by `A`.
-    Confirmed(A),
-    /// The chain data is not confirmed and last seen in the mempool at this timestamp.
-    Unconfirmed(u64),
+    /// The chain data is confirmed as it is anchored in the best chain by `A`.
+    Confirmed {
+        /// The [`Anchor`].
+        anchor: A,
+        /// Whether the chain data is anchored transitively by a child transaction.
+        ///
+        /// If the value is `Some`, it means we have incomplete data. We can only deduce that the
+        /// chain data is confirmed at a block equal to or lower than the block referenced by `A`.
+        transitively: Option<Txid>,
+    },
+    /// The chain data is not confirmed.
+    Unconfirmed {
+        /// When the chain data is last seen in the mempool.
+        ///
+        /// This value will be `None` if the chain data was never seen in the mempool and only seen
+        /// in a conflicting chain.
+        last_seen: Option<u64>,
+    },
 }
 
 impl<A> ChainPosition<A> {
     /// Returns whether [`ChainPosition`] is confirmed or not.
     pub fn is_confirmed(&self) -> bool {
-        matches!(self, Self::Confirmed(_))
+        matches!(self, Self::Confirmed { .. })
     }
 }
 
@@ -32,8 +46,14 @@ impl<A: Clone> ChainPosition<&A> {
     /// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
     pub fn cloned(self) -> ChainPosition<A> {
         match self {
-            ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
-            ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
+            ChainPosition::Confirmed {
+                anchor,
+                transitively,
+            } => ChainPosition::Confirmed {
+                anchor: anchor.clone(),
+                transitively,
+            },
+            ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen },
         }
     }
 }
@@ -42,8 +62,10 @@ impl<A: Anchor> ChainPosition<A> {
     /// Determines the upper bound of the confirmation height.
     pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
         match self {
-            ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
-            ChainPosition::Unconfirmed(_) => None,
+            ChainPosition::Confirmed { anchor, .. } => {
+                Some(anchor.confirmation_height_upper_bound())
+            }
+            ChainPosition::Unconfirmed { .. } => None,
         }
     }
 }
@@ -73,14 +95,14 @@ impl<A: Anchor> FullTxOut<A> {
     /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
     pub fn is_mature(&self, tip: u32) -> bool {
         if self.is_on_coinbase {
-            let tx_height = match &self.chain_position {
-                ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
-                ChainPosition::Unconfirmed(_) => {
+            let conf_height = match self.chain_position.confirmation_height_upper_bound() {
+                Some(height) => height,
+                None => {
                     debug_assert!(false, "coinbase tx can never be unconfirmed");
                     return false;
                 }
             };
-            let age = tip.saturating_sub(tx_height);
+            let age = tip.saturating_sub(conf_height);
             if age + 1 < COINBASE_MATURITY {
                 return false;
             }
@@ -103,17 +125,21 @@ impl<A: Anchor> FullTxOut<A> {
             return false;
         }
 
-        let confirmation_height = match &self.chain_position {
-            ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
-            ChainPosition::Unconfirmed(_) => return false,
+        let conf_height = match self.chain_position.confirmation_height_upper_bound() {
+            Some(height) => height,
+            None => return false,
         };
-        if confirmation_height > tip {
+        if conf_height > tip {
             return false;
         }
 
         // if the spending tx is confirmed within tip height, the txout is no longer spendable
-        if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
-            if spending_anchor.anchor_block().height <= tip {
+        if let Some(spend_height) = self
+            .spent_by
+            .as_ref()
+            .and_then(|(pos, _)| pos.confirmation_height_upper_bound())
+        {
+            if spend_height <= tip {
                 return false;
             }
         }
@@ -132,22 +158,32 @@ mod test {
 
     #[test]
     fn chain_position_ord() {
-        let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
-        let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
-        let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
-            confirmation_time: 20,
-            block_id: BlockId {
-                height: 9,
-                ..Default::default()
+        let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
+            last_seen: Some(10),
+        };
+        let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed {
+            last_seen: Some(20),
+        };
+        let conf1 = ChainPosition::Confirmed {
+            anchor: ConfirmationBlockTime {
+                confirmation_time: 20,
+                block_id: BlockId {
+                    height: 9,
+                    ..Default::default()
+                },
             },
-        });
-        let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
-            confirmation_time: 15,
-            block_id: BlockId {
-                height: 12,
-                ..Default::default()
+            transitively: None,
+        };
+        let conf2 = ChainPosition::Confirmed {
+            anchor: ConfirmationBlockTime {
+                confirmation_time: 15,
+                block_id: BlockId {
+                    height: 12,
+                    ..Default::default()
+                },
             },
-        });
+            transitively: None,
+        };
 
         assert!(unconf2 > unconf1, "higher last_seen means higher ord");
         assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
index a10d1aeb8cbf873058243134f5cbf74cb92f4cd1..b266cf9ea2bec24757340b4a65dc14bc0725e834 100644 (file)
@@ -770,7 +770,12 @@ impl<A: Anchor> TxGraph<A> {
 
         for anchor in anchors {
             match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? {
-                Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))),
+                Some(true) => {
+                    return Ok(Some(ChainPosition::Confirmed {
+                        anchor,
+                        transitively: None,
+                    }))
+                }
                 _ => continue,
             }
         }
@@ -877,7 +882,9 @@ impl<A: Anchor> TxGraph<A> {
             }
         }
 
-        Ok(Some(ChainPosition::Unconfirmed(last_seen)))
+        Ok(Some(ChainPosition::Unconfirmed {
+            last_seen: Some(last_seen),
+        }))
     }
 
     /// Get the position of the transaction in `chain` with tip `chain_tip`.
@@ -1146,14 +1153,14 @@ impl<A: Anchor> TxGraph<A> {
             let (spk_i, txout) = res?;
 
             match &txout.chain_position {
-                ChainPosition::Confirmed(_) => {
+                ChainPosition::Confirmed { .. } => {
                     if txout.is_confirmed_and_spendable(chain_tip.height) {
                         confirmed += txout.txout.value;
                     } else if !txout.is_mature(chain_tip.height) {
                         immature += txout.txout.value;
                     }
                 }
-                ChainPosition::Unconfirmed(_) => {
+                ChainPosition::Unconfirmed { .. } => {
                     if trust_predicate(&spk_i, txout.txout.script_pubkey) {
                         trusted_pending += txout.txout.value;
                     } else {
index a8d17ca91094daf26ae6cc7214e2e46b2678e170..1b3dff573e6d5a1d75aa7ba8bd0cc4d27dda88e5 100644 (file)
@@ -293,7 +293,7 @@ fn test_list_owned_txouts() {
             let confirmed_txouts_txid = txouts
                 .iter()
                 .filter_map(|(_, full_txout)| {
-                    if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
+                    if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
                         Some(full_txout.outpoint.txid)
                     } else {
                         None
@@ -304,7 +304,7 @@ fn test_list_owned_txouts() {
             let unconfirmed_txouts_txid = txouts
                 .iter()
                 .filter_map(|(_, full_txout)| {
-                    if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
+                    if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
                         Some(full_txout.outpoint.txid)
                     } else {
                         None
@@ -315,7 +315,7 @@ fn test_list_owned_txouts() {
             let confirmed_utxos_txid = utxos
                 .iter()
                 .filter_map(|(_, full_txout)| {
-                    if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
+                    if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) {
                         Some(full_txout.outpoint.txid)
                     } else {
                         None
@@ -326,7 +326,7 @@ fn test_list_owned_txouts() {
             let unconfirmed_utxos_txid = utxos
                 .iter()
                 .filter_map(|(_, full_txout)| {
-                    if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
+                    if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) {
                         Some(full_txout.outpoint.txid)
                     } else {
                         None
@@ -618,7 +618,7 @@ fn test_get_chain_position() {
             },
             anchor: None,
             last_seen: Some(2),
-            exp_pos: Some(ChainPosition::Unconfirmed(2)),
+            exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
         },
         TestCase {
             name: "tx anchor in best chain - confirmed",
@@ -631,7 +631,10 @@ fn test_get_chain_position() {
             },
             anchor: Some(blocks[1]),
             last_seen: None,
-            exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
+            exp_pos: Some(ChainPosition::Confirmed {
+                anchor: blocks[1],
+                transitively: None,
+            }),
         },
         TestCase {
             name: "tx unknown anchor with last_seen - unconfirmed",
@@ -644,7 +647,7 @@ fn test_get_chain_position() {
             },
             anchor: Some(block_id!(2, "B'")),
             last_seen: Some(2),
-            exp_pos: Some(ChainPosition::Unconfirmed(2)),
+            exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }),
         },
         TestCase {
             name: "tx unknown anchor - no chain pos",
index 66fc08fcc316959e5baf6abaa6f2d0327809b474..05d5d63e48035ac95ba6f2250c58be73b7878975 100644 (file)
@@ -885,13 +885,16 @@ fn test_chain_spends() {
             OutPoint::new(tx_0.compute_txid(), 0)
         ),
         Some((
-            ChainPosition::Confirmed(&ConfirmationBlockTime {
-                block_id: BlockId {
-                    hash: tip.get(98).unwrap().hash(),
-                    height: 98,
+            ChainPosition::Confirmed {
+                anchor: &ConfirmationBlockTime {
+                    block_id: BlockId {
+                        hash: tip.get(98).unwrap().hash(),
+                        height: 98,
+                    },
+                    confirmation_time: 100
                 },
-                confirmation_time: 100
-            }),
+                transitively: None
+            },
             tx_1.compute_txid(),
         )),
     );
@@ -900,13 +903,16 @@ fn test_chain_spends() {
     assert_eq!(
         graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
         // Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
-        Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
-            block_id: BlockId {
-                hash: tip.get(95).unwrap().hash(),
-                height: 95,
+        Some(ChainPosition::Confirmed {
+            anchor: &ConfirmationBlockTime {
+                block_id: BlockId {
+                    hash: tip.get(95).unwrap().hash(),
+                    height: 95,
+                },
+                confirmation_time: 100
             },
-            confirmation_time: 100
-        }))
+            transitively: None
+        })
     );
 
     // Mark the unconfirmed as seen and check correct ObservedAs status is returned.
@@ -921,7 +927,12 @@ fn test_chain_spends() {
                 OutPoint::new(tx_0.compute_txid(), 1)
             )
             .unwrap(),
-        (ChainPosition::Unconfirmed(1234567), tx_2.compute_txid())
+        (
+            ChainPosition::Unconfirmed {
+                last_seen: Some(1234567)
+            },
+            tx_2.compute_txid()
+        )
     );
 
     // A conflicting transaction that conflicts with tx_1.
@@ -957,7 +968,9 @@ fn test_chain_spends() {
         graph
             .get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid())
             .expect("position expected"),
-        ChainPosition::Unconfirmed(1234568)
+        ChainPosition::Unconfirmed {
+            last_seen: Some(1234568)
+        }
     );
 
     // Chain_spend now catches the new transaction as the spend.
@@ -970,7 +983,9 @@ fn test_chain_spends() {
             )
             .expect("expect observation"),
         (
-            ChainPosition::Unconfirmed(1234568),
+            ChainPosition::Unconfirmed {
+                last_seen: Some(1234568)
+            },
             tx_2_conflict.compute_txid()
         )
     );
index 050b9fb19ae842aa9c1ff20016994ce71fd72dbc..c69de620af747850ac637272ab21345eec49c3a4 100644 (file)
@@ -229,12 +229,15 @@ pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoi
     let latest_cp = wallet.latest_checkpoint();
     let height = latest_cp.height();
     let anchor = if height == 0 {
-        ChainPosition::Unconfirmed(0)
+        ChainPosition::Unconfirmed { last_seen: Some(0) }
     } else {
-        ChainPosition::Confirmed(ConfirmationBlockTime {
-            block_id: latest_cp.block_id(),
-            confirmation_time: 0,
-        })
+        ChainPosition::Confirmed {
+            anchor: ConfirmationBlockTime {
+                block_id: latest_cp.block_id(),
+                confirmation_time: 0,
+            },
+            transitively: None,
+        }
     };
     receive_output(wallet, value, anchor)
 }
@@ -270,11 +273,13 @@ pub fn receive_output_to_address(
     insert_tx(wallet, tx);
 
     match pos {
-        ChainPosition::Confirmed(anchor) => {
+        ChainPosition::Confirmed { anchor, .. } => {
             insert_anchor(wallet, txid, anchor);
         }
-        ChainPosition::Unconfirmed(last_seen) => {
-            insert_seen_at(wallet, txid, last_seen);
+        ChainPosition::Unconfirmed { last_seen } => {
+            if let Some(last_seen) = last_seen {
+                insert_seen_at(wallet, txid, last_seen);
+            }
         }
     }
 
index 0f0e4a88e0ebdfb3feac576b85adbdc89f7bc0ca..a651ddc54ca5bdbaeec8a80f5666b09671915b2a 100644 (file)
@@ -754,7 +754,13 @@ mod test {
     const FEE_AMOUNT: u64 = 50;
 
     fn unconfirmed_utxo(value: u64, index: u32, last_seen: u64) -> WeightedUtxo {
-        utxo(value, index, ChainPosition::Unconfirmed(last_seen))
+        utxo(
+            value,
+            index,
+            ChainPosition::Unconfirmed {
+                last_seen: Some(last_seen),
+            },
+        )
     }
 
     fn confirmed_utxo(
@@ -766,13 +772,16 @@ mod test {
         utxo(
             value,
             index,
-            ChainPosition::Confirmed(ConfirmationBlockTime {
-                block_id: chain::BlockId {
-                    height: confirmation_height,
-                    hash: bitcoin::BlockHash::all_zeros(),
+            ChainPosition::Confirmed {
+                anchor: ConfirmationBlockTime {
+                    block_id: chain::BlockId {
+                        height: confirmation_height,
+                        hash: bitcoin::BlockHash::all_zeros(),
+                    },
+                    confirmation_time,
                 },
-                confirmation_time,
-            }),
+                transitively: None,
+            },
         )
     }
 
@@ -838,15 +847,18 @@ mod test {
                     is_spent: false,
                     derivation_index: rng.next_u32(),
                     chain_position: if rng.gen_bool(0.5) {
-                        ChainPosition::Confirmed(ConfirmationBlockTime {
-                            block_id: chain::BlockId {
-                                height: rng.next_u32(),
-                                hash: BlockHash::all_zeros(),
+                        ChainPosition::Confirmed {
+                            anchor: ConfirmationBlockTime {
+                                block_id: chain::BlockId {
+                                    height: rng.next_u32(),
+                                    hash: BlockHash::all_zeros(),
+                                },
+                                confirmation_time: rng.next_u64(),
                             },
-                            confirmation_time: rng.next_u64(),
-                        })
+                            transitively: None,
+                        }
                     } else {
-                        ChainPosition::Unconfirmed(0)
+                        ChainPosition::Unconfirmed { last_seen: Some(0) }
                     },
                 }),
             });
@@ -871,7 +883,7 @@ mod test {
                     keychain: KeychainKind::External,
                     is_spent: false,
                     derivation_index: 42,
-                    chain_position: ChainPosition::Unconfirmed(0),
+                    chain_position: ChainPosition::Unconfirmed { last_seen: Some(0) },
                 }),
             })
             .collect()
@@ -1228,7 +1240,7 @@ mod test {
         optional.push(utxo(
             500_000,
             3,
-            ChainPosition::<ConfirmationBlockTime>::Unconfirmed(0),
+            ChainPosition::<ConfirmationBlockTime>::Unconfirmed { last_seen: Some(0) },
         ));
 
         // Defensive assertions, for sanity and in case someone changes the test utxos vector.
@@ -1590,13 +1602,16 @@ mod test {
                     keychain: KeychainKind::External,
                     is_spent: false,
                     derivation_index: 0,
-                    chain_position: ChainPosition::Confirmed(ConfirmationBlockTime {
-                        block_id: BlockId {
-                            height: 12345,
-                            hash: BlockHash::all_zeros(),
+                    chain_position: ChainPosition::Confirmed {
+                        anchor: ConfirmationBlockTime {
+                            block_id: BlockId {
+                                height: 12345,
+                                hash: BlockHash::all_zeros(),
+                            },
+                            confirmation_time: 12345,
                         },
-                        confirmation_time: 12345,
-                    }),
+                        transitively: None,
+                    },
                 }),
             }
         }
index 6441d3b58978c8885fd4fd7debf1e7616918fe49..cbbee2e2e4c8597a98e3097f78042a21f7f9f59c 100644 (file)
@@ -129,10 +129,10 @@ impl FullyNodedExport {
 
         let blockheight = if include_blockheight {
             wallet.transactions().next().map_or(0, |canonical_tx| {
-                match canonical_tx.chain_position {
-                    bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height,
-                    bdk_chain::ChainPosition::Unconfirmed(_) => 0,
-                }
+                canonical_tx
+                    .chain_position
+                    .confirmation_height_upper_bound()
+                    .unwrap_or(0)
             })
         } else {
             0
index 7c8c8872d9ba2ac163e22aea338502d419f7c65a..e1c35eb79ca8c49cca834f2daf3c64b694c06b9b 100644 (file)
@@ -1037,12 +1037,22 @@ impl Wallet {
     ///
     /// // get confirmation status of transaction
     /// match wallet_tx.chain_position {
-    ///     ChainPosition::Confirmed(anchor) => println!(
+    ///     ChainPosition::Confirmed {
+    ///         anchor,
+    ///         transitively: None,
+    ///     } => println!(
     ///         "tx is confirmed at height {}, we know this since {}:{} is in the best chain",
     ///         anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash,
     ///     ),
-    ///     ChainPosition::Unconfirmed(last_seen) => println!(
-    ///         "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain",
+    ///     ChainPosition::Confirmed {
+    ///         anchor,
+    ///         transitively: Some(_),
+    ///     } => println!(
+    ///         "tx is an ancestor of a tx anchored in {}:{}",
+    ///         anchor.block_id.height, anchor.block_id.hash,
+    ///     ),
+    ///     ChainPosition::Unconfirmed { last_seen } => println!(
+    ///         "tx is last seen at {:?}, it is unconfirmed as it is not anchored in the best chain",
     ///         last_seen,
     ///     ),
     /// }
@@ -1590,7 +1600,7 @@ impl Wallet {
         let pos = graph
             .get_chain_position(&self.chain, chain_tip, txid)
             .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?;
-        if let ChainPosition::Confirmed(_) = pos {
+        if pos.is_confirmed() {
             return Err(BuildFeeBumpError::TransactionConfirmed(txid));
         }
 
@@ -1840,9 +1850,10 @@ impl Wallet {
                 .indexed_graph
                 .graph()
                 .get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
-                .map(|chain_position| match chain_position {
-                    ChainPosition::Confirmed(a) => a.block_id.height,
-                    ChainPosition::Unconfirmed(_) => u32::MAX,
+                .map(|chain_position| {
+                    chain_position
+                        .confirmation_height_upper_bound()
+                        .unwrap_or(u32::MAX)
                 });
             let current_height = sign_options
                 .assume_height
@@ -2032,9 +2043,10 @@ impl Wallet {
                     );
                     if let Some(current_height) = current_height {
                         match chain_position {
-                            ChainPosition::Confirmed(a) => {
+                            ChainPosition::Confirmed { anchor, .. } => {
                                 // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
-                                spendable &= (current_height.saturating_sub(a.block_id.height))
+                                spendable &= (current_height
+                                    .saturating_sub(anchor.block_id.height))
                                     >= COINBASE_MATURITY;
                             }
                             ChainPosition::Unconfirmed { .. } => spendable = false,
index 49f3c19d153ce944baf0b4367aea6803411d03e7..8872dc2e08a36d6c4d76b0f9730b03189cf9f432 100644 (file)
@@ -1017,7 +1017,7 @@ mod test {
                 txout: TxOut::NULL,
                 keychain: KeychainKind::External,
                 is_spent: false,
-                chain_position: chain::ChainPosition::Unconfirmed(0),
+                chain_position: chain::ChainPosition::Unconfirmed { last_seen: Some(0) },
                 derivation_index: 0,
             },
             LocalOutput {
@@ -1028,13 +1028,16 @@ mod test {
                 txout: TxOut::NULL,
                 keychain: KeychainKind::Internal,
                 is_spent: false,
-                chain_position: chain::ChainPosition::Confirmed(chain::ConfirmationBlockTime {
-                    block_id: chain::BlockId {
-                        height: 32,
-                        hash: bitcoin::BlockHash::all_zeros(),
+                chain_position: chain::ChainPosition::Confirmed {
+                    anchor: chain::ConfirmationBlockTime {
+                        block_id: chain::BlockId {
+                            height: 32,
+                            hash: bitcoin::BlockHash::all_zeros(),
+                        },
+                        confirmation_time: 42,
                     },
-                    confirmation_time: 42,
-                }),
+                    transitively: None,
+                },
                 derivation_index: 1,
             },
         ]
index bc8ee4d4946ac09c0777b427a7d61cd2eae8e29d..cd9408d4588079b8a2fd47ca7e6975af80aa364f 100644 (file)
@@ -1439,7 +1439,11 @@ fn test_create_tx_increment_change_index() {
             .create_wallet_no_persist()
             .unwrap();
         // fund wallet
-        receive_output(&mut wallet, amount, ChainPosition::Unconfirmed(0));
+        receive_output(
+            &mut wallet,
+            amount,
+            ChainPosition::Unconfirmed { last_seen: Some(0) },
+        );
         // create tx
         let mut builder = wallet.build_tx();
         builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send));
@@ -2541,7 +2545,11 @@ fn test_bump_fee_unconfirmed_inputs_only() {
     let psbt = builder.finish().unwrap();
     // Now we receive one transaction with 0 confirmations. We won't be able to use that for
     // fee bumping, as it's still unconfirmed!
-    receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
+    receive_output(
+        &mut wallet,
+        25_000,
+        ChainPosition::Unconfirmed { last_seen: Some(0) },
+    );
     let mut tx = psbt.extract_tx().expect("failed to extract tx");
     let txid = tx.compute_txid();
     for txin in &mut tx.input {
@@ -2566,7 +2574,11 @@ fn test_bump_fee_unconfirmed_input() {
         .assume_checked();
     // We receive a tx with 0 confirmations, which will be used as an input
     // in the drain tx.
-    receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
+    receive_output(
+        &mut wallet,
+        25_000,
+        ChainPosition::Unconfirmed { last_seen: Some(0) },
+    );
     let mut builder = wallet.build_tx();
     builder.drain_wallet().drain_to(addr.script_pubkey());
     let psbt = builder.finish().unwrap();
@@ -4036,13 +4048,16 @@ fn test_keychains_with_overlapping_spks() {
         .last()
         .unwrap()
         .address;
-    let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime {
-        block_id: BlockId {
-            height: 2000,
-            hash: BlockHash::all_zeros(),
+    let chain_position = ChainPosition::Confirmed {
+        anchor: ConfirmationBlockTime {
+            block_id: BlockId {
+                height: 2000,
+                hash: BlockHash::all_zeros(),
+            },
+            confirmation_time: 0,
         },
-        confirmation_time: 0,
-    });
+        transitively: None,
+    };
     let _outpoint = receive_output_to_address(&mut wallet, addr, 8000, chain_position);
     assert_eq!(wallet.balance().confirmed, Amount::from_sat(58000));
 }
@@ -4132,7 +4147,11 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() {
         .unwrap();
     assert_eq!(wallet.keychains().count(), 1);
     let amt = Amount::from_sat(5_000);
-    receive_output(&mut wallet, 2 * amt.to_sat(), ChainPosition::Unconfirmed(2));
+    receive_output(
+        &mut wallet,
+        2 * amt.to_sat(),
+        ChainPosition::Unconfirmed { last_seen: Some(2) },
+    );
     // create spend tx that produces a change output
     let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")
         .unwrap()
@@ -4158,7 +4177,11 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() {
 #[test]
 fn test_transactions_sort_by() {
     let (mut wallet, _txid) = get_funded_wallet_wpkh();
-    receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0));
+    receive_output(
+        &mut wallet,
+        25_000,
+        ChainPosition::Unconfirmed { last_seen: Some(0) },
+    );
 
     // sort by chain position, unconfirmed then confirmed by descending block height
     let sorted_txs: Vec<WalletTx> =