]> Untitled Git - bdk/commitdiff
feat(chain): Add min_confirmations parameter to CanonicalView::balance
author志宇 <hello@evanlinjin.me>
Thu, 11 Sep 2025 03:48:27 +0000 (03:48 +0000)
committer志宇 <hello@evanlinjin.me>
Wed, 17 Sep 2025 23:46:29 +0000 (23:46 +0000)
Add min_confirmations parameter to control confirmation depth requirements:
- min_confirmations = 0: Include all confirmed transactions (same as 1)
- min_confirmations = 1: Standard behavior - require at least 1 confirmation
- min_confirmations = 6: High security - require at least 6 confirmations

Transactions with fewer than min_confirmations are treated as trusted/untrusted
pending based on the trust_predicate. This restores the minimum confirmation
functionality that was available in the old TxGraph::balance doctest but with
a more intuitive API since CanonicalView has the tip internally.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
crates/bitcoind_rpc/tests/test_emitter.rs
crates/chain/benches/indexer.rs
crates/chain/src/canonical_view.rs
crates/chain/tests/test_indexed_tx_graph.rs
crates/chain/tests/test_tx_graph_conflicts.rs
crates/electrum/tests/test_electrum.rs
examples/example_bitcoind_rpc_polling/src/main.rs
examples/example_cli/src/lib.rs

index 79b44b00a7ff4da396dcaaf462e290925530619f..6453037e6f8f95195128f8ff6f309f8260e9ffb9 100644 (file)
@@ -312,7 +312,7 @@ fn get_balance(
     let outpoints = recv_graph.index.outpoints().clone();
     let balance = recv_graph
         .canonical_view(recv_chain, chain_tip, CanonicalizationParams::default())
-        .balance(outpoints, |_, _| true);
+        .balance(outpoints, |_, _| true, 1);
     Ok(balance)
 }
 
index df4e3f361aa4487127ffc30666761baacd922b0d..3caea42d27f3b9c9e730936608a3f2afbeecdfc3 100644 (file)
@@ -86,7 +86,7 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
     let op = graph.index.outpoints().clone();
     let bal = graph
         .canonical_view(chain, chain_tip, CanonicalizationParams::default())
-        .balance(op, |_, _| false);
+        .balance(op, |_, _| false, 1);
     assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
 }
 
