Skip to content

Commit abc9cd8

Browse files
committed
Merge #336: Add apply_block_events and apply_block_connected_to_events
e9a3034 test(wallet): add tests for apply_block_events (Steve Myers) 3f0664e f Duplicate event logic rather than business logic (Elias Rohrer) df444d0 Add `apply_block_events` and `apply_block_connected_to_events` (Elias Rohrer) Pull request description: ### Description Previously, we added a new `Wallet::apply_update_events` method that returned `WalletEvent`s. Unfortunately, no corresponding APIs were added for the `apply_block` counterparts. Here we fix this omission. ### Notes to the reviewers I opened this towards the `release-2.2` branch, but it would probably need another release branch. Or let me know if you prefer to open it against master (which seems to be lacking `apply_update_events` currently though). I also added no test coverage given that none seems to exist for `Wallet::apply_block` in the first place. Let me know if I should add something here. ### Checklists #### All Submissions: * [x] I've signed all my commits * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md) * [x] I ran `just p` before pushing #### New Features: * [ ] I've added tests for the new feature * [x] I've added docs for the new feature cc @notmandatory ACKs for top commit: ValuedMammal: ACK e9a3034 notmandatory: (self) ACK e9a3034 Tree-SHA512: f62771ee8f8cd902df914dfb44767a45e9b0ca5b64ec342eba3c17f468e2b707c627d150a6a3994cfbd1966e5948acb9b8f7f289dbaf9ef7b0490d596dc76e3c
2 parents b5de563 + e9a3034 commit abc9cd8

File tree

2 files changed

+281
-9
lines changed

2 files changed

+281
-9
lines changed

wallet/src/wallet/mod.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2560,6 +2560,57 @@ impl Wallet {
25602560
})
25612561
}
25622562

2563+
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
2564+
/// `prev_blockhash` of the block's header.
2565+
///
2566+
/// This is a convenience method that is equivalent to calling
2567+
/// [`apply_block_connected_to_events`] with `prev_blockhash` and `height-1` as the
2568+
/// `connected_to` parameter.
2569+
///
2570+
/// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s.
2571+
///
2572+
/// [`apply_block_connected_to_events`]: Self::apply_block_connected_to_events
2573+
/// [`apply_update_events`]: Self::apply_update_events
2574+
pub fn apply_block_events(
2575+
&mut self,
2576+
block: &Block,
2577+
height: u32,
2578+
) -> Result<Vec<WalletEvent>, CannotConnectError> {
2579+
// snapshot of chain tip and transactions before update
2580+
let chain_tip1 = self.chain.tip().block_id();
2581+
let wallet_txs1 = self
2582+
.transactions()
2583+
.map(|wtx| {
2584+
(
2585+
wtx.tx_node.txid,
2586+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2587+
)
2588+
})
2589+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2590+
2591+
self.apply_block(block, height)?;
2592+
2593+
// chain tip and transactions after update
2594+
let chain_tip2 = self.chain.tip().block_id();
2595+
let wallet_txs2 = self
2596+
.transactions()
2597+
.map(|wtx| {
2598+
(
2599+
wtx.tx_node.txid,
2600+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2601+
)
2602+
})
2603+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2604+
2605+
Ok(wallet_events(
2606+
self,
2607+
chain_tip1,
2608+
chain_tip2,
2609+
wallet_txs1,
2610+
wallet_txs2,
2611+
))
2612+
}
2613+
25632614
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
25642615
/// block to the internal chain.
25652616
///
@@ -2591,6 +2642,56 @@ impl Wallet {
25912642
Ok(())
25922643
}
25932644

