Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Comment on lines +1248 to +1252
Copy link
Collaborator

@ValuedMammal ValuedMammal Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 This relies on there being a checkpoint in the local chain at the target_height. If there's not, then the next "tip" might be significantly lower. A better way IMO would be to pass the target height as input to FullTxOut::is_confirmed_and_spendable.

Then again, if a UTXO originally confirmed at height $h$, then we can probably assume that it's also the height of the floor checkpoint.

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.
Expand Down
96 changes: 96 additions & 0 deletions wallet/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
Loading