Skip to content
Open
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
88 changes: 83 additions & 5 deletions src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ pub struct Wallet {
network: Network,
secp: SecpCtx,
locked_outpoints: HashSet<OutPoint>,
min_output_value: Option<Amount>,
}

/// An update to [`Wallet`].
Expand Down Expand Up @@ -374,6 +375,7 @@ impl Wallet {
stage,
secp,
locked_outpoints,
min_output_value: params.min_output_value,
})
}

Expand Down Expand Up @@ -581,6 +583,7 @@ impl Wallet {
network,
secp,
locked_outpoints,
min_output_value: params.min_output_value,
}))
}

Expand All @@ -589,6 +592,16 @@ impl Wallet {
self.network
}

/// Get the minimum output value, if set.
pub fn min_output_value(&self) -> Option<Amount> {
self.min_output_value
}

/// Set or clear the minimum output value.
pub fn set_min_output_value(&mut self, min_output_value: Option<Amount>) {
self.min_output_value = min_output_value;
}

/// Iterator over all keychains in this wallet
pub fn keychains(&self) -> impl Iterator<Item = (KeychainKind, &ExtendedDescriptor)> {
self.tx_graph.index.keychains()
Expand Down Expand Up @@ -777,7 +790,11 @@ impl Wallet {
}

/// Return the list of unspent outputs of this wallet
///
/// If [`min_output_value`](Wallet::min_output_value) is set, outputs with a value below
/// that threshold are excluded.
pub fn list_unspent(&self) -> impl Iterator<Item = LocalOutput> + '_ {
let min_value = self.min_output_value;
self.tx_graph
.graph()
.filter_chain_unspents(
Expand All @@ -787,6 +804,7 @@ impl Wallet {
self.tx_graph.index.outpoints().iter().cloned(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
.filter(move |utxo| min_value.is_none_or(|threshold| utxo.txout.value >= threshold))
}

/// Get the [`TxDetails`] of a wallet transaction.
Expand All @@ -799,7 +817,7 @@ impl Wallet {
let (sent, received) = self.sent_and_received(&tx.tx_node.tx);
let fee: Option<Amount> = self.calculate_fee(&tx.tx_node.tx).ok();
let fee_rate: Option<FeeRate> = self.calculate_fee_rate(&tx.tx_node.tx).ok();
let balance_delta: SignedAmount = self.tx_graph.index.net_value(&tx.tx_node.tx, ..);
let balance_delta: SignedAmount = self.net_value(&tx.tx_node.tx);
let chain_position = tx.chain_position;

let tx_details: TxDetails = TxDetails {
Expand All @@ -818,8 +836,11 @@ impl Wallet {

/// List all relevant outputs (includes both spent and unspent, confirmed and unconfirmed).
///
/// If [`min_output_value`](Wallet::min_output_value) is set, outputs with a value below
/// that threshold are excluded.
/// To list only unspent outputs (UTXOs), use [`Wallet::list_unspent`] instead.
pub fn list_output(&self) -> impl Iterator<Item = LocalOutput> + '_ {
let min_value = self.min_output_value;
self.tx_graph
.graph()
.filter_chain_txouts(
Expand All @@ -829,6 +850,7 @@ impl Wallet {
self.tx_graph.index.outpoints().iter().cloned(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
.filter(move |output| min_value.is_none_or(|threshold| output.txout.value >= threshold))
}

/// Get all the checkpoints the wallet is currently storing indexed by height.
Expand Down Expand Up @@ -976,6 +998,8 @@ impl Wallet {
/// This method returns a tuple `(sent, received)`. Sent is the sum of the txin amounts
/// that spend from previous txouts tracked by this wallet. Received is the summation
/// of this tx's outputs that send to script pubkeys tracked by this wallet.
/// If [`min_output_value`](Wallet::min_output_value) is set, outputs with a value below
/// that threshold are excluded from both `sent` and `received`.
///
/// # Examples
///
Expand All @@ -997,7 +1021,36 @@ impl Wallet {
/// let (sent, received) = wallet.sent_and_received(tx);
/// ```
pub fn sent_and_received(&self, tx: &Transaction) -> (Amount, Amount) {
self.tx_graph.index.sent_and_received(tx, ..)
let (mut sent, mut received) = self.tx_graph.index.sent_and_received(tx, ..);

// filter out outputs below the min_output_value threshold
if let Some(threshold) = self.min_output_value {
for spent in self.tx_graph.index.spent_txouts(tx) {
if spent.txout.value < threshold {
sent -= spent.txout.value;
}
}
for created in self.tx_graph.index.created_txouts(tx) {
if created.txout.value < threshold {
received -= created.txout.value;
}
}
}

(sent, received)
}

/// Compute the net value transferred to/from this wallet by `tx`.
///
/// This is equivalent to `received - sent` from [`sent_and_received`],
/// and respects [`min_output_value`] if set.
///
/// [`sent_and_received`]: Wallet::sent_and_received
/// [`min_output_value`]: Wallet::min_output_value
pub fn net_value(&self, tx: &Transaction) -> SignedAmount {
let (sent, received) = self.sent_and_received(tx);
received.to_signed().expect("valid `SignedAmount`")
- sent.to_signed().expect("valid `SignedAmount`")
}

/// Get a single transaction from the wallet as a [`WalletTx`] (if the transaction exists).
Expand Down Expand Up @@ -1114,12 +1167,32 @@ impl Wallet {

/// Return the balance, separated into available, trusted-pending, untrusted-pending, and
/// immature values.
///
/// If [`min_output_value`](Wallet::min_output_value) is set, outputs with a value below
/// that threshold are excluded from the balance.
pub fn balance(&self) -> Balance {
self.tx_graph.graph().balance(
let graph = self.tx_graph.graph();
let chain_tip = self.chain.tip().block_id();
let min_value = self.min_output_value;

let outpoints = self
.tx_graph
.index
.outpoints()
.iter()
.filter(|(_, op)| match min_value {
None => true,
Some(threshold) => graph
.get_txout(*op)
.is_none_or(|txout| txout.value >= threshold),
})
.cloned();

graph.balance(
&self.chain,
self.chain.tip().block_id(),
chain_tip,
CanonicalizationParams::default(),
self.tx_graph.index.outpoints().iter().cloned(),
outpoints,
|&(k, _), _| k == KeychainKind::Internal,
)
}
Expand Down Expand Up @@ -2001,6 +2074,11 @@ impl Wallet {
)
// Filter out locked outpoints.
.filter(|(_, txo)| !self.is_outpoint_locked(txo.outpoint))
// Filter out outputs below `min_output_value`, if set.
.filter(|(_, txo)| {
self.min_output_value
.is_none_or(|threshold| txo.txout.value >= threshold)
})
// Only create LocalOutput if UTxO is mature.
.filter_map(move |((k, i), full_txo)| {
full_txo
Expand Down
49 changes: 48 additions & 1 deletion src/wallet/params.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use alloc::boxed::Box;

use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD;
use bitcoin::{BlockHash, Network, NetworkKind};
use bitcoin::{Amount, BlockHash, Network, NetworkKind};
use miniscript::descriptor::KeyMap;

use crate::{
Expand Down Expand Up @@ -67,6 +67,7 @@ pub struct CreateParams {
pub(crate) genesis_hash: Option<BlockHash>,
pub(crate) lookahead: u32,
pub(crate) use_spk_cache: bool,
pub(crate) min_output_value: Option<Amount>,
}

impl CreateParams {
Expand All @@ -90,6 +91,7 @@ impl CreateParams {
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
use_spk_cache: false,
min_output_value: None,
}
}

Expand All @@ -112,6 +114,7 @@ impl CreateParams {
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
use_spk_cache: false,
min_output_value: None,
}
}

Expand All @@ -137,6 +140,7 @@ impl CreateParams {
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
use_spk_cache: false,
min_output_value: None,
}
}

Expand Down Expand Up @@ -182,6 +186,25 @@ impl CreateParams {
self
}

/// Use a minimum output value for the wallet.
///
/// Outputs with a value below this threshold are ignored by the wallet, similar to
/// a custom dust limit.
///
/// **WARNING:** This affects [`balance`], [`list_unspent`], [`list_output`],
/// [`sent_and_received`], [`net_value`], and coin selection. Outputs below this value
/// will not appear in these methods and will not be selected for spending.
///
/// [`balance`]: Wallet::balance
/// [`list_unspent`]: Wallet::list_unspent
/// [`list_output`]: Wallet::list_output
/// [`sent_and_received`]: Wallet::sent_and_received
/// [`net_value`]: Wallet::net_value
pub fn min_output_value(mut self, value: Amount) -> Self {
self.min_output_value = Some(value);
self
}

/// Create [`PersistedWallet`] with the given [`WalletPersister`].
pub fn create_wallet<P>(
self,
Expand Down Expand Up @@ -222,6 +245,7 @@ pub struct LoadParams {
pub(crate) check_change_descriptor: Option<Option<DescriptorToExtract>>,
pub(crate) extract_keys: bool,
pub(crate) use_spk_cache: bool,
pub(crate) min_output_value: Option<Amount>,
}

impl LoadParams {
Expand All @@ -239,6 +263,7 @@ impl LoadParams {
check_change_descriptor: None,
extract_keys: false,
use_spk_cache: false,
min_output_value: None,
}
}

Expand Down Expand Up @@ -309,6 +334,28 @@ impl LoadParams {
self
}

/// Use a custom minimum output value for the wallet.
///
/// Outputs with a value below this threshold are ignored by the wallet, similar to
/// a custom dust limit. Unlike the standard dust limit, this is a
/// user-defined threshold that applies to balance, output listing, and tx construction.
///
/// **WARNING:** This affects [`balance`], [`list_unspent`], [`list_output`],
/// [`sent_and_received`], [`net_value`], and coin selection. Outputs below this value
/// will not appear in these methods and will not be selected for spending.
/// [`get_utxo`] and fee calculations are not affected.
///
/// [`balance`]: Wallet::balance
/// [`list_unspent`]: Wallet::list_unspent
/// [`list_output`]: Wallet::list_output
/// [`sent_and_received`]: Wallet::sent_and_received
/// [`net_value`]: Wallet::net_value
/// [`get_utxo`]: Wallet::get_utxo
pub fn min_output_value(mut self, value: Amount) -> Self {
self.min_output_value = Some(value);
self
}

/// Load [`PersistedWallet`] with the given [`WalletPersister`].
pub fn load_wallet<P>(
self,
Expand Down
Loading