2645+
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
2646+
/// block to the internal chain.
2647+
///
2648+
/// See [`apply_block_connected_to`] for more information.
2649+
///
2650+
/// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s.
2651+
///
2652+
/// [`apply_block_connected_to`]: Self::apply_block_connected_to
2653+
/// [`apply_update_events`]: Self::apply_update_events
2654+
pub fn apply_block_connected_to_events(
2655+
&mut self,
2656+
block: &Block,
2657+
height: u32,
2658+
connected_to: BlockId,
2659+
) -> Result<Vec<WalletEvent>, ApplyHeaderError> {
2660+
// snapshot of chain tip and transactions before update
2661+
let chain_tip1 = self.chain.tip().block_id();
2662+
let wallet_txs1 = self
2663+
.transactions()
2664+
.map(|wtx| {
2665+
(
2666+
wtx.tx_node.txid,
2667+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2668+
)
2669+
})
2670+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2671+
2672+
self.apply_block_connected_to(block, height, connected_to)?;
2673+
2674+
// chain tip and transactions after update
2675+
let chain_tip2 = self.chain.tip().block_id();
2676+
let wallet_txs2 = self
2677+
.transactions()
2678+
.map(|wtx| {
2679+
(
2680+
wtx.tx_node.txid,
2681+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2682+
)
2683+
})
2684+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2685+
2686+
Ok(wallet_events(
2687+
self,
2688+
chain_tip1,
2689+
chain_tip2,
2690+
wallet_txs1,
2691+
wallet_txs2,
2692+
))
2693+
}
2694+
25942695
/// Apply relevant unconfirmed transactions to the wallet.
25952696
///
25962697
/// Transactions that are not relevant are filtered out.

wallet/tests/wallet_event.rs

Lines changed: 180 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ use bdk_chain::{BlockId, CheckPoint, ConfirmationBlockTime};
33
use bdk_wallet::event::WalletEvent;
44
use bdk_wallet::test_utils::{get_test_wpkh_and_change_desc, new_wallet_and_funding_update};
55
use bdk_wallet::{SignOptions, Update};
6+
use bitcoin::block::Header;
67
use bitcoin::hashes::Hash;
7-
use bitcoin::{Address, Amount, BlockHash, FeeRate};
8+
use bitcoin::{Address, Amount, Block, BlockHash, FeeRate, Transaction, TxMerkleNode};
89
use core::str::FromStr;
910
use std::sync::Arc;
1011

12+
/// apply_update_events tests.
1113
#[test]
1214
fn test_new_confirmed_tx_event() {
1315
let (desc, change_desc) = get_test_wpkh_and_change_desc();
@@ -28,9 +30,8 @@ fn test_new_confirmed_tx_event() {
2830
);
2931
assert!(matches!(&events[1], WalletEvent::TxConfirmed {tx, ..} if tx.output.len() == 1));
3032
assert!(
31-
matches!(events[2], WalletEvent::TxConfirmed {block_time, ..} if block_time.block_id.height == 2000)
33+
matches!(&events[2], WalletEvent::TxConfirmed {tx, block_time, ..} if block_time.block_id.height == 2000 && tx.output.len() == 2)
3234
);
33-
assert!(matches!(&events[2], WalletEvent::TxConfirmed {tx, ..} if tx.output.len() == 2));
3435
}
3536

