diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index efdb7a099..2f5dca5bf 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -15,16 +15,30 @@ use crate::Anchor; )) )] pub enum ChainPosition { - /// The chain data is seen as confirmed, and in anchored by `A`. - Confirmed(A), - /// The chain data is not confirmed and last seen in the mempool at this timestamp. - Unconfirmed(u64), + /// The chain data is confirmed as it is anchored in the best chain by `A`. + Confirmed { + /// The [`Anchor`]. + anchor: A, + /// Whether the chain data is anchored transitively by a child transaction. + /// + /// If the value is `Some`, it means we have incomplete data. We can only deduce that the + /// chain data is confirmed at a block equal to or lower than the block referenced by `A`. + transitively: Option, + }, + /// The chain data is not confirmed. + Unconfirmed { + /// When the chain data is last seen in the mempool. + /// + /// This value will be `None` if the chain data was never seen in the mempool and only seen + /// in a conflicting chain. + last_seen: Option, + }, } impl ChainPosition { /// Returns whether [`ChainPosition`] is confirmed or not. pub fn is_confirmed(&self) -> bool { - matches!(self, Self::Confirmed(_)) + matches!(self, Self::Confirmed { .. }) } } @@ -32,8 +46,14 @@ impl ChainPosition<&A> { /// Maps a [`ChainPosition<&A>`] into a [`ChainPosition`] by cloning the contents. pub fn cloned(self) -> ChainPosition { match self { - ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()), - ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen), + ChainPosition::Confirmed { + anchor, + transitively, + } => ChainPosition::Confirmed { + anchor: anchor.clone(), + transitively, + }, + ChainPosition::Unconfirmed { last_seen } => ChainPosition::Unconfirmed { last_seen }, } } } @@ -42,8 +62,10 @@ impl ChainPosition { /// Determines the upper bound of the confirmation height. pub fn confirmation_height_upper_bound(&self) -> Option { match self { - ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()), - ChainPosition::Unconfirmed(_) => None, + ChainPosition::Confirmed { anchor, .. } => { + Some(anchor.confirmation_height_upper_bound()) + } + ChainPosition::Unconfirmed { .. } => None, } } } @@ -73,9 +95,9 @@ impl FullTxOut { /// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound pub fn is_mature(&self, tip: u32) -> bool { if self.is_on_coinbase { - let tx_height = match &self.chain_position { - ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), - ChainPosition::Unconfirmed(_) => { + let tx_height = match self.chain_position.confirmation_height_upper_bound() { + Some(conf_height_upper_bound) => conf_height_upper_bound, + None => { debug_assert!(false, "coinbase tx can never be unconfirmed"); return false; } @@ -103,17 +125,21 @@ impl FullTxOut { return false; } - let confirmation_height = match &self.chain_position { - ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(), - ChainPosition::Unconfirmed(_) => return false, + let confirmation_height = match self.chain_position.confirmation_height_upper_bound() { + Some(h) => h, + None => return false, }; if confirmation_height > tip { return false; } // if the spending tx is confirmed within tip height, the txout is no longer spendable - if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by { - if spending_anchor.anchor_block().height <= tip { + if let Some(spend_height) = self + .spent_by + .as_ref() + .and_then(|(pos, _)| pos.confirmation_height_upper_bound()) + { + if spend_height <= tip { return false; } } @@ -132,22 +158,32 @@ mod test { #[test] fn chain_position_ord() { - let unconf1 = ChainPosition::::Unconfirmed(10); - let unconf2 = ChainPosition::::Unconfirmed(20); - let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime { - confirmation_time: 20, - block_id: BlockId { - height: 9, - ..Default::default() + let unconf1 = ChainPosition::::Unconfirmed { + last_seen: Some(10), + }; + let unconf2 = ChainPosition::::Unconfirmed { + last_seen: Some(20), + }; + let conf1 = ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + confirmation_time: 20, + block_id: BlockId { + height: 9, + ..Default::default() + }, }, - }); - let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime { - confirmation_time: 15, - block_id: BlockId { - height: 12, - ..Default::default() + transitively: None, + }; + let conf2 = ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + confirmation_time: 15, + block_id: BlockId { + height: 12, + ..Default::default() + }, }, - }); + transitively: None, + }; assert!(unconf2 > unconf1, "higher last_seen means higher ord"); assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed"); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index a10d1aeb8..b266cf9ea 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -770,7 +770,12 @@ impl TxGraph { for anchor in anchors { match chain.is_block_in_chain(anchor.anchor_block(), chain_tip)? { - Some(true) => return Ok(Some(ChainPosition::Confirmed(anchor))), + Some(true) => { + return Ok(Some(ChainPosition::Confirmed { + anchor, + transitively: None, + })) + } _ => continue, } } @@ -877,7 +882,9 @@ impl TxGraph { } } - Ok(Some(ChainPosition::Unconfirmed(last_seen))) + Ok(Some(ChainPosition::Unconfirmed { + last_seen: Some(last_seen), + })) } /// Get the position of the transaction in `chain` with tip `chain_tip`. @@ -1146,14 +1153,14 @@ impl TxGraph { let (spk_i, txout) = res?; match &txout.chain_position { - ChainPosition::Confirmed(_) => { + ChainPosition::Confirmed { .. } => { if txout.is_confirmed_and_spendable(chain_tip.height) { confirmed += txout.txout.value; } else if !txout.is_mature(chain_tip.height) { immature += txout.txout.value; } } - ChainPosition::Unconfirmed(_) => { + ChainPosition::Unconfirmed { .. } => { if trust_predicate(&spk_i, txout.txout.script_pubkey) { trusted_pending += txout.txout.value; } else { diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index a8d17ca91..1b3dff573 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -293,7 +293,7 @@ fn test_list_owned_txouts() { let confirmed_txouts_txid = txouts .iter() .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) { + if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) { Some(full_txout.outpoint.txid) } else { None @@ -304,7 +304,7 @@ fn test_list_owned_txouts() { let unconfirmed_txouts_txid = txouts .iter() .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { + if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) { Some(full_txout.outpoint.txid) } else { None @@ -315,7 +315,7 @@ fn test_list_owned_txouts() { let confirmed_utxos_txid = utxos .iter() .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) { + if matches!(full_txout.chain_position, ChainPosition::Confirmed { .. }) { Some(full_txout.outpoint.txid) } else { None @@ -326,7 +326,7 @@ fn test_list_owned_txouts() { let unconfirmed_utxos_txid = utxos .iter() .filter_map(|(_, full_txout)| { - if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) { + if matches!(full_txout.chain_position, ChainPosition::Unconfirmed { .. }) { Some(full_txout.outpoint.txid) } else { None @@ -618,7 +618,7 @@ fn test_get_chain_position() { }, anchor: None, last_seen: Some(2), - exp_pos: Some(ChainPosition::Unconfirmed(2)), + exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }), }, TestCase { name: "tx anchor in best chain - confirmed", @@ -631,7 +631,10 @@ fn test_get_chain_position() { }, anchor: Some(blocks[1]), last_seen: None, - exp_pos: Some(ChainPosition::Confirmed(blocks[1])), + exp_pos: Some(ChainPosition::Confirmed { + anchor: blocks[1], + transitively: None, + }), }, TestCase { name: "tx unknown anchor with last_seen - unconfirmed", @@ -644,7 +647,7 @@ fn test_get_chain_position() { }, anchor: Some(block_id!(2, "B'")), last_seen: Some(2), - exp_pos: Some(ChainPosition::Unconfirmed(2)), + exp_pos: Some(ChainPosition::Unconfirmed { last_seen: Some(2) }), }, TestCase { name: "tx unknown anchor - no chain pos", diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 08be91c7a..1cb52f653 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -885,13 +885,16 @@ fn test_chain_spends() { OutPoint::new(tx_0.compute_txid(), 0) ), Some(( - ChainPosition::Confirmed(&ConfirmationBlockTime { - block_id: BlockId { - hash: tip.get(98).unwrap().hash(), - height: 98, + ChainPosition::Confirmed { + anchor: &ConfirmationBlockTime { + block_id: BlockId { + hash: tip.get(98).unwrap().hash(), + height: 98, + }, + confirmation_time: 100 }, - confirmation_time: 100 - }), + transitively: None + }, tx_1.compute_txid(), )), ); @@ -900,13 +903,16 @@ fn test_chain_spends() { assert_eq!( graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()), // Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))), - Some(ChainPosition::Confirmed(&ConfirmationBlockTime { - block_id: BlockId { - hash: tip.get(95).unwrap().hash(), - height: 95, + Some(ChainPosition::Confirmed { + anchor: &ConfirmationBlockTime { + block_id: BlockId { + hash: tip.get(95).unwrap().hash(), + height: 95, + }, + confirmation_time: 100 }, - confirmation_time: 100 - })) + transitively: None + }) ); // Mark the unconfirmed as seen and check correct ObservedAs status is returned. @@ -921,7 +927,12 @@ fn test_chain_spends() { OutPoint::new(tx_0.compute_txid(), 1) ) .unwrap(), - (ChainPosition::Unconfirmed(1234567), tx_2.compute_txid()) + ( + ChainPosition::Unconfirmed { + last_seen: Some(1234567) + }, + tx_2.compute_txid() + ) ); // A conflicting transaction that conflicts with tx_1. @@ -957,7 +968,9 @@ fn test_chain_spends() { graph .get_chain_position(&local_chain, tip.block_id(), tx_2_conflict.compute_txid()) .expect("position expected"), - ChainPosition::Unconfirmed(1234568) + ChainPosition::Unconfirmed { + last_seen: Some(1234568) + } ); // Chain_spend now catches the new transaction as the spend. @@ -970,7 +983,9 @@ fn test_chain_spends() { ) .expect("expect observation"), ( - ChainPosition::Unconfirmed(1234568), + ChainPosition::Unconfirmed { + last_seen: Some(1234568) + }, tx_2_conflict.compute_txid() ) ); diff --git a/crates/wallet/src/test_utils.rs b/crates/wallet/src/test_utils.rs index 050b9fb19..c69de620a 100644 --- a/crates/wallet/src/test_utils.rs +++ b/crates/wallet/src/test_utils.rs @@ -229,12 +229,15 @@ pub fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoi let latest_cp = wallet.latest_checkpoint(); let height = latest_cp.height(); let anchor = if height == 0 { - ChainPosition::Unconfirmed(0) + ChainPosition::Unconfirmed { last_seen: Some(0) } } else { - ChainPosition::Confirmed(ConfirmationBlockTime { - block_id: latest_cp.block_id(), - confirmation_time: 0, - }) + ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + block_id: latest_cp.block_id(), + confirmation_time: 0, + }, + transitively: None, + } }; receive_output(wallet, value, anchor) } @@ -270,11 +273,13 @@ pub fn receive_output_to_address( insert_tx(wallet, tx); match pos { - ChainPosition::Confirmed(anchor) => { + ChainPosition::Confirmed { anchor, .. } => { insert_anchor(wallet, txid, anchor); } - ChainPosition::Unconfirmed(last_seen) => { - insert_seen_at(wallet, txid, last_seen); + ChainPosition::Unconfirmed { last_seen } => { + if let Some(last_seen) = last_seen { + insert_seen_at(wallet, txid, last_seen); + } } } diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 0f0e4a88e..a651ddc54 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -754,7 +754,13 @@ mod test { const FEE_AMOUNT: u64 = 50; fn unconfirmed_utxo(value: u64, index: u32, last_seen: u64) -> WeightedUtxo { - utxo(value, index, ChainPosition::Unconfirmed(last_seen)) + utxo( + value, + index, + ChainPosition::Unconfirmed { + last_seen: Some(last_seen), + }, + ) } fn confirmed_utxo( @@ -766,13 +772,16 @@ mod test { utxo( value, index, - ChainPosition::Confirmed(ConfirmationBlockTime { - block_id: chain::BlockId { - height: confirmation_height, - hash: bitcoin::BlockHash::all_zeros(), + ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + block_id: chain::BlockId { + height: confirmation_height, + hash: bitcoin::BlockHash::all_zeros(), + }, + confirmation_time, }, - confirmation_time, - }), + transitively: None, + }, ) } @@ -838,15 +847,18 @@ mod test { is_spent: false, derivation_index: rng.next_u32(), chain_position: if rng.gen_bool(0.5) { - ChainPosition::Confirmed(ConfirmationBlockTime { - block_id: chain::BlockId { - height: rng.next_u32(), - hash: BlockHash::all_zeros(), + ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + block_id: chain::BlockId { + height: rng.next_u32(), + hash: BlockHash::all_zeros(), + }, + confirmation_time: rng.next_u64(), }, - confirmation_time: rng.next_u64(), - }) + transitively: None, + } } else { - ChainPosition::Unconfirmed(0) + ChainPosition::Unconfirmed { last_seen: Some(0) } }, }), }); @@ -871,7 +883,7 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 42, - chain_position: ChainPosition::Unconfirmed(0), + chain_position: ChainPosition::Unconfirmed { last_seen: Some(0) }, }), }) .collect() @@ -1228,7 +1240,7 @@ mod test { optional.push(utxo( 500_000, 3, - ChainPosition::::Unconfirmed(0), + ChainPosition::::Unconfirmed { last_seen: Some(0) }, )); // Defensive assertions, for sanity and in case someone changes the test utxos vector. @@ -1590,13 +1602,16 @@ mod test { keychain: KeychainKind::External, is_spent: false, derivation_index: 0, - chain_position: ChainPosition::Confirmed(ConfirmationBlockTime { - block_id: BlockId { - height: 12345, - hash: BlockHash::all_zeros(), + chain_position: ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + block_id: BlockId { + height: 12345, + hash: BlockHash::all_zeros(), + }, + confirmation_time: 12345, }, - confirmation_time: 12345, - }), + transitively: None, + }, }), } } diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 6441d3b58..cbbee2e2e 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -129,10 +129,10 @@ impl FullyNodedExport { let blockheight = if include_blockheight { wallet.transactions().next().map_or(0, |canonical_tx| { - match canonical_tx.chain_position { - bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height, - bdk_chain::ChainPosition::Unconfirmed(_) => 0, - } + canonical_tx + .chain_position + .confirmation_height_upper_bound() + .unwrap_or(0) }) } else { 0 diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 7c8c8872d..e1c35eb79 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1037,12 +1037,22 @@ impl Wallet { /// /// // get confirmation status of transaction /// match wallet_tx.chain_position { - /// ChainPosition::Confirmed(anchor) => println!( + /// ChainPosition::Confirmed { + /// anchor, + /// transitively: None, + /// } => println!( /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", /// anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash, /// ), - /// ChainPosition::Unconfirmed(last_seen) => println!( - /// "tx is last seen at {}, it is unconfirmed as it is not anchored in the best chain", + /// ChainPosition::Confirmed { + /// anchor, + /// transitively: Some(_), + /// } => println!( + /// "tx is an ancestor of a tx anchored in {}:{}", + /// anchor.block_id.height, anchor.block_id.hash, + /// ), + /// ChainPosition::Unconfirmed { last_seen } => println!( + /// "tx is last seen at {:?}, it is unconfirmed as it is not anchored in the best chain", /// last_seen, /// ), /// } @@ -1590,7 +1600,7 @@ impl Wallet { let pos = graph .get_chain_position(&self.chain, chain_tip, txid) .ok_or(BuildFeeBumpError::TransactionNotFound(txid))?; - if let ChainPosition::Confirmed(_) = pos { + if pos.is_confirmed() { return Err(BuildFeeBumpError::TransactionConfirmed(txid)); } @@ -1840,9 +1850,10 @@ impl Wallet { .indexed_graph .graph() .get_chain_position(&self.chain, chain_tip, input.previous_output.txid) - .map(|chain_position| match chain_position { - ChainPosition::Confirmed(a) => a.block_id.height, - ChainPosition::Unconfirmed(_) => u32::MAX, + .map(|chain_position| { + chain_position + .confirmation_height_upper_bound() + .unwrap_or(u32::MAX) }); let current_height = sign_options .assume_height @@ -2032,9 +2043,10 @@ impl Wallet { ); if let Some(current_height) = current_height { match chain_position { - ChainPosition::Confirmed(a) => { + ChainPosition::Confirmed { anchor, .. } => { // https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 - spendable &= (current_height.saturating_sub(a.block_id.height)) + spendable &= (current_height + .saturating_sub(anchor.block_id.height)) >= COINBASE_MATURITY; } ChainPosition::Unconfirmed { .. } => spendable = false, diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index 49f3c19d1..8872dc2e0 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -1017,7 +1017,7 @@ mod test { txout: TxOut::NULL, keychain: KeychainKind::External, is_spent: false, - chain_position: chain::ChainPosition::Unconfirmed(0), + chain_position: chain::ChainPosition::Unconfirmed { last_seen: Some(0) }, derivation_index: 0, }, LocalOutput { @@ -1028,13 +1028,16 @@ mod test { txout: TxOut::NULL, keychain: KeychainKind::Internal, is_spent: false, - chain_position: chain::ChainPosition::Confirmed(chain::ConfirmationBlockTime { - block_id: chain::BlockId { - height: 32, - hash: bitcoin::BlockHash::all_zeros(), + chain_position: chain::ChainPosition::Confirmed { + anchor: chain::ConfirmationBlockTime { + block_id: chain::BlockId { + height: 32, + hash: bitcoin::BlockHash::all_zeros(), + }, + confirmation_time: 42, }, - confirmation_time: 42, - }), + transitively: None, + }, derivation_index: 1, }, ] diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index bc8ee4d49..cd9408d45 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -1439,7 +1439,11 @@ fn test_create_tx_increment_change_index() { .create_wallet_no_persist() .unwrap(); // fund wallet - receive_output(&mut wallet, amount, ChainPosition::Unconfirmed(0)); + receive_output( + &mut wallet, + amount, + ChainPosition::Unconfirmed { last_seen: Some(0) }, + ); // create tx let mut builder = wallet.build_tx(); builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send)); @@ -2541,7 +2545,11 @@ fn test_bump_fee_unconfirmed_inputs_only() { let psbt = builder.finish().unwrap(); // Now we receive one transaction with 0 confirmations. We won't be able to use that for // fee bumping, as it's still unconfirmed! - receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); + receive_output( + &mut wallet, + 25_000, + ChainPosition::Unconfirmed { last_seen: Some(0) }, + ); let mut tx = psbt.extract_tx().expect("failed to extract tx"); let txid = tx.compute_txid(); for txin in &mut tx.input { @@ -2566,7 +2574,11 @@ fn test_bump_fee_unconfirmed_input() { .assume_checked(); // We receive a tx with 0 confirmations, which will be used as an input // in the drain tx. - receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); + receive_output( + &mut wallet, + 25_000, + ChainPosition::Unconfirmed { last_seen: Some(0) }, + ); let mut builder = wallet.build_tx(); builder.drain_wallet().drain_to(addr.script_pubkey()); let psbt = builder.finish().unwrap(); @@ -4036,13 +4048,16 @@ fn test_keychains_with_overlapping_spks() { .last() .unwrap() .address; - let chain_position = ChainPosition::Confirmed(ConfirmationBlockTime { - block_id: BlockId { - height: 2000, - hash: BlockHash::all_zeros(), + let chain_position = ChainPosition::Confirmed { + anchor: ConfirmationBlockTime { + block_id: BlockId { + height: 2000, + hash: BlockHash::all_zeros(), + }, + confirmation_time: 0, }, - confirmation_time: 0, - }); + transitively: None, + }; let _outpoint = receive_output_to_address(&mut wallet, addr, 8000, chain_position); assert_eq!(wallet.balance().confirmed, Amount::from_sat(58000)); } @@ -4132,7 +4147,11 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { .unwrap(); assert_eq!(wallet.keychains().count(), 1); let amt = Amount::from_sat(5_000); - receive_output(&mut wallet, 2 * amt.to_sat(), ChainPosition::Unconfirmed(2)); + receive_output( + &mut wallet, + 2 * amt.to_sat(), + ChainPosition::Unconfirmed { last_seen: Some(2) }, + ); // create spend tx that produces a change output let addr = Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm") .unwrap() @@ -4158,7 +4177,11 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { #[test] fn test_transactions_sort_by() { let (mut wallet, _txid) = get_funded_wallet_wpkh(); - receive_output(&mut wallet, 25_000, ChainPosition::Unconfirmed(0)); + receive_output( + &mut wallet, + 25_000, + ChainPosition::Unconfirmed { last_seen: Some(0) }, + ); // sort by chain position, unconfirmed then confirmed by descending block height let sorted_txs: Vec =