Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<WalletEvent>, 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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

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.
///
Expand Down Expand Up @@ -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<Vec<WalletEvent>, 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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

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::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();

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.
Expand Down
189 changes: 180 additions & 9 deletions wallet/tests/wallet_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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]
Expand Down Expand Up @@ -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)
);
Expand All @@ -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)))
);
}
Expand Down Expand Up @@ -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)
);
Expand Down Expand Up @@ -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)
);
Expand Down Expand Up @@ -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)
);
Expand All @@ -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<Transaction>) -> 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())
);
}
Loading