From 8a5787af5d4c95f20bff2dca3d0e3a43f47648d2 Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Sat, 23 Aug 2025 08:41:19 +0000 Subject: [PATCH] feat(wallet): add helper to compute balance with minimum confirmations --- wallet/src/wallet/mod.rs | 23 ++++++++++ wallet/tests/wallet.rs | 96 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index c1aab082..cdb3b076 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -1236,6 +1236,29 @@ impl Wallet { ) } + /// Return the balance, separated into available, trusted-pending, untrusted-pending and + /// immature values, considering only transactions with at least `confirmation_threshold` + /// confirmations. + pub fn balance_with_confirmation_depth(&self, confirmation_threshold: u32) -> Balance { + let target_height = self + .chain + .tip() + .height() + .saturating_sub(confirmation_threshold.saturating_sub(1)); + let target_tip = self + .chain + .range(..=target_height) + .next() + .expect("local chain must contain genesis"); + self.indexed_graph.graph().balance( + &self.chain, + target_tip.block_id(), + CanonicalizationParams::default(), + self.indexed_graph.index.outpoints().iter().cloned(), + |&(k, _), _| k == KeychainKind::Internal, + ) + } + /// Add an external signer /// /// See [the `signer` module](signer) for an example. diff --git a/wallet/tests/wallet.rs b/wallet/tests/wallet.rs index c779c0a4..76616cb5 100644 --- a/wallet/tests/wallet.rs +++ b/wallet/tests/wallet.rs @@ -2581,6 +2581,102 @@ fn test_spend_coinbase() { builder.finish().unwrap(); } +#[test] +fn test_balance_with_confirmation_depth() { + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let mut wallet = Wallet::create(desc, change_desc) + .network(Network::Regtest) + .create_wallet_no_persist() + .unwrap(); + + let spk = wallet + .next_unused_address(KeychainKind::External) + .script_pubkey(); + + let confirmation_height = 10; + let confirmation_block_id = BlockId { + height: confirmation_height, + hash: BlockHash::all_zeros(), + }; + insert_checkpoint(&mut wallet, confirmation_block_id); + + let coinbase_tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::null(), + ..Default::default() + }], + output: vec![TxOut { + script_pubkey: spk.clone(), + value: Amount::from_sat(70_000), + }], + }; + let coinbase_txid = coinbase_tx.compute_txid(); + + let tx = Transaction { + version: transaction::Version::ONE, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::new(coinbase_txid, 0), + ..Default::default() + }], + output: vec![TxOut { + script_pubkey: spk.clone(), + value: Amount::from_sat(42_000), + }], + }; + let txid = tx.compute_txid(); + + let anchor = ConfirmationBlockTime { + block_id: confirmation_block_id, + confirmation_time: 123456, + }; + + let mut tx_update = bdk_chain::TxUpdate::default(); + tx_update.txs = vec![Arc::new(coinbase_tx), Arc::new(tx)]; + tx_update.anchors = [(anchor, txid)].into(); + wallet + .apply_update(Update { + tx_update, + ..Default::default() + }) + .unwrap(); + + // Insert tip checkpoint at height 15, so the confirmed `tx` has six confirmations. + insert_checkpoint( + &mut wallet, + BlockId { + height: 15, + hash: BlockHash::all_zeros(), + }, + ); + + // With a `confirmation_depth` of 6, the balance should be `confirmed`. + let balance = wallet.balance_with_confirmation_depth(6); + assert_eq!( + balance, + Balance { + immature: Amount::ZERO, + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::ZERO, + confirmed: Amount::from_sat(42_000) + } + ); + + // With a `confirmation_depth` of 7, the balance should be `untrusted_pending`. + let balance = wallet.balance_with_confirmation_depth(7); + assert_eq!( + balance, + Balance { + immature: Amount::ZERO, + trusted_pending: Amount::ZERO, + untrusted_pending: Amount::from_sat(42_000), + confirmed: Amount::ZERO + } + ); +} + #[test] fn test_allow_dust_limit() { let (mut wallet, _) = get_funded_wallet_single(get_test_single_sig_cltv());