index 2685344e9de91bb306bc043bd62692f476ffb92b..32adc456ea7d86a511548f9ded4434f7f4e9cf7c 100644 (file)
@@ -209,10 +209,25 @@ impl<A: Anchor> CanonicalView<A> {
     /// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
     /// (`O`) for convenience. If `O` is not necessary, the caller can use `()`, or
     /// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
+    ///
+    /// ### Minimum confirmations
+    ///
+    /// `min_confirmations` specifies the minimum number of confirmations required for a transaction
+    /// to be counted as confirmed in the returned [`Balance`]. Transactions with fewer than
+    /// `min_confirmations` will be treated as trusted pending (assuming the `trust_predicate`
+    /// returns `true`).
+    ///
+    /// - `min_confirmations = 0`: Include all confirmed transactions (same as `1`)
+    /// - `min_confirmations = 1`: Standard behavior - require at least 1 confirmation
+    /// - `min_confirmations = 6`: High security - require at least 6 confirmations
+    ///
+    /// Note: `0` and `1` behave identically since confirmed transactions always have ≥1
+    /// confirmation.
     pub fn balance<'v, O: Clone + 'v>(
         &'v self,
         outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
         mut trust_predicate: impl FnMut(&O, ScriptBuf) -> bool,
+        min_confirmations: u32,
     ) -> Balance {
         let mut immature = Amount::ZERO;
         let mut trusted_pending = Amount::ZERO;
@@ -221,8 +236,23 @@ impl<A: Anchor> CanonicalView<A> {
 
         for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) {
             match &txout.chain_position {
-                ChainPosition::Confirmed { .. } => {
-                    if txout.is_confirmed_and_spendable(self.tip.height) {
+                ChainPosition::Confirmed { anchor, .. } => {
+                    let confirmation_height = anchor.confirmation_height_upper_bound();
+                    let confirmations = self
+                        .tip
+                        .height
+                        .saturating_sub(confirmation_height)
+                        .saturating_add(1);
+                    let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically
+
+                    if confirmations < min_confirmations {
+                        // Not enough confirmations, treat as trusted/untrusted pending
+                        if trust_predicate(&spk_i, txout.txout.script_pubkey) {
+                            trusted_pending += txout.txout.value;
+                        } else {
+                            untrusted_pending += txout.txout.value;
+                        }
+                    } else if txout.is_confirmed_and_spendable(self.tip.height) {
                         confirmed += txout.txout.value;
                     } else if !txout.is_mature(self.tip.height) {
                         immature += txout.txout.value;
index 13a3ab0ba831333e9eba27275c86a16a5476419f..5b44cb1634d31af8dcaebbf7a3c47b0317b8bb43 100644 (file)
@@ -474,6 +474,7 @@ fn test_list_owned_txouts() {
                 .balance(
                     graph.index.outpoints().iter().cloned(),
                     |_, spk: ScriptBuf| trusted_spks.contains(&spk),
+                    1,
                 );
 
             let confirmed_txouts_txid = txouts
index 1c413e4e94c38868215098103b501eb2cbe915ac..f91a3a8d356dd641756284eb8f503f089e2ea4b9 100644 (file)
@@ -1033,6 +1033,7 @@ fn test_tx_conflict_handling() {
             .balance(
                 env.indexer.outpoints().iter().cloned(),
                 |_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(),
+                1,
             );
         assert_eq!(
             balance, scenario.exp_balance,
index 8c7580f972603456ae640c71fecfe8563f7bda59..0663061e4dc184ebd65e3947b0da532d78e81157 100644 (file)
@@ -42,7 +42,7 @@ fn get_balance(
     let outpoints = recv_graph.index.outpoints().clone();
     let balance = recv_graph
         .canonical_view(recv_chain, chain_tip, CanonicalizationParams::default())
-        .balance(outpoints, |_, _| true);
+        .balance(outpoints, |_, _| true, 1);
     Ok(balance)
 }
 
index 8c5483bf4d298011dff483b9d82723e46b4f53f3..0263c5b0bd5afee470d5fa4c47972097a981dc2d 100644 (file)
@@ -202,9 +202,11 @@ fn main() -> anyhow::Result<()> {
                                 synced_to.block_id(),
                                 CanonicalizationParams::default(),
                             )
-                            .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
-                                k == &Keychain::Internal
-                            })
+                            .balance(
+                                graph.index.outpoints().iter().cloned(),
+                                |(k, _), _| k == &Keychain::Internal,
+                                1,
+                            )
                     };
                     println!(
                         "[{:>10}s] synced to {} @ {} | total: {}",
@@ -360,9 +362,11 @@ fn main() -> anyhow::Result<()> {
                                 synced_to.block_id(),
                                 CanonicalizationParams::default(),
                             )
-                            .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
-                                k == &Keychain::Internal
-                            })
+                            .balance(
+                                graph.index.outpoints().iter().cloned(),
+                                |(k, _), _| k == &Keychain::Internal,
+                                1,
+                            )
                     };
                     println!(
                         "[{:>10}s] synced to {} @ {} / {} | total: {}",
index 5f642581a9175d07fbe50ea7b4d618493d5e6225..5ef4130f7cf7a3695fee422c5d9972d3042f8bfa 100644 (file)
@@ -530,9 +530,11 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
                     chain.get_chain_tip()?,
                     CanonicalizationParams::default(),
                 )?
-                .balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
-                    k == &Keychain::Internal
-                });
+                .balance(
+                    graph.index.outpoints().iter().cloned(),
+                    |(k, _), _| k == &Keychain::Internal,
+                    1,
+                );
 
             let confirmed_total = balance.confirmed + balance.immature;
             let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;