Skip to content

Commit 67bd171

Browse files
110CodingPthunderbiscuit
authored andcommitted
feat: add balance method to Wallet
which in turn calls `Wallet::balance_with_params_conf_threshold` under the hood. Also added some test utilities and a test to check the implementation of the new methods.
1 parent 366dbbe commit 67bd171

File tree

3 files changed

+530
-43
lines changed

3 files changed

+530
-43
lines changed

src/test_utils.rs

Lines changed: 50 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
#![allow(unused)]
33
use alloc::string::ToString;
44
use alloc::sync::Arc;
5+
use core::fmt;
56
use core::str::FromStr;
67

78
use bdk_chain::{BlockId, ConfirmationBlockTime, TxUpdate};
@@ -303,50 +304,57 @@ impl From<ConfirmationBlockTime> for ReceiveTo {
303304
// OutPoint { txid, vout: 0 }
304305
// }
305306

306-
// /// Insert a checkpoint into the wallet. This can be used to extend the wallet's local chain
307-
// /// or to insert a block that did not exist previously. Note that if replacing a block with
308-
// /// a different one at the same height, then all later blocks are evicted as well.
309-
// pub fn insert_checkpoint(wallet: &mut Wallet, block: BlockId) {
310-
// let mut cp = wallet.latest_checkpoint();
311-
// cp = cp.insert(block);
312-
// wallet
313-
// .apply_update(Update {
314-
// chain: Some(cp),
315-
// ..Default::default()
316-
// })
317-
// .unwrap();
318-
// }
307+
/// Insert a checkpoint into the wallet. This can be used to extend the wallet's local chain
308+
/// or to insert a block that did not exist previously. Note that if replacing a block with
309+
/// a different one at the same height, then all later blocks are evicted as well.
310+
pub fn insert_checkpoint<K: Ord + Clone + fmt::Debug>(wallet: &mut Wallet<K>, block: BlockId) {
311+
let mut cp = wallet.latest_checkpoint();
312+
cp = cp.insert(block);
313+
wallet
314+
.apply_update(Update {
315+
chain: Some(cp),
316+
..Default::default()
317+
})
318+
.unwrap();
319+
}
319320

320-
// /// Inserts a transaction into the local view, assuming it is currently present in the mempool.
321-
// ///
322-
// /// This can be used, for example, to track a transaction immediately after it is broadcast.
323-
// pub fn insert_tx(wallet: &mut Wallet, tx: Transaction) {
324-
// let txid = tx.compute_txid();
325-
// let seen_at = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
326-
// let mut tx_update = TxUpdate::default();
327-
// tx_update.txs = vec![Arc::new(tx)];
328-
// tx_update.seen_ats = [(txid, seen_at)].into();
329-
// wallet
330-
// .apply_update(Update {
331-
// tx_update,
332-
// ..Default::default()
333-
// })
334-
// .expect("failed to apply update");
335-
// }
321+
/// Inserts a transaction into the local view, assuming it is currently present in the mempool.
322+
///
323+
/// This can be used, for example, to track a transaction immediately after it is broadcast.
324+
pub fn insert_tx<K>(wallet: &mut Wallet<K>, tx: Transaction)
325+
where
326+
K: Ord + fmt::Debug + Clone,
327+
{
328+
let txid = tx.compute_txid();
329+
let seen_at = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
330+
let mut tx_update = TxUpdate::default();
331+
tx_update.txs = vec![Arc::new(tx)];
332+
tx_update.seen_ats = [(txid, seen_at)].into();
333+
wallet
334+
.apply_update(Update {
335+
tx_update,
336+
..Default::default()
337+
})
338+
.expect("failed to apply update");
339+
}
336340

337-
// /// Simulates confirming a tx with `txid` by applying an update to the wallet containing
338-
// /// the given `anchor`. Note: to be considered confirmed the anchor block must exist in
339-
// /// the current active chain.
340-
// pub fn insert_anchor(wallet: &mut Wallet, txid: Txid, anchor: ConfirmationBlockTime) {
341-
// let mut tx_update = TxUpdate::default();
342-
// tx_update.anchors = [(anchor, txid)].into();
343-
// wallet
344-
// .apply_update(Update {
345-
// tx_update,
346-
// ..Default::default()
347-
// })
348-
// .expect("failed to apply update");
349-
// }
341+
/// Simulates confirming a tx with `txid` by applying an update to the wallet containing
342+
/// the given `anchor`. Note: to be considered confirmed the anchor block must exist in
343+
/// the current active chain.
344+
pub fn insert_anchor<K: Ord + fmt::Debug + Clone>(
345+
wallet: &mut Wallet<K>,
346+
txid: Txid,
347+
anchor: ConfirmationBlockTime,
348+
) {
349+
let mut tx_update = TxUpdate::default();
350+
tx_update.anchors = [(anchor, txid)].into();
351+
wallet
352+
.apply_update(Update {
353+
tx_update,
354+
..Default::default()
355+
})
356+
.expect("failed to apply update");
357+
}
350358

351359
// /// Marks the given `txid` seen as unconfirmed at `seen_at`
352360
// pub fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) {

src/wallet/mod.rs

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use bdk_chain::{
3434
SyncResponse,
3535
},
3636
tx_graph::{self, CalculateFeeError, CanonicalTx, TxGraph, TxUpdate},
37-
BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt,
37+
Anchor, BlockId, CanonicalizationParams, ChainPosition, ConfirmationBlockTime, DescriptorExt,
3838
FullTxOut, Indexed, IndexedTxGraph, Indexer, Merge,
3939
};
4040
use bitcoin::{
@@ -571,6 +571,107 @@ where
571571
}
572572
}
573573

