recv_chain.tip().block_id(),
Default::default(),
)
- .balance(outpoints, |_, _| true, 0);
+ .balance(
+ outpoints.into_iter().map(|(_, op)| op),
+ bdk_chain::taints_unowned(&recv_graph.index),
+ |pos| pos.is_confirmed(),
+ );
Ok(balance)
}
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
- trusted_pending: SEND_AMOUNT * reorg_count as u64,
+ untrusted_pending: SEND_AMOUNT * reorg_count as u64,
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
..Balance::default()
},
let op = graph.index.outpoints().clone();
let bal = chain
.canonical_view(graph.graph(), chain.tip().block_id(), Default::default())
- .balance(op, |_, _| false, 1);
+ .balance(
+ op.into_iter().map(|(_, o)| o),
+ bdk_chain::taints_unowned(&graph.index),
+ |pos| pos.is_confirmed(),
+ );
assert_eq!(bal.total(), AMOUNT * TX_CT as u64);
}
use core::{fmt, ops::RangeBounds};
use bdk_core::BlockId;
-use bitcoin::{
- constants::COINBASE_MATURITY, Amount, OutPoint, ScriptBuf, Transaction, TxOut, Txid,
-};
+use bitcoin::{constants::COINBASE_MATURITY, OutPoint, ScriptBuf, Transaction, TxOut, Txid};
use crate::{spk_txout::SpkTxOutIndex, Anchor, Balance, CanonicalViewTask, ChainPosition, TxGraph};
/// * `does_taint` - Returns `true` for a transaction that pulls in untrusted funds (e.g. it
/// spends an output the wallet doesn't own). It drives the [`Trust`] of an unsettled output:
/// a tainting transaction in its ancestry makes it [`Untrusted`](Trust::Untrusted). Outputs
- /// with missing ancestry stay [`Unknown`](Trust::Unknown) regardless of this predicate.
+ /// with missing ancestry stay [`Unknown`](Trust::Unknown) regardless of this predicate. Use
+ /// [`taints_unowned`] to classify everything foreign as untrusted.
/// * `is_settled` - Returns `true` for the [position](ChainPosition) of a transaction we
/// consider settled (unlikely to be replaced), for example one with enough confirmations.
pub fn classify_outpoints<'a>(
// `Enter`: if tx is unsettled and not directly tainted, queue its parents.
// `Exit`: by now every parent is resolved, so the tx is tainted if any parent is.
- enum Frame<A: Anchor> {
+ enum Frame<A> {
Enter(Txid),
Exit(CanonicalTx<ChainPosition<A>>),
}
let parent_trust = cache
.get(&txin.previous_output.txid)
.copied()
- .unwrap_or(Trust::Trusted);
+ .expect("parent transaction should already be cached");
trust = match (trust, parent_trust) {
(Trust::Untrusted, _) | (_, Trust::Untrusted) => Trust::Untrusted,
(Trust::Unknown, _) | (_, Trust::Unknown) => Trust::Unknown,
cache[&seed_txid]
}
- /// Calculate the total balance of the given outpoints.
+ /// Calculate the total [`Balance`] of the given outpoints.
///
- /// This method computes a detailed balance breakdown for a set of outpoints, categorizing
- /// outputs as confirmed, pending (trusted/untrusted), or immature based on their chain
- /// position and the provided trust predicate.
+ /// This is a fold over [`classify_outpoints`](Self::classify_outpoints): each output's value is
+ /// added to the bucket matching its [`Eligibility`].
///
- /// # Arguments
- ///
- /// * `outpoints` - Iterator of `(identifier, outpoint)` pairs to calculate balance for
- /// * `trust_predicate` - Function that returns `true` for trusted scripts. Trusted outputs
- /// count toward `trusted_pending` balance, while untrusted ones count toward
- /// `untrusted_pending`
- /// * `min_confirmations` - Minimum confirmations required for an output to be considered
- /// confirmed. Outputs with fewer confirmations are treated as pending.
- ///
- /// # Minimum Confirmations
- ///
- /// The `min_confirmations` parameter controls when outputs are considered confirmed. A
- /// `min_confirmations` value of `0` is equivalent to `1` (require at least 1 confirmation).
- ///
- /// Outputs with fewer than `min_confirmations` are categorized as pending (trusted or
- /// untrusted based on the trust predicate).
+ /// See `classify_outpoints` for `does_taint` and `is_settled` meaning.
///
/// # Example
///
/// ```
- /// # use bdk_chain::{CanonicalParams, TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
+ /// # use bdk_chain::{CanonicalParams, ChainPosition, TxGraph, local_chain::LocalChain, keychain_txout::KeychainTxOutIndex};
/// # use bdk_core::BlockId;
/// # use bitcoin::hashes::Hash;
/// # let tx_graph = TxGraph::<BlockId>::default();
/// # let chain_tip = chain.tip().block_id();
/// # let view = chain.canonical_view(&tx_graph, chain_tip, CanonicalParams::default());
/// # let indexer = KeychainTxOutIndex::<&str>::default();
- /// // Calculate balance with 6 confirmations, trusting all outputs
+ /// let tip_height = view.tip().height;
+ /// // Calculate balance requiring 6 confirmations.
/// let balance = view.balance(
- /// indexer.outpoints().into_iter().map(|(k, op)| (k.clone(), *op)),
- /// |_keychain, _script| true, // Trust all outputs
- /// 6, // Require 6 confirmations
+ /// indexer.outpoints().iter().map(|(_, op)| *op),
+ /// bdk_chain::taints_unowned(&indexer),
+ /// |pos: &ChainPosition<_>| {
+ /// pos.confirmation_height_upper_bound()
+ /// .is_some_and(|h| tip_height.saturating_sub(h).saturating_add(1) >= 6)
+ /// },
/// );
/// ```
- pub fn balance<'v, O: Clone + 'v>(
- &'v self,
- outpoints: impl IntoIterator<Item = (O, OutPoint)> + 'v,
- mut trust_predicate: impl FnMut(&O, &CanonicalTxOut<ChainPosition<A>>) -> bool,
- min_confirmations: u32,
+ pub fn balance(
+ &self,
+ outpoints: impl IntoIterator<Item = OutPoint>,
+ does_taint: impl FnMut(&CanonicalTx<ChainPosition<A>>) -> bool,
+ is_settled: impl Fn(&ChainPosition<A>) -> bool,
) -> Balance {
- let mut immature = Amount::ZERO;
- let mut trusted_pending = Amount::ZERO;
- let mut untrusted_pending = Amount::ZERO;
- let mut confirmed = Amount::ZERO;
-
- for (spk_i, txout) in self.filter_unspent_outpoints(outpoints) {
- match &txout.pos {
- ChainPosition::Confirmed { anchor, .. } => {
- let confirmation_height = anchor.confirmation_height_upper_bound();
- let confirmations = self
- .tip
- .height
- .saturating_sub(confirmation_height)
- .saturating_add(1);
- let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically
-
- if confirmations < min_confirmations {
- // Not enough confirmations, treat as trusted/untrusted pending
- if trust_predicate(&spk_i, &txout) {
- trusted_pending += txout.txout.value;
- } else {
- untrusted_pending += txout.txout.value;
- }
- } else if txout.is_confirmed_and_spendable(self.tip.height) {
- confirmed += txout.txout.value;
- } else if !txout.is_mature(self.tip.height) {
- immature += txout.txout.value;
- }
- }
- ChainPosition::Unconfirmed { .. } => {
- if trust_predicate(&spk_i, &txout) {
- trusted_pending += txout.txout.value;
- } else {
- untrusted_pending += txout.txout.value;
- }
+ self.classify_outpoints(outpoints, does_taint, is_settled)
+ .collect()
+ }
+}
+
+impl<A: Anchor> FromIterator<(CanonicalTxOut<ChainPosition<A>>, Eligibility)> for Balance {
+ /// Sums each output's value into the [`Balance`] bucket matching its [`Eligibility`].
+ fn from_iter<I: IntoIterator<Item = (CanonicalTxOut<ChainPosition<A>>, Eligibility)>>(
+ iter: I,
+ ) -> Self {
+ let mut balance = Balance::default();
+ for (txout, eligibility) in iter {
+ let bucket = match eligibility {
+ Eligibility::Immature => &mut balance.immature,
+ Eligibility::Settled => &mut balance.confirmed,
+ Eligibility::Unsettled(Trust::Trusted) => &mut balance.trusted_pending,
+ Eligibility::Unsettled(Trust::Untrusted | Trust::Unknown) => {
+ &mut balance.untrusted_pending
}
- }
+ };
+ *bucket += txout.txout.value;
}
+ balance
+ }
+}
- Balance {
- immature,
- trusted_pending,
- untrusted_pending,
- confirmed,
- }
+/// A `does_taint` predicate: a transaction taints when any input spends a coin this indexer
+/// doesn't recognise as ours, including one whose parent we've never seen.
+pub fn taints_unowned<'a, P, I>(
+ indexer: &'a impl AsRef<SpkTxOutIndex<I>>,
+) -> impl Fn(&CanonicalTx<P>) -> bool + 'a
+where
+ I: fmt::Debug + Clone + Ord + 'a,
+{
+ let indexer = indexer.as_ref();
+ move |c_tx| {
+ c_tx.tx.input.iter().any(|txin| {
+ !txin.previous_output.is_null() && indexer.txout(txin.previous_output).is_none()
+ })
}
}
use std::collections::BTreeMap;
-use bdk_chain::{local_chain::LocalChain, BlockId, ConfirmationBlockTime, TxGraph};
+use bdk_chain::{local_chain::LocalChain, BlockId, ChainPosition, ConfirmationBlockTime, TxGraph};
use bdk_testenv::{hash, utils::new_tx};
use bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
+/// Builds an `is_settled` predicate requiring at least `min_confirmations` confirmations.
+fn settled(
+ tip_height: u32,
+ min_confirmations: u32,
+) -> impl Fn(&ChainPosition<ConfirmationBlockTime>) -> bool {
+ let min_confirmations = min_confirmations.max(1); // 0 and 1 behave identically
+ move |pos| {
+ pos.confirmation_height_upper_bound()
+ .is_some_and(|h| tip_height.saturating_sub(h).saturating_add(1) >= min_confirmations)
+ }
+}
+
#[test]
-fn test_min_confirmations_parameter() {
+fn test_is_settled_boundary() {
// Create a local chain with several blocks
let blocks: BTreeMap<u32, BlockHash> = [
(0, hash!("block0")),
let canonical_view =
chain.canonical_view(&tx_graph, chain.tip().block_id(), Default::default());
+ let tip_height = canonical_view.tip().height;
// Test min_confirmations = 1: Should be confirmed (has 6 confirmations)
let balance_1_conf = canonical_view.balance(
- [((), outpoint)],
- |_, _| true, // trust all
- 1,
+ [outpoint],
+ |_tx| false, // leave trust to ancestry
+ settled(tip_height, 1),
);
assert_eq!(balance_1_conf.confirmed, Amount::from_sat(50_000));
// Test min_confirmations = 6: Should be confirmed (has exactly 6 confirmations)
let balance_6_conf = canonical_view.balance(
- [((), outpoint)],
- |_, _| true, // trust all
- 6,
+ [outpoint],
+ |_tx| false, // leave trust to ancestry
+ settled(tip_height, 6),
);
assert_eq!(balance_6_conf.confirmed, Amount::from_sat(50_000));
assert_eq!(balance_6_conf.trusted_pending, Amount::ZERO);
// Test min_confirmations = 7: Should be trusted pending (only has 6 confirmations)
let balance_7_conf = canonical_view.balance(
- [((), outpoint)],
- |_, _| true, // trust all
- 7,
+ [outpoint],
+ |_tx| false, // leave trust to ancestry
+ settled(tip_height, 7),
);
assert_eq!(balance_7_conf.confirmed, Amount::ZERO);
assert_eq!(balance_7_conf.trusted_pending, Amount::from_sat(50_000));
-
- // Test min_confirmations = 0: Should behave same as 1 (confirmed)
- let balance_0_conf = canonical_view.balance(
- [((), outpoint)],
- |_, _| true, // trust all
- 0,
- );
- assert_eq!(balance_0_conf.confirmed, Amount::from_sat(50_000));
- assert_eq!(balance_0_conf.trusted_pending, Amount::ZERO);
- assert_eq!(balance_0_conf, balance_1_conf);
}
#[test]
let mut tx_graph = TxGraph::default();
+ // A settled parent, so ancestry alone would make the child trusted and `does_taint` is what
+ // decides the outcome.
+ let parent = Transaction {
+ input: vec![TxIn {
+ previous_output: OutPoint::new(hash!("root"), 0),
+ ..Default::default()
+ }],
+ output: vec![TxOut {
+ value: Amount::from_sat(25_000),
+ script_pubkey: ScriptBuf::new(),
+ }],
+ ..new_tx(0)
+ };
+ let parent_txid = parent.compute_txid();
+ let _ = tx_graph.insert_tx(parent.clone());
+ let _ = tx_graph.insert_anchor(
+ parent_txid,
+ ConfirmationBlockTime {
+ block_id: chain.get(1).unwrap().block_id(),
+ confirmation_time: 100,
+ },
+ );
+
// Create a transaction
let tx = Transaction {
input: vec![TxIn {
- previous_output: OutPoint::new(hash!("parent"), 0),
+ previous_output: OutPoint::new(parent_txid, 0),
..Default::default()
}],
output: vec![TxOut {
let canonical_view =
chain.canonical_view(&tx_graph, chain.tip().block_id(), Default::default());
+ let tip_height = canonical_view.tip().height;
- // Test with min_confirmations = 5 and untrusted predicate
+ // Test with min_confirmations = 5 and everything tainted
let balance = canonical_view.balance(
- [((), outpoint)],
- |_, _| false, // don't trust
- 5,
+ [outpoint],
+ |_tx| true, // taint everything
+ settled(tip_height, 5),
);
// Should be untrusted pending (not enough confirmations and not trusted)
assert_eq!(balance.confirmed, Amount::ZERO);
assert_eq!(balance.trusted_pending, Amount::ZERO);
assert_eq!(balance.untrusted_pending, Amount::from_sat(25_000));
+
+ // Without the taint, the settled ancestry makes it trusted instead.
+ let balance = canonical_view.balance(
+ [outpoint],
+ |_tx| false, // leave trust to ancestry
+ settled(tip_height, 5),
+ );
+ assert_eq!(balance.trusted_pending, Amount::from_sat(25_000));
+ assert_eq!(balance.untrusted_pending, Amount::ZERO);
}
#[test]
confirmation_time: 123456,
},
);
- outpoints.push(((), outpoint0));
+ outpoints.push(outpoint0);
// Transaction 1: anchored at height 10, has 6 confirmations (15-10+1 = 6)
let tx1 = Transaction {
confirmation_time: 123457,
},
);
- outpoints.push(((), outpoint1));
+ outpoints.push(outpoint1);
// Transaction 2: anchored at height 13, has 3 confirmations (15-13+1 = 3)
let tx2 = Transaction {
confirmation_time: 123458,
},
);
- outpoints.push(((), outpoint2));
+ outpoints.push(outpoint2);
let canonical_view =
chain.canonical_view(&tx_graph, chain.tip().block_id(), Default::default());
+ let tip_height = canonical_view.tip().height;
// Test with min_confirmations = 5
// tx0: 11 confirmations -> confirmed
// tx1: 6 confirmations -> confirmed
// tx2: 3 confirmations -> trusted pending
- let balance = canonical_view.balance(outpoints.clone(), |_, _| true, 5);
+ let balance = canonical_view.balance(outpoints.clone(), |_tx| false, settled(tip_height, 5));
assert_eq!(
balance.confirmed,
// tx0: 11 confirmations -> confirmed
// tx1: 6 confirmations -> trusted pending
// tx2: 3 confirmations -> trusted pending
- let balance_high = canonical_view.balance(outpoints, |_, _| true, 10);
+ let balance_high = canonical_view.balance(outpoints, |_tx| false, settled(tip_height, 10));
assert_eq!(
balance_high.confirmed,
///
/// Keychains:
///
-/// keychain_1: Trusted
-/// keychain_2: Untrusted
+/// keychain_1 and keychain_2 are owned.
///
/// Transactions:
///
-/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
-/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
-/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
-/// tx4: Mempool tx, sending 20000 sats to "untrusted" address.
-/// tx5: Mempool tx, sending 15000 sats to "trusted" address.
+/// tx1: A Coinbase, sending 70000 sats to a keychain_1 address. [Block 0]
+/// tx2: A external Receive, sending 30000 sats to a keychain_2 address. [Block 1]
+/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to a keychain_1 address. [Block 2]
+/// tx4: Mempool tx, sending 20000 sats to a keychain_2 address.
+/// tx5: Mempool tx, sending 15000 sats to a keychain_1 address.
/// tx6: Complete unrelated tx. [Block 3]
///
/// Different transactions are added via `insert_relevant_txs`.
indexer
});
- // Get trusted and untrusted addresses
+ // Get addresses for both keychains
- let mut trusted_spks: Vec<ScriptBuf> = Vec::new();
- let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
+ let mut keychain_1_spks: Vec<ScriptBuf> = Vec::new();
+ let mut keychain_2_spks: Vec<ScriptBuf> = Vec::new();
{
// we need to scope here to take immutable reference of the graph
.reveal_next_spk("keychain_1".to_string())
.unwrap();
// TODO Assert indexes
- trusted_spks.push(script.to_owned());
+ keychain_1_spks.push(script.to_owned());
}
}
{
.index
.reveal_next_spk("keychain_2".to_string())
.unwrap();
- untrusted_spks.push(script.to_owned());
+ keychain_2_spks.push(script.to_owned());
}
}
}],
output: vec![TxOut {
value: Amount::from_sat(70000),
- script_pubkey: trusted_spks[0].to_owned(),
+ script_pubkey: keychain_1_spks[0].to_owned(),
}],
..new_tx(1)
};
- // tx2 is an incoming transaction received at untrusted keychain at block 1.
+ // tx2 is an incoming transaction received at keychain_2 at block 1.
let tx2 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(30000),
- script_pubkey: untrusted_spks[0].to_owned(),
+ script_pubkey: keychain_2_spks[0].to_owned(),
}],
..new_tx(2)
};
- // tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
+ // tx3 spends tx2 and gives a change back in keychain_1. Confirmed at Block 2.
let tx3 = Transaction {
input: vec![TxIn {
previous_output: OutPoint::new(tx2.compute_txid(), 0),
}],
output: vec![TxOut {
value: Amount::from_sat(10000),
- script_pubkey: trusted_spks[1].to_owned(),
+ script_pubkey: keychain_1_spks[1].to_owned(),
}],
..new_tx(3)
};
- // tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
+ // tx4 is unconfirmed and pays one of our addresses, but it spends a third-party
+ // coin we don't own. That foreign input is what makes its ancestry untrusted.
let tx4 = Transaction {
+ input: vec![TxIn {
+ // A coin outside our wallet, never inserted into the graph.
+ previous_output: OutPoint::new(new_tx(40).compute_txid(), 0),
+ ..Default::default()
+ }],
output: vec![TxOut {
value: Amount::from_sat(20000),
- script_pubkey: untrusted_spks[1].to_owned(),
+ script_pubkey: keychain_2_spks[1].to_owned(),
}],
..new_tx(4)
};
- // tx5 is an external transaction receiving at trusted keychain, unconfirmed.
+ // tx5 is an external transaction receiving at keychain_1, unconfirmed.
let tx5 = Transaction {
output: vec![TxOut {
value: Amount::from_sat(15000),
- script_pubkey: trusted_spks[2].to_owned(),
+ script_pubkey: keychain_1_spks[2].to_owned(),
}],
..new_tx(5)
};
.collect::<Vec<_>>();
let balance = canonical_view.balance(
- graph.index.outpoints().iter().cloned(),
- |_, txout| trusted_spks.contains(&txout.txout.script_pubkey),
- 0,
+ graph.index.outpoints().iter().map(|(_, op)| *op),
+ bdk_chain::taints_unowned(&graph.index),
+ |pos| pos.is_confirmed(),
);
let confirmed_txouts_txid = txouts
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3, tx5
untrusted_pending: Amount::from_sat(20000), // tx4
- confirmed: Amount::ZERO // Nothing is confirmed yet
+ ..Default::default()
}
);
}
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(25000), // tx3, tx5
untrusted_pending: Amount::from_sat(20000), // tx4
- confirmed: Amount::from_sat(0) // tx2 got confirmed (but spent by 3)
+ confirmed: Amount::from_sat(0), // tx2 got confirmed (but spent by 3)
}
);
}
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
- confirmed: Amount::from_sat(10000) // tx3 got confirmed
+ confirmed: Amount::from_sat(10000), // tx3 got confirmed
}
);
}
immature: Amount::from_sat(70000), // immature coinbase
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
- confirmed: Amount::from_sat(10000) // tx3 is confirmed
+ confirmed: Amount::from_sat(10000), // tx3 is confirmed
}
);
}
assert_eq!(
balance,
Balance {
- immature: Amount::ZERO, // coinbase matured
trusted_pending: Amount::from_sat(15000), // tx5
untrusted_pending: Amount::from_sat(20000), // tx4
- confirmed: Amount::from_sat(80000) // tx1 + tx3
+ confirmed: Amount::from_sat(80000), // tx1 + tx3
+ ..Default::default()
}
);
}
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(40000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::from_sat(20000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ untrusted_pending: Amount::from_sat(20000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
exp_unspents: HashSet::from([("C", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::from_sat(30000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ untrusted_pending: Amount::from_sat(30000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(20000),
+ ..Default::default()
},
},
Scenario {
]),
exp_unspents: HashSet::from([("C", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(30000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
exp_unspents: HashSet::from([("B'", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(50000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("first", 0), ("second", 0), ("anchored", 0)]),
exp_unspents: HashSet::from([("anchored", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(800),
+ ..Default::default()
}
},
Scenario {
exp_chain_txs: HashSet::from(["root", "tx"]),
exp_chain_txouts: HashSet::from([("tx", 0)]),
exp_unspents: HashSet::from([("tx", 0)]),
- exp_balance: Balance { trusted_pending: Amount::from_sat(9000), ..Default::default() }
+ exp_balance: Balance { untrusted_pending: Amount::from_sat(9000), ..Default::default() }
},
Scenario {
name: "tx spends from 2 conflicting transactions where a conflict spends another",
exp_chain_txs: HashSet::from(["A", "S1", "B"]),
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("S1", 0)]),
exp_unspents: HashSet::from([("B", 0)]),
- exp_balance: Balance { trusted_pending: Amount::from_sat(8_000), ..Default::default() },
+ exp_balance: Balance { untrusted_pending: Amount::from_sat(8_000), ..Default::default() },
},
Scenario {
name: "tx spends from 2 conflicting transactions where the conflict is nested (different last_seens)",
exp_chain_txs: HashSet::from(["A", "S1", "B"]),
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("S1", 0)]),
exp_unspents: HashSet::from([("B", 0)]),
- exp_balance: Balance { trusted_pending: Amount::from_sat(8_000), ..Default::default() },
+ exp_balance: Balance { untrusted_pending: Amount::from_sat(8_000), ..Default::default() },
},
Scenario {
name: "assume-canonical-tx displaces unconfirmed chain",
exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]),
exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(19_000),
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(21_000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("root", 0), ("root", 1), ("assume_canonical", 0)]),
exp_unspents: HashSet::from([("root", 1), ("assume_canonical", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(19_000),
- untrusted_pending: Amount::ZERO,
confirmed: Amount::from_sat(21_000),
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([("root", 0), ("assume_c", 0)]),
exp_unspents: HashSet::from([("assume_c", 0)]),
exp_balance: Balance {
- immature: Amount::ZERO,
trusted_pending: Amount::from_sat(18_000),
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
},
},
Scenario {
exp_chain_txouts: HashSet::from([]),
exp_unspents: HashSet::from([]),
exp_balance: Balance {
- immature: Amount::ZERO,
- trusted_pending: Amount::ZERO,
- untrusted_pending: Amount::ZERO,
- confirmed: Amount::ZERO,
+ ..Default::default()
}
}
];
);
let balance = canonical_view.balance(
- env.indexer.outpoints().iter().cloned(),
- |_, txout| {
- env.indexer
- .index_of_spk(txout.txout.script_pubkey.as_script())
- .is_some()
- },
- 0,
+ env.indexer.outpoints().iter().map(|(_, op)| *op),
+ bdk_chain::taints_unowned(&env.indexer),
+ |pos| pos.is_confirmed(),
);
assert_eq!(
balance, scenario.exp_balance,
recv_chain.tip().block_id(),
Default::default(),
)
- .balance(outpoints, |_, _| true, 0);
+ .balance(
+ outpoints.into_iter().map(|(_, op)| op),
+ bdk_chain::taints_unowned(&recv_graph.index),
+ |pos| pos.is_confirmed(),
+ );
Ok(balance)
}
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
- trusted_pending: SEND_AMOUNT,
+ untrusted_pending: SEND_AMOUNT,
..Balance::default()
},
"balance must be correct",
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
- trusted_pending: SEND_AMOUNT,
+ untrusted_pending: SEND_AMOUNT,
..Balance::default()
},
);
assert_eq!(
get_balance(&recv_chain, &recv_graph)?,
Balance {
- trusted_pending: SEND_AMOUNT * depth as u64,
+ untrusted_pending: SEND_AMOUNT * depth as u64,
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
..Balance::default()
},