Skip to content

Commit 7ac59b9

Browse files
evanlinjinclaude
authored andcommitted
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 <[email protected]>
1 parent 53a0820 commit 7ac59b9

File tree

8 files changed

+52
-14
lines changed

8 files changed

+52
-14
lines changed

crates/bitcoind_rpc/tests/test_emitter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ fn get_balance(
312312
let outpoints = recv_graph.index.outpoints().clone();
313313
let balance = recv_graph
314314
.canonical_view(recv_chain, chain_tip, CanonicalizationParams::default())
315-
.balance(outpoints, |_, _| true);
315+
.balance(outpoints, |_, _| true, 1);
316316
Ok(balance)
317317
}
318318

crates/chain/benches/indexer.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) {
8686
let op = graph.index.outpoints().clone();
8787
let bal = graph
8888
.canonical_view(chain, chain_tip, CanonicalizationParams::default())
89-
.balance(op, |_, _| false);
89+
.balance(op, |_, _| false, 1);
9090
assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
9191
}
9292

crates/chain/src/canonical_view.rs

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,25 @@ impl<A: Anchor> CanonicalView<A> {
209209
/// `outpoints` is a list of outpoints we are interested in, coupled with an outpoint identifier
210210
/// (`O`) for convenience. If `O` is not necessary, the caller can use `()`, or
211211
/// [`Iterator::enumerate`] over a list of [`OutPoint`]s.
212+
///
213+
/// ### Minimum confirmations
214+
///
215+
/// `min_confirmations` specifies the minimum number of confirmations required for a transaction
216+
/// to be counted as confirmed in the returned [`Balance`]. Transactions with fewer than
217+
/// `min_confirmations` will be treated as trusted pending (assuming the `trust_predicate`
218+
/// returns `true`).
219+
///
220+
/// - `min_confirmations = 0`: Include all confirmed transactions (same as `1`)
221+
/// - `min_confirmations = 1`: Standard behavior - require at least 1 confirmation
222+
/// - `min_confirmations = 6`: High security - require at least 6 confirmations
223+
///
224+
/// Note: `0` and `1` behave identically since confirmed transactions always have ≥1
225+
/// confirmation.
212226
pub fn balance<'v, O: Clone + 'v>(
213227
&'v self,
214228
outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
215229
mut trust_predicate: impl FnMut(&O, ScriptBuf) -> bool,
230+
min_confirmations: u32,
216231
) -> Balance {
217232
let mut immature = Amount::ZERO;
218233
let mut trusted_pending = Amount::ZERO;
@@ -221,8 +236,23 @@ impl<A: Anchor> CanonicalView<A> {
221236

222237
for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) {
223238
match &txout.chain_position {
224-
ChainPosition::Confirmed { .. } => {
225-
if txout.is_confirmed_and_spendable(self.tip.height) {
239+
ChainPosition::Confirmed { anchor, .. } => {
240+
let confirmation_height = anchor.confirmation_height_upper_bound();
241+
let confirmations = self
242+
.tip
243+
.height
244+
.saturating_sub(confirmation_height)
245+
.saturating_add(1);
246+
let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically
247+
248+
if confirmations < min_confirmations {
249+
// Not enough confirmations, treat as trusted/untrusted pending
250+
if trust_predicate(&spk_i, txout.txout.script_pubkey) {
251+
trusted_pending += txout.txout.value;
252+
} else {
253+
untrusted_pending += txout.txout.value;
254+
}
255+
} else if txout.is_confirmed_and_spendable(self.tip.height) {
226256
confirmed += txout.txout.value;
227257
} else if !txout.is_mature(self.tip.height) {
228258
immature += txout.txout.value;

crates/chain/tests/test_indexed_tx_graph.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,7 @@ fn test_list_owned_txouts() {
474474
.balance(
475475
graph.index.outpoints().iter().cloned(),
476476
|_, spk: ScriptBuf| trusted_spks.contains(&spk),
477+
1,
477478
);
478479

479480
let confirmed_txouts_txid = txouts

crates/chain/tests/test_tx_graph_conflicts.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,7 @@ fn test_tx_conflict_handling() {
10331033
.balance(
10341034
env.indexer.outpoints().iter().cloned(),
10351035
|_, spk: ScriptBuf| env.indexer.index_of_spk(spk).is_some(),
1036+
1,
10361037
);
10371038
assert_eq!(
10381039
balance, scenario.exp_balance,

crates/electrum/tests/test_electrum.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ fn get_balance(
4242
let outpoints = recv_graph.index.outpoints().clone();
4343
let balance = recv_graph
4444
.canonical_view(recv_chain, chain_tip, CanonicalizationParams::default())
45-
.balance(outpoints, |_, _| true);
45+
.balance(outpoints, |_, _| true, 1);
4646
Ok(balance)
4747
}
4848

examples/example_bitcoind_rpc_polling/src/main.rs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,9 +202,11 @@ fn main() -> anyhow::Result<()> {
202202
synced_to.block_id(),
203203
CanonicalizationParams::default(),
204204
)
205-
.balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
206-
k == &Keychain::Internal
207-
})
205+
.balance(
206+
graph.index.outpoints().iter().cloned(),
207+
|(k, _), _| k == &Keychain::Internal,
208+
1,
209+
)
208210
};
209211
println!(
210212
"[{:>10}s] synced to {} @ {} | total: {}",
@@ -360,9 +362,11 @@ fn main() -> anyhow::Result<()> {
360362
synced_to.block_id(),
361363
CanonicalizationParams::default(),
362364
)
363-
.balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
364-
k == &Keychain::Internal
365-
})
365+
.balance(
366+
graph.index.outpoints().iter().cloned(),
367+
|(k, _), _| k == &Keychain::Internal,
368+
1,
369+
)
366370
};
367371
println!(
368372
"[{:>10}s] synced to {} @ {} / {} | total: {}",

examples/example_cli/src/lib.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -530,9 +530,11 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
530530
chain.get_chain_tip()?,
531531
CanonicalizationParams::default(),
532532
)?
533-
.balance(graph.index.outpoints().iter().cloned(), |(k, _), _| {
534-
k == &Keychain::Internal
535-
});
533+
.balance(
534+
graph.index.outpoints().iter().cloned(),
535+
|(k, _), _| k == &Keychain::Internal,
536+
1,
537+
);
536538

537539
let confirmed_total = balance.confirmed + balance.immature;
538540
let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;

0 commit comments

Comments
 (0)