574+
impl<K> Wallet<K>
575+
where
576+
K: Ord + Clone + Debug,
577+
{
578+
/// Computes the wallet balance.
579+
pub fn balance_with_params_conf_threshold(
580+
&self,
581+
params: CanonicalizationParams,
582+
outpoints: impl IntoIterator<Item = ((K, u32), OutPoint)>,
583+
conf_threshold: u32,
584+
trust_predicate: impl Fn(&FullTxOut<ConfirmationBlockTime>) -> bool,
585+
) -> Balance {
586+
let mut immature = Amount::ZERO;
587+
let mut trusted_pending = Amount::ZERO;
588+
let mut untrusted_pending = Amount::ZERO;
589+
let mut confirmed = Amount::ZERO;
590+
591+
let chain = &self.chain;
592+
let chain_tip = chain.tip().block_id();
593+
594+
for (_, txo) in self
595+
.tx_graph
596+
.graph()
597+
.filter_chain_unspents(chain, chain_tip, params, outpoints)
598+
{
599+
match &txo.chain_position {
600+
ChainPosition::Confirmed { anchor, .. } => {
601+
let confirmation_height = anchor.confirmation_height_upper_bound();
602+
let confirmations = chain_tip
603+
.height
604+
.saturating_sub(confirmation_height)
605+
.saturating_add(1);
606+
607+
if confirmations < conf_threshold {
608+
if trust_predicate(&txo) {
609+
trusted_pending += txo.txout.value;
610+
} else {
611+
untrusted_pending += txo.txout.value;
612+
}
613+
} else if txo.is_confirmed_and_spendable(chain_tip.height) {
614+
confirmed += txo.txout.value;
615+
} else if !txo.is_mature(chain_tip.height) {
616+
immature += txo.txout.value;
617+
}
618+
}
619+
ChainPosition::Unconfirmed { .. } => {
620+
if trust_predicate(&txo) {
621+
trusted_pending += txo.txout.value;
622+
} else {
623+
untrusted_pending += txo.txout.value;
624+
}
625+
}
626+
}
627+
}
628+
629+
Balance {
630+
immature,
631+
trusted_pending,
632+
untrusted_pending,
633+
confirmed,
634+
}
635+
}
636+
637+
/// Computes the wallet balance.
638+
pub fn balance(&self) -> Balance {
639+
self.balance_with_params_conf_threshold(
640+
CanonicalizationParams::default(),
641+
self.tx_graph.index.outpoints().clone(),
642+
1,
643+
|txo| self.is_tx_trusted(txo.outpoint.txid),
644+
)
645+
}
646+
647+
/// Computes the wallet balance over a keychain range.
648+
pub fn keychain_balance(&self, keychains: impl core::ops::RangeBounds<K>) -> Balance {
649+
self.balance_with_params_conf_threshold(
650+
CanonicalizationParams::default(),
651+
self.tx_graph.index.keychain_outpoints_in_range(keychains),
652+
1,
653+
|txo| self.is_tx_trusted(txo.outpoint.txid),
654+
)
655+
}
656+
657+
/// Whether the transaction of `txid` is considered trusted by this wallet.
658+
///
659+
/// Trust is defined as a tx of which all of the inputs are controlled by the wallet, as we can
660+
/// assume it won't be double-spent unintentionally.
661+
pub fn is_tx_trusted(&self, txid: Txid) -> bool {
662+
let Some(tx) = self.tx_graph.graph().get_tx(txid) else {
663+
return false;
664+
};
665+
if tx.input.is_empty() {
666+
return false;
667+
}
668+
tx.input.iter().all(|txin| {
669+
let outpoint = txin.previous_output;
670+
self.tx_graph.index.txout(outpoint).is_some()
671+
})
672+
}
673+
}
674+
574675
// TODO: replace with `PersistedWallet`
575676
#[cfg(feature = "rusqlite")]
576677
impl<K> Wallet<K>

0 commit comments

Comments
 (0)