diff --git a/wallet/src/wallet/mod.rs b/wallet/src/wallet/mod.rs index 9f276e9d..4939d01c 100644 --- a/wallet/src/wallet/mod.rs +++ b/wallet/src/wallet/mod.rs @@ -2560,6 +2560,57 @@ impl Wallet { }) } + /// Introduces a `block` of `height` to the wallet, and tries to connect it to the + /// `prev_blockhash` of the block's header. + /// + /// This is a convenience method that is equivalent to calling + /// [`apply_block_connected_to_events`] with `prev_blockhash` and `height-1` as the + /// `connected_to` parameter. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_block_connected_to_events`]: Self::apply_block_connected_to_events + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_block_events( + &mut self, + block: &Block, + height: u32, + ) -> Result, CannotConnectError> { + // snapshot of chain tip and transactions before update + let chain_tip1 = self.chain.tip().block_id(); + let wallet_txs1 = self + .transactions() + .map(|wtx| { + ( + wtx.tx_node.txid, + (wtx.tx_node.tx.clone(), wtx.chain_position), + ) + }) + .collect::, ChainPosition)>>(); + + self.apply_block(block, height)?; + + // chain tip and transactions after update + let chain_tip2 = self.chain.tip().block_id(); + let wallet_txs2 = self + .transactions() + .map(|wtx| { + ( + wtx.tx_node.txid, + (wtx.tx_node.tx.clone(), wtx.chain_position), + ) + }) + .collect::, ChainPosition)>>(); + + Ok(wallet_events( + self, + chain_tip1, + chain_tip2, + wallet_txs1, + wallet_txs2, + )) + } + /// Applies relevant transactions from `block` of `height` to the wallet, and connects the /// block to the internal chain. /// @@ -2591,6 +2642,56 @@ impl Wallet { Ok(()) } + /// Applies relevant transactions from `block` of `height` to the wallet, and connects the + /// block to the internal chain. + /// + /// See [`apply_block_connected_to`] for more information. + /// + /// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s. + /// + /// [`apply_block_connected_to`]: Self::apply_block_connected_to + /// [`apply_update_events`]: Self::apply_update_events + pub fn apply_block_connected_to_events( + &mut self, + block: &Block, + height: u32, + connected_to: BlockId, + ) -> Result, ApplyHeaderError> { + // snapshot of chain tip and transactions before update + let chain_tip1 = self.chain.tip().block_id(); + let wallet_txs1 = self + .transactions() + .map(|wtx| { + ( + wtx.tx_node.txid, + (wtx.tx_node.tx.clone(), wtx.chain_position), + ) + }) + .collect::, ChainPosition)>>(); + + self.apply_block_connected_to(block, height, connected_to)?; + + // chain tip and transactions after update + let chain_tip2 = self.chain.tip().block_id(); + let wallet_txs2 = self + .transactions() + .map(|wtx| { + ( + wtx.tx_node.txid, + (wtx.tx_node.tx.clone(), wtx.chain_position), + ) + }) + .collect::, ChainPosition)>>(); + + Ok(wallet_events( + self, + chain_tip1, + chain_tip2, + wallet_txs1, + wallet_txs2, + )) + } + /// Apply relevant unconfirmed transactions to the wallet. /// /// Transactions that are not relevant are filtered out. diff --git a/wallet/tests/wallet_event.rs b/wallet/tests/wallet_event.rs index 335f3e5e..bf72a35e 100644 --- a/wallet/tests/wallet_event.rs +++ b/wallet/tests/wallet_event.rs @@ -3,11 +3,13 @@ use bdk_chain::{BlockId, CheckPoint, ConfirmationBlockTime}; use bdk_wallet::event::WalletEvent; use bdk_wallet::test_utils::{get_test_wpkh_and_change_desc, new_wallet_and_funding_update}; use bdk_wallet::{SignOptions, Update}; +use bitcoin::block::Header; use bitcoin::hashes::Hash; -use bitcoin::{Address, Amount, BlockHash, FeeRate}; +use bitcoin::{Address, Amount, Block, BlockHash, FeeRate, Transaction, TxMerkleNode}; use core::str::FromStr; use std::sync::Arc; +/// apply_update_events tests. #[test] fn test_new_confirmed_tx_event() { let (desc, change_desc) = get_test_wpkh_and_change_desc(); @@ -28,9 +30,8 @@ fn test_new_confirmed_tx_event() { ); assert!(matches!(&events[1], WalletEvent::TxConfirmed {tx, ..} if tx.output.len() == 1)); assert!( - matches!(events[2], WalletEvent::TxConfirmed {block_time, ..} if block_time.block_id.height == 2000) + matches!(&events[2], WalletEvent::TxConfirmed {tx, block_time, ..} if block_time.block_id.height == 2000 && tx.output.len() == 2) ); - assert!(matches!(&events[2], WalletEvent::TxConfirmed {tx, ..} if tx.output.len() == 2)); } #[test] @@ -88,7 +89,6 @@ fn test_tx_replaced_event() { update.tx_update.seen_ats = [(orig_txid, 210)].into(); let events = wallet.apply_update_events(update).unwrap(); assert_eq!(events.len(), 1); - assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. })); assert!( matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == orig_txid) ); @@ -110,9 +110,8 @@ fn test_tx_replaced_event() { let events = wallet.apply_update_events(update).unwrap(); assert_eq!(events.len(), 2); assert!(matches!(events[0], WalletEvent::TxUnconfirmed { txid, .. } if txid == rbf_txid)); - assert!(matches!(events[1], WalletEvent::TxReplaced { txid, ..} if txid == orig_txid)); assert!( - matches!(&events[1], WalletEvent::TxReplaced {conflicts, ..} if conflicts.len() == 1 && + matches!(&events[1], WalletEvent::TxReplaced {txid, conflicts, ..} if *txid == orig_txid && conflicts.len() == 1 && conflicts.contains(&(0, rbf_txid))) ); } @@ -143,7 +142,6 @@ fn test_tx_confirmed_event() { update.tx_update.seen_ats = [(new_txid, 210)].into(); let events = wallet.apply_update_events(update).unwrap(); assert_eq!(events.len(), 1); - assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. })); assert!( matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == new_txid) ); @@ -201,7 +199,6 @@ fn test_tx_confirmed_new_block_event() { update.tx_update.seen_ats = [(new_txid, 210)].into(); let events = wallet.apply_update_events(update).unwrap(); assert_eq!(events.len(), 1); - assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. })); assert!( matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == new_txid) ); @@ -286,7 +283,6 @@ fn test_tx_dropped_event() { update.tx_update.seen_ats = [(new_txid, 210)].into(); let events = wallet.apply_update_events(update).unwrap(); assert_eq!(events.len(), 1); - assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. })); assert!( matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == new_txid) ); @@ -299,3 +295,178 @@ fn test_tx_dropped_event() { assert_eq!(events.len(), 1); assert!(matches!(events[0], WalletEvent::TxDropped { txid, .. } if txid == new_txid)); } + +// apply_block_events tests. + +fn test_block(prev_blockhash: BlockHash, time: u32, txdata: Vec) -> Block { + Block { + header: Header { + version: Default::default(), + prev_blockhash, + merkle_root: TxMerkleNode::all_zeros(), + time, + bits: Default::default(), + nonce: time, + }, + txdata, + } +} + +#[test] +fn test_apply_block_new_confirmed_tx_event() { + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let (mut wallet, _, update) = new_wallet_and_funding_update(desc, Some(change_desc)); + + let genesis = BlockId { + height: 0, + hash: wallet.local_chain().genesis_hash(), + }; + // apply empty block + let block1 = test_block(genesis.hash, 1000, vec![]); + let events = wallet.apply_block_events(&block1, 1).unwrap(); + assert_eq!(events.len(), 1); + + // apply funding block + let block2 = test_block( + block1.block_hash(), + 2000, + update.tx_update.txs[..1] + .iter() + .map(|tx| (**tx).clone()) + .collect(), + ); + let events = wallet.apply_block_events(&block2, 2).unwrap(); + assert_eq!(events.len(), 2); + let new_tip2 = wallet.local_chain().tip().block_id(); + assert!( + matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (1, block1.block_hash()).into() && new_tip == new_tip2) + ); + assert!( + matches!(&events[1], WalletEvent::TxConfirmed { tx, block_time, ..} if block_time.block_id.height == 2 && tx.output.len() == 1) + ); + + // apply empty block + let block3 = test_block(block2.block_hash(), 3000, vec![]); + let events = wallet.apply_block_events(&block3, 3).unwrap(); + assert_eq!(events.len(), 1); + + // apply spending block + let block4 = test_block( + block3.block_hash(), + 4000, + update.tx_update.txs[1..] + .iter() + .map(|tx| (**tx).clone()) + .collect(), + ); + let events = wallet.apply_block_events(&block4, 4).unwrap(); + let new_tip3 = wallet.local_chain().tip().block_id(); + assert_eq!(events.len(), 2); + assert!( + matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (3, block3.block_hash()).into() && new_tip == new_tip3) + ); + assert!( + matches!(&events[1], WalletEvent::TxConfirmed {tx, block_time, ..} if block_time.block_id.height == 4 && tx.output.len() == 2) + ); +} + +#[test] +fn test_apply_block_tx_unconfirmed_event() { + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let (mut wallet, _, update) = new_wallet_and_funding_update(desc, Some(change_desc)); + // apply funding block + let genesis = BlockId { + height: 0, + hash: wallet.local_chain().genesis_hash(), + }; + let block1 = test_block( + genesis.hash, + 1000, + update.tx_update.txs[..1] + .iter() + .map(|tx| (**tx).clone()) + .collect(), + ); + let events = wallet.apply_block_events(&block1, 1).unwrap(); + assert_eq!(events.len(), 2); + + // apply spending block + let block2 = test_block( + block1.block_hash(), + 2000, + update.tx_update.txs[1..] + .iter() + .map(|tx| (**tx).clone()) + .collect(), + ); + let events = wallet.apply_block_events(&block2, 2).unwrap(); + assert_eq!(events.len(), 2); + let new_tip2 = wallet.local_chain().tip().block_id(); + assert!( + matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (1, block1.block_hash()).into() && new_tip == new_tip2) + ); + assert!( + matches!(&events[1], WalletEvent::TxConfirmed {block_time, tx, ..} if block_time.block_id.height == 2 && tx.output.len() == 2) + ); + + // apply reorg of spending block without previously confirmed tx + let reorg_block2 = test_block(block1.block_hash(), 2100, vec![]); + let events = wallet.apply_block_events(&reorg_block2, 2).unwrap(); + assert_eq!(events.len(), 2); + assert!( + matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == +(2, block2.block_hash()).into() && new_tip == (2, reorg_block2.block_hash()).into()) + ); + assert!( + matches!(&events[1], WalletEvent::TxUnconfirmed {tx, old_block_time, ..} if +tx.output.len() == 2 && old_block_time.is_some()) + ); +} + +#[test] +fn test_apply_block_tx_confirmed_new_block_event() { + let (desc, change_desc) = get_test_wpkh_and_change_desc(); + let (mut wallet, _, update) = new_wallet_and_funding_update(desc, Some(change_desc)); + // apply funding block + let genesis = BlockId { + height: 0, + hash: wallet.local_chain().genesis_hash(), + }; + let block1 = test_block( + genesis.hash, + 1000, + update.tx_update.txs[..1] + .iter() + .map(|tx| (**tx).clone()) + .collect(), + ); + let events = wallet.apply_block_events(&block1, 1).unwrap(); + assert_eq!(events.len(), 2); + + // apply spending block + let spending_tx: Transaction = (*update.tx_update.txs[1].clone()).clone(); + let block2 = test_block(block1.block_hash(), 2000, vec![spending_tx.clone()]); + let events = wallet.apply_block_events(&block2, 2).unwrap(); + assert_eq!(events.len(), 2); + let new_tip2 = wallet.local_chain().tip().block_id(); + assert!( + matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (1, block1.block_hash()).into() && new_tip == new_tip2) + ); + assert!( + matches!(events[1], WalletEvent::TxConfirmed { txid, block_time, old_block_time, .. } if + txid == spending_tx.compute_txid() && block_time.block_id == (2, block2.block_hash()).into() && old_block_time.is_none()) + ); + + // apply reorg of spending block including the original spending tx + let reorg_block2 = test_block(block1.block_hash(), 2100, vec![spending_tx.clone()]); + let events = wallet.apply_block_events(&reorg_block2, 2).unwrap(); + assert_eq!(events.len(), 2); + assert!( + matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == +(2, block2.block_hash()).into() && new_tip == (2, reorg_block2.block_hash()).into()) + ); + assert!( + matches!(events[1], WalletEvent::TxConfirmed { txid, block_time, old_block_time, .. } if +txid == spending_tx.compute_txid() && block_time.block_id == (2, reorg_block2.block_hash()).into() && old_block_time.is_some()) + ); +}