From: 志宇 Date: Thu, 11 Sep 2025 03:48:27 +0000 (+0000) Subject: feat(chain): Add min_confirmations parameter to CanonicalView::balance X-Git-Url: http://internal-gitweb-vhost/?a=commitdiff_plain;h=45249457aa2f1205f5944e73c8d14468712648ab;p=bdk feat(chain): Add min_confirmations parameter to CanonicalView::balance 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 --- diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 79b44b00..6453037e 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -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) } diff --git a/crates/chain/benches/indexer.rs b/crates/chain/benches/indexer.rs index df4e3f36..3caea42d 100644 --- a/crates/chain/benches/indexer.rs +++ b/crates/chain/benches/indexer.rs @@ -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); } diff --git a/crates/chain/src/canonical_view.rs b/crates/chain/src/canonical_view.rs index 2685344e..32adc456 100644 --- a/crates/chain/src/canonical_view.rs +++ b/crates/chain/src/canonical_view.rs @@ -209,10 +209,25 @@ impl CanonicalView { /// `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 + '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 CanonicalView { 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; diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 13a3ab0b..5b44cb16 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -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 diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index 1c413e4e..f91a3a8d 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -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, diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 8c7580f9..0663061e 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -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) } diff --git a/examples/example_bitcoind_rpc_polling/src/main.rs b/examples/example_bitcoind_rpc_polling/src/main.rs index 8c5483bf..0263c5b0 100644 --- a/examples/example_bitcoind_rpc_polling/src/main.rs +++ b/examples/example_bitcoind_rpc_polling/src/main.rs @@ -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: {}", diff --git a/examples/example_cli/src/lib.rs b/examples/example_cli/src/lib.rs index 5f642581..5ef4130f 100644 --- a/examples/example_cli/src/lib.rs +++ b/examples/example_cli/src/lib.rs @@ -530,9 +530,11 @@ pub fn handle_commands( 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;