From: 志宇 Date: Wed, 22 Jul 2026 16:35:37 +0000 (+0200) Subject: feat(chain)!: derive balance trust from output ancestry X-Git-Url: http://internal-gitweb-vhost/-client-interface/%22https:/bdk/descriptor/enum.Descriptor.html?a=commitdiff_plain;h=3f7c1bde852826cd3df0a12dd0ae8023a6f0bb07;p=bdk feat(chain)!: derive balance trust from output ancestry balance becomes a fold over classify_outpoints. A pending output's trust is now taken from its unsettled ancestry (untrusted if an unsettled ancestor taints or is missing from the view), not from a per-output flag. BREAKING CHANGE: balance takes does_taint and is_settled instead of trust_predicate and min_confirmations, and plain OutPoints instead of (identifier, outpoint) pairs. Co-authored-by: Dmenec Co-Authored-By: Claude Opus 4.8 (1M context) --- diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 2aeede62..f1853cfa 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -325,7 +325,11 @@ fn get_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) } @@ -409,7 +413,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { 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() }, diff --git a/crates/chain/benches/indexer.rs b/crates/chain/benches/indexer.rs index 97917ce3..1cf7a61a 100644 --- a/crates/chain/benches/indexer.rs +++ b/crates/chain/benches/indexer.rs @@ -85,7 +85,11 @@ fn do_bench(indexed_tx_graph: &KeychainTxGraph, chain: &LocalChain) { 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); } diff --git a/crates/chain/src/canonical.rs b/crates/chain/src/canonical.rs index fc1db415..20603c8d 100644 --- a/crates/chain/src/canonical.rs +++ b/crates/chain/src/canonical.rs @@ -28,9 +28,7 @@ use alloc::vec::Vec; 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}; @@ -434,7 +432,8 @@ impl CanonicalView { /// * `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>( @@ -493,7 +492,7 @@ impl CanonicalView { // `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 { + enum Frame { Enter(Txid), Exit(CanonicalTx>), } @@ -545,7 +544,7 @@ impl CanonicalView { 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, @@ -564,33 +563,17 @@ impl CanonicalView { 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::::default(); @@ -598,64 +581,62 @@ impl CanonicalView { /// # 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 + 'v, - mut trust_predicate: impl FnMut(&O, &CanonicalTxOut>) -> bool, - min_confirmations: u32, + pub fn balance( + &self, + outpoints: impl IntoIterator, + does_taint: impl FnMut(&CanonicalTx>) -> bool, + is_settled: impl Fn(&ChainPosition) -> 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 FromIterator<(CanonicalTxOut>, Eligibility)> for Balance { + /// Sums each output's value into the [`Balance`] bucket matching its [`Eligibility`]. + fn from_iter>, 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>, +) -> impl Fn(&CanonicalTx

) -> 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() + }) } } diff --git a/crates/chain/tests/test_canonical_view.rs b/crates/chain/tests/test_canonical_view.rs index 09693c32..ed84d2f4 100644 --- a/crates/chain/tests/test_canonical_view.rs +++ b/crates/chain/tests/test_canonical_view.rs @@ -2,12 +2,24 @@ 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) -> 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 = [ (0, hash!("block0")), @@ -55,12 +67,13 @@ fn test_min_confirmations_parameter() { 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)); @@ -68,31 +81,21 @@ fn test_min_confirmations_parameter() { // 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] @@ -117,10 +120,33 @@ fn test_min_confirmations_with_untrusted_tx() { 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 { @@ -143,18 +169,28 @@ fn test_min_confirmations_with_untrusted_tx() { 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] @@ -209,7 +245,7 @@ fn test_min_confirmations_multiple_transactions() { 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 { @@ -233,7 +269,7 @@ fn test_min_confirmations_multiple_transactions() { 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 { @@ -257,16 +293,17 @@ fn test_min_confirmations_multiple_transactions() { 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, @@ -282,7 +319,7 @@ fn test_min_confirmations_multiple_transactions() { // 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, diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index dd0c9789..4ff40f99 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -374,16 +374,15 @@ fn reindex_reaches_fixed_point() { /// /// 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`. @@ -418,10 +417,10 @@ fn test_list_owned_txouts() { indexer }); - // Get trusted and untrusted addresses + // Get addresses for both keychains - let mut trusted_spks: Vec = Vec::new(); - let mut untrusted_spks: Vec = Vec::new(); + let mut keychain_1_spks: Vec = Vec::new(); + let mut keychain_2_spks: Vec = Vec::new(); { // we need to scope here to take immutable reference of the graph @@ -431,7 +430,7 @@ fn test_list_owned_txouts() { .reveal_next_spk("keychain_1".to_string()) .unwrap(); // TODO Assert indexes - trusted_spks.push(script.to_owned()); + keychain_1_spks.push(script.to_owned()); } } { @@ -440,7 +439,7 @@ fn test_list_owned_txouts() { .index .reveal_next_spk("keychain_2".to_string()) .unwrap(); - untrusted_spks.push(script.to_owned()); + keychain_2_spks.push(script.to_owned()); } } @@ -454,21 +453,21 @@ fn test_list_owned_txouts() { }], 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), @@ -476,25 +475,31 @@ fn test_list_owned_txouts() { }], 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) }; @@ -544,9 +549,9 @@ fn test_list_owned_txouts() { .collect::>(); 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 @@ -640,7 +645,7 @@ fn test_list_owned_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() } ); } @@ -678,7 +683,7 @@ fn test_list_owned_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::from_sat(0) // tx2 got confirmed (but spent by 3) + confirmed: Amount::from_sat(0), // tx2 got confirmed (but spent by 3) } ); } @@ -719,7 +724,7 @@ fn test_list_owned_txouts() { 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 } ); } @@ -760,7 +765,7 @@ fn test_list_owned_txouts() { 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 } ); } @@ -773,10 +778,10 @@ fn test_list_owned_txouts() { 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() } ); } diff --git a/crates/chain/tests/test_tx_graph_conflicts.rs b/crates/chain/tests/test_tx_graph_conflicts.rs index f1f16698..07760a8a 100644 --- a/crates/chain/tests/test_tx_graph_conflicts.rs +++ b/crates/chain/tests/test_tx_graph_conflicts.rs @@ -116,10 +116,8 @@ fn test_tx_conflict_handling() { 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 { @@ -152,10 +150,8 @@ fn test_tx_conflict_handling() { 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 { @@ -195,10 +191,8 @@ fn test_tx_conflict_handling() { 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 { @@ -232,10 +226,8 @@ fn test_tx_conflict_handling() { 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 { @@ -269,10 +261,8 @@ fn test_tx_conflict_handling() { 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 { @@ -319,10 +309,8 @@ fn test_tx_conflict_handling() { 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 { @@ -364,10 +352,8 @@ fn test_tx_conflict_handling() { 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 { @@ -406,10 +392,8 @@ fn test_tx_conflict_handling() { 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 { @@ -453,10 +437,8 @@ fn test_tx_conflict_handling() { ]), 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 { @@ -499,10 +481,8 @@ fn test_tx_conflict_handling() { 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 { @@ -545,10 +525,8 @@ fn test_tx_conflict_handling() { 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 { @@ -597,10 +575,8 @@ fn test_tx_conflict_handling() { 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 { @@ -630,10 +606,8 @@ fn test_tx_conflict_handling() { 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 { @@ -697,7 +671,7 @@ fn test_tx_conflict_handling() { 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", @@ -764,7 +738,7 @@ fn test_tx_conflict_handling() { 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)", @@ -801,7 +775,7 @@ fn test_tx_conflict_handling() { 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", @@ -845,10 +819,9 @@ fn test_tx_conflict_handling() { 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 { @@ -893,10 +866,9 @@ fn test_tx_conflict_handling() { 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 { @@ -937,10 +909,8 @@ fn test_tx_conflict_handling() { 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 { @@ -959,10 +929,7 @@ fn test_tx_conflict_handling() { 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() } } ]; @@ -1028,13 +995,9 @@ fn test_tx_conflict_handling() { ); 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, diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index c569b065..d0632db4 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -62,7 +62,11 @@ fn get_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) } @@ -592,7 +596,7 @@ fn test_sync() -> anyhow::Result<()> { assert_eq!( get_balance(&recv_chain, &recv_graph)?, Balance { - trusted_pending: SEND_AMOUNT, + untrusted_pending: SEND_AMOUNT, ..Balance::default() }, "balance must be correct", @@ -634,7 +638,7 @@ fn test_sync() -> anyhow::Result<()> { assert_eq!( get_balance(&recv_chain, &recv_graph)?, Balance { - trusted_pending: SEND_AMOUNT, + untrusted_pending: SEND_AMOUNT, ..Balance::default() }, ); @@ -770,7 +774,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { 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() },