3637
#[test]
@@ -88,7 +89,6 @@ fn test_tx_replaced_event() {
8889
update.tx_update.seen_ats = [(orig_txid, 210)].into();
8990
let events = wallet.apply_update_events(update).unwrap();
9091
assert_eq!(events.len(), 1);
91-
assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. }));
9292
assert!(
9393
matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == orig_txid)
9494
);
@@ -110,9 +110,8 @@ fn test_tx_replaced_event() {
110110
let events = wallet.apply_update_events(update).unwrap();
111111
assert_eq!(events.len(), 2);
112112
assert!(matches!(events[0], WalletEvent::TxUnconfirmed { txid, .. } if txid == rbf_txid));
113-
assert!(matches!(events[1], WalletEvent::TxReplaced { txid, ..} if txid == orig_txid));
114113
assert!(
115-
matches!(&events[1], WalletEvent::TxReplaced {conflicts, ..} if conflicts.len() == 1 &&
114+
matches!(&events[1], WalletEvent::TxReplaced {txid, conflicts, ..} if *txid == orig_txid && conflicts.len() == 1 &&
116115
conflicts.contains(&(0, rbf_txid)))
117116
);
118117
}
@@ -143,7 +142,6 @@ fn test_tx_confirmed_event() {
143142
update.tx_update.seen_ats = [(new_txid, 210)].into();
144143
let events = wallet.apply_update_events(update).unwrap();
145144
assert_eq!(events.len(), 1);
146-
assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. }));
147145
assert!(
148146
matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == new_txid)
149147
);
@@ -201,7 +199,6 @@ fn test_tx_confirmed_new_block_event() {
201199
update.tx_update.seen_ats = [(new_txid, 210)].into();
202200
let events = wallet.apply_update_events(update).unwrap();
203201
assert_eq!(events.len(), 1);
204-
assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. }));
205202
assert!(
206203
matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == new_txid)
207204
);
@@ -286,7 +283,6 @@ fn test_tx_dropped_event() {
286283
update.tx_update.seen_ats = [(new_txid, 210)].into();
287284
let events = wallet.apply_update_events(update).unwrap();
288285
assert_eq!(events.len(), 1);
289-
assert!(matches!(events[0], WalletEvent::TxUnconfirmed { .. }));
290286
assert!(
291287
matches!(&events[0], WalletEvent::TxUnconfirmed {tx, ..} if tx.compute_txid() == new_txid)
292288
);
@@ -299,3 +295,178 @@ fn test_tx_dropped_event() {
299295
assert_eq!(events.len(), 1);
300296
assert!(matches!(events[0], WalletEvent::TxDropped { txid, .. } if txid == new_txid));
301297
}
298+
299+
// apply_block_events tests.
300+
301+
fn test_block(prev_blockhash: BlockHash, time: u32, txdata: Vec<Transaction>) -> Block {
302+
Block {
303+
header: Header {
304+
version: Default::default(),
305+
prev_blockhash,
306+
merkle_root: TxMerkleNode::all_zeros(),
307+
time,
308+
bits: Default::default(),
309+
nonce: time,
310+
},
311+
txdata,
312+
}
313+
}
314+
315+
#[test]
316+
fn test_apply_block_new_confirmed_tx_event() {
317+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
318+
let (mut wallet, _, update) = new_wallet_and_funding_update(desc, Some(change_desc));
319+
320+
let genesis = BlockId {
321+
height: 0,
322+
hash: wallet.local_chain().genesis_hash(),
323+
};
324+
// apply empty block
325+
let block1 = test_block(genesis.hash, 1000, vec![]);
326+
let events = wallet.apply_block_events(&block1, 1).unwrap();
327+
assert_eq!(events.len(), 1);
328+
329+
// apply funding block
330+
let block2 = test_block(
331+
block1.block_hash(),
332+
2000,
333+
update.tx_update.txs[..1]
334+
.iter()
335+
.map(|tx| (**tx).clone())
336+
.collect(),
337+
);
338+
let events = wallet.apply_block_events(&block2, 2).unwrap();
339+
assert_eq!(events.len(), 2);
340+
let new_tip2 = wallet.local_chain().tip().block_id();
341+
assert!(
342+
matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (1, block1.block_hash()).into() && new_tip == new_tip2)
343+
);
344+
assert!(
345+
matches!(&events[1], WalletEvent::TxConfirmed { tx, block_time, ..} if block_time.block_id.height == 2 && tx.output.len() == 1)
346+
);
347+
348+
// apply empty block
349+
let block3 = test_block(block2.block_hash(), 3000, vec![]);
350+
let events = wallet.apply_block_events(&block3, 3).unwrap();
351+
assert_eq!(events.len(), 1);
352+
353+
// apply spending block
354+
let block4 = test_block(
355+
block3.block_hash(),
356+
4000,
357+
update.tx_update.txs[1..]
358+
.iter()
359+
.map(|tx| (**tx).clone())
360+
.collect(),
361+
);
362+
let events = wallet.apply_block_events(&block4, 4).unwrap();
363+
let new_tip3 = wallet.local_chain().tip().block_id();
364+
assert_eq!(events.len(), 2);
365+
assert!(
366+
matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (3, block3.block_hash()).into() && new_tip == new_tip3)
367+
);
368+
assert!(
369+
matches!(&events[1], WalletEvent::TxConfirmed {tx, block_time, ..} if block_time.block_id.height == 4 && tx.output.len() == 2)
370+
);
371+
}
372+
373+
#[test]
374+
fn test_apply_block_tx_unconfirmed_event() {
375+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
376+
let (mut wallet, _, update) = new_wallet_and_funding_update(desc, Some(change_desc));
377+
// apply funding block
378+
let genesis = BlockId {
379+
height: 0,
380+
hash: wallet.local_chain().genesis_hash(),
381+
};
382+
let block1 = test_block(
383+
genesis.hash,
384+
1000,
385+
update.tx_update.txs[..1]
386+
.iter()
387+
.map(|tx| (**tx).clone())
388+
.collect(),
389+
);
390+
let events = wallet.apply_block_events(&block1, 1).unwrap();
391+
assert_eq!(events.len(), 2);
392+
393+
// apply spending block
394+
let block2 = test_block(
395+
block1.block_hash(),
396+
2000,
397+
update.tx_update.txs[1..]
398+
.iter()
399+
.map(|tx| (**tx).clone())
400+
.collect(),
401+
);
402+
let events = wallet.apply_block_events(&block2, 2).unwrap();
403+
assert_eq!(events.len(), 2);
404+
let new_tip2 = wallet.local_chain().tip().block_id();
405+
assert!(
406+
matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (1, block1.block_hash()).into() && new_tip == new_tip2)
407+
);
408+
assert!(
409+
matches!(&events[1], WalletEvent::TxConfirmed {block_time, tx, ..} if block_time.block_id.height == 2 && tx.output.len() == 2)
410+
);
411+
412+
// apply reorg of spending block without previously confirmed tx
413+
let reorg_block2 = test_block(block1.block_hash(), 2100, vec![]);
414+
let events = wallet.apply_block_events(&reorg_block2, 2).unwrap();
415+
assert_eq!(events.len(), 2);
416+
assert!(
417+
matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip ==
418+
(2, block2.block_hash()).into() && new_tip == (2, reorg_block2.block_hash()).into())
419+
);
420+
assert!(
421+
matches!(&events[1], WalletEvent::TxUnconfirmed {tx, old_block_time, ..} if
422+
tx.output.len() == 2 && old_block_time.is_some())
423+
);
424+
}
425+
426+
#[test]
427+
fn test_apply_block_tx_confirmed_new_block_event() {
428+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
429+
let (mut wallet, _, update) = new_wallet_and_funding_update(desc, Some(change_desc));
430+
// apply funding block
431+
let genesis = BlockId {
432+
height: 0,
433+
hash: wallet.local_chain().genesis_hash(),
434+
};
435+
let block1 = test_block(
436+
genesis.hash,
437+
1000,
438+
update.tx_update.txs[..1]
439+
.iter()
440+
.map(|tx| (**tx).clone())
441+
.collect(),
442+
);
443+
let events = wallet.apply_block_events(&block1, 1).unwrap();
444+
assert_eq!(events.len(), 2);
445+
446+
// apply spending block
447+
let spending_tx: Transaction = (*update.tx_update.txs[1].clone()).clone();
448+
let block2 = test_block(block1.block_hash(), 2000, vec![spending_tx.clone()]);
449+
let events = wallet.apply_block_events(&block2, 2).unwrap();
450+
assert_eq!(events.len(), 2);
451+
let new_tip2 = wallet.local_chain().tip().block_id();
452+
assert!(
453+
matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip == (1, block1.block_hash()).into() && new_tip == new_tip2)
454+
);
455+
assert!(
456+
matches!(events[1], WalletEvent::TxConfirmed { txid, block_time, old_block_time, .. } if
457+
txid == spending_tx.compute_txid() && block_time.block_id == (2, block2.block_hash()).into() && old_block_time.is_none())
458+
);
459+
460+
// apply reorg of spending block including the original spending tx
461+
let reorg_block2 = test_block(block1.block_hash(), 2100, vec![spending_tx.clone()]);
462+
let events = wallet.apply_block_events(&reorg_block2, 2).unwrap();
463+
assert_eq!(events.len(), 2);
464+
assert!(
465+
matches!(events[0], WalletEvent::ChainTipChanged { old_tip, new_tip } if old_tip ==
466+
(2, block2.block_hash()).into() && new_tip == (2, reorg_block2.block_hash()).into())
467+
);
468+
assert!(
469+
matches!(events[1], WalletEvent::TxConfirmed { txid, block_time, old_block_time, .. } if
470+
txid == spending_tx.compute_txid() && block_time.block_id == (2, reorg_block2.block_hash()).into() && old_block_time.is_some())
471+
);
472+
}

0 commit comments

Comments
 (0)