Skip to content

Commit 2891853

Browse files
committed
docs(event): improve wallet event docs and tests
per suggestions from ValuedMammal: 1. re-export WalletEvent type 2. add comments to wallet_events function 3. rename ambiguous variable names in wallet_events from cp to pos 4. remove signing from wallet_event tests 5. change wallet_events function assert_eq to debug_asset_eq 6. update ADR 0003 decision outcome and add option 4 re: creating events only from Update
1 parent 633bb54 commit 2891853

File tree

5 files changed

+217
-197
lines changed

5 files changed

+217
-197
lines changed

docs/adr/0003_events.md

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Authors: @notmandatory
55
* Date: 2025-09-21
66
* Targeted modules: wallet
7-
* Associated tickets/PRs: #6, #310
7+
* Associated tickets/PRs: #6, #310, #319
88

99
## Context and Problem Statement
1010

@@ -89,10 +89,30 @@ return the list of `WalletEvent` enums.
8989
* Could be confusing to users which function to use, the original or new one.
9090
* If in a future breaking release we decide to always return events we'll need to deprecate `Wallet::apply_update_events`.
9191

92+
#### Option 4: Create events directly from Wallet::Update
93+
94+
The `wallet::Update` structure passed into the `Wallet::apply_update` function contains any new transaction or
95+
blockchain data found in a `FullScanResponse` or `SyncResponse`. Events could be generated from only this data.
96+
97+
**Pros:**
98+
99+
* No further wallet lookups is required to create events, it would be more efficient.
100+
* Could be implemented as a function directly on the `wallet::Update` structure, a non-breaking API change.
101+
102+
**Cons:**
103+
104+
* A `wallet::Update` only contains the blocks, tx, and anchors found during a sync or full scan. It does not show how
105+
this data changes the canonical status of already known blocks and tx.
106+
92107
## Decision Outcome
93108

94-
Chosen option: "Option 3", because it can be delivered to users in the next minor release. This option also lets us
95-
get user feedback and see how the events are used before forcing all users to generate them during an update.
109+
Chosen option:
110+
111+
"Option 3" for the 2.2 release because it can be delivered to users as a minor release. This option also
112+
lets us get user feedback and see how the events are used before forcing all users to generate them during an update.
113+
114+
"Option 2" for the 3.0 release to simplify the API by only using one function `apply_update` that will now return
115+
events.
96116

97117
### Positive Consequences
98118

src/wallet/event.rs

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//! User facing wallet events.
2+
3+
use crate::collections::BTreeMap;
4+
use crate::wallet::ChainPosition::{Confirmed, Unconfirmed};
5+
use crate::Wallet;
6+
use alloc::sync::Arc;
7+
use alloc::vec::Vec;
8+
use bitcoin::{Transaction, Txid};
9+
use chain::{BlockId, ChainPosition, ConfirmationBlockTime};
10+
11+
/// Events representing changes to wallet transactions.
12+
///
13+
/// Returned after calling
14+
/// [`Wallet::apply_update_events`](crate::wallet::Wallet::apply_update_events).
15+
#[derive(Debug, Clone, PartialEq, Eq)]
16+
#[non_exhaustive]
17+
pub enum WalletEvent {
18+
/// The latest chain tip known to the wallet changed.
19+
ChainTipChanged {
20+
/// Previous chain tip.
21+
old_tip: BlockId,
22+
/// New chain tip.
23+
new_tip: BlockId,
24+
},
25+
/// A transaction is now confirmed.
26+
///
27+
/// If the transaction was previously unconfirmed `old_block_time` will be `None`.
28+
///
29+
/// If a confirmed transaction is now re-confirmed in a new block `old_block_time` will contain
30+
/// the block id and the time it was previously confirmed. This can happen after a chain
31+
/// reorg.
32+
TxConfirmed {
33+
/// Transaction id.
34+
txid: Txid,
35+
/// Transaction.
36+
tx: Arc<Transaction>,
37+
/// Confirmation block time.
38+
block_time: ConfirmationBlockTime,
39+
/// Old confirmation block and time if previously confirmed in a different block.
40+
old_block_time: Option<ConfirmationBlockTime>,
41+
},
42+
/// A transaction is now unconfirmed.
43+
///
44+
/// If the transaction is first seen in the mempool `old_block_time` will be `None`.
45+
///
46+
/// If a previously confirmed transaction is now seen in the mempool `old_block_time` will
47+
/// contain the block id and the time it was previously confirmed. This can happen after a
48+
/// chain reorg.
49+
TxUnconfirmed {
50+
/// Transaction id.
51+
txid: Txid,
52+
/// Transaction.
53+
tx: Arc<Transaction>,
54+
/// Old confirmation block and time, if previously confirmed.
55+
old_block_time: Option<ConfirmationBlockTime>,
56+
},
57+
/// An unconfirmed transaction was replaced.
58+
///
59+
/// This can happen after an RBF is broadcast or if a third party double spends an input of
60+
/// a received payment transaction before it is confirmed.
61+
///
62+
/// The conflicts field contains the txid and vin (in which it conflicts) of the conflicting
63+
/// transactions.
64+
TxReplaced {
65+
/// Transaction id.
66+
txid: Txid,
67+
/// Transaction.
68+
tx: Arc<Transaction>,
69+
/// Conflicting transaction ids.
70+
conflicts: Vec<(usize, Txid)>,
71+
},
72+
/// Unconfirmed transaction dropped.
73+
///
74+
/// The transaction was dropped from the local mempool. This is generally due to the fee rate
75+
/// being too low. The transaction can still reappear in the mempool in the future resulting in
76+
/// a [`WalletEvent::TxUnconfirmed`] event.
77+
TxDropped {
78+
/// Transaction id.
79+
txid: Txid,
80+
/// Transaction.
81+
tx: Arc<Transaction>,
82+
},
83+
}
84+
85+
/// Generate events by comparing the chain tip and wallet transactions before and after applying
86+
/// `wallet::Update` to `Wallet`. Any changes are added to the list of returned `WalletEvent`s.
87+
pub(crate) fn wallet_events(
88+
wallet: &mut Wallet,
89+
chain_tip1: BlockId,
90+
chain_tip2: BlockId,
91+
wallet_txs1: BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>,
92+
wallet_txs2: BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>,
93+
) -> Vec<WalletEvent> {
94+
let mut events: Vec<WalletEvent> = Vec::new();
95+
96+
// find chain tip change
97+
if chain_tip1 != chain_tip2 {
98+
events.push(WalletEvent::ChainTipChanged {
99+
old_tip: chain_tip1,
100+
new_tip: chain_tip2,
101+
});
102+
}
103+
104+
// find transaction canonical status changes
105+
wallet_txs2.iter().for_each(|(txid2, (tx2, pos2))| {
106+
if let Some((tx1, pos1)) = wallet_txs1.get(txid2) {
107+
debug_assert_eq!(tx1.compute_txid(), *txid2);
108+
match (pos1, pos2) {
109+
(Unconfirmed { .. }, Confirmed { anchor, .. }) => {
110+
events.push(WalletEvent::TxConfirmed {
111+
txid: *txid2,
112+
tx: tx2.clone(),
113+
block_time: *anchor,
114+
old_block_time: None,
115+
});
116+
}
117+
(Confirmed { anchor, .. }, Unconfirmed { .. }) => {
118+
events.push(WalletEvent::TxUnconfirmed {
119+
txid: *txid2,
120+
tx: tx2.clone(),
121+
old_block_time: Some(*anchor),
122+
});
123+
}
124+
(
125+
Confirmed {
126+
anchor: anchor1, ..
127+
},
128+
Confirmed {
129+
anchor: anchor2, ..
130+
},
131+
) => {
132+
if *anchor1 != *anchor2 {
133+
events.push(WalletEvent::TxConfirmed {
134+
txid: *txid2,
135+
tx: tx2.clone(),
136+
block_time: *anchor2,
137+
old_block_time: Some(*anchor1),
138+
});
139+
}
140+
}
141+
(Unconfirmed { .. }, Unconfirmed { .. }) => {
142+
// do nothing if still unconfirmed
143+
}
144+
}
145+
} else {
146+
match pos2 {
147+
Confirmed { anchor, .. } => {
148+
events.push(WalletEvent::TxConfirmed {
149+
txid: *txid2,
150+
tx: tx2.clone(),
151+
block_time: *anchor,
152+
old_block_time: None,
153+
});
154+
}
155+
Unconfirmed { .. } => {
156+
events.push(WalletEvent::TxUnconfirmed {
157+
txid: *txid2,
158+
tx: tx2.clone(),
159+
old_block_time: None,
160+
});
161+
}
162+
}
163+
}
164+
});
165+
166+
// find tx that are no longer canonical
167+
wallet_txs1.iter().for_each(|(txid1, (tx1, _))| {
168+
if !wallet_txs2.contains_key(txid1) {
169+
let conflicts = wallet.tx_graph().direct_conflicts(tx1).collect::<Vec<_>>();
170+
if !conflicts.is_empty() {
171+
events.push(WalletEvent::TxReplaced {
172+
txid: *txid1,
173+
tx: tx1.clone(),
174+
conflicts,
175+
});
176+
} else {
177+
events.push(WalletEvent::TxDropped {
178+
txid: *txid1,
179+
tx: tx1.clone(),
180+
});
181+
}
182+
}
183+
});
184+
185+
events
186+
}

src/wallet/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,12 @@ use crate::wallet::{
7575
tx_builder::{FeePolicy, TxBuilder, TxParams},
7676
utils::{check_nsequence_rbf, After, Older, SecpCtx},
7777
};
78+
use event::wallet_events;
7879

7980
// re-exports
80-
use crate::event::{wallet_events, WalletEvent};
8181
pub use bdk_chain::Balance;
8282
pub use changeset::ChangeSet;
83+
pub use event::WalletEvent;
8384
pub use params::*;
8485
pub use persisted::*;
8586
pub use utils::IsDust;

tests/wallet_event.rs

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use bdk_chain::{BlockId, CheckPoint, ConfirmationBlockTime};
22
use bdk_wallet::event::WalletEvent;
33
use bdk_wallet::test_utils::{get_test_wpkh_and_change_desc, new_wallet_and_funding_update};
4-
use bdk_wallet::{SignOptions, Update};
4+
use bdk_wallet::Update;
55
use bitcoin::hashes::Hash;
66
use bitcoin::{Address, Amount, BlockHash, FeeRate};
77
use core::str::FromStr;
@@ -76,8 +76,7 @@ fn test_tx_replaced_event() {
7676
.assume_checked(),
7777
Amount::from_sat(10_000),
7878
);
79-
let mut psbt = builder.finish().unwrap();
80-
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
79+
let psbt = builder.finish().unwrap();
8180
let orig_tx = Arc::new(psbt.extract_tx().unwrap());
8281
let orig_txid = orig_tx.compute_txid();
8382

@@ -95,8 +94,7 @@ fn test_tx_replaced_event() {
9594
// create rbf tx
9695
let mut builder = wallet.build_fee_bump(orig_txid).unwrap();
9796
builder.fee_rate(FeeRate::from_sat_per_vb(10).unwrap());
98-
let mut psbt = builder.finish().unwrap();
99-
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
97+
let psbt = builder.finish().unwrap();
10098
let rbf_tx = Arc::new(psbt.extract_tx().unwrap());
10199
let rbf_txid = rbf_tx.compute_txid();
102100

@@ -131,8 +129,7 @@ fn test_tx_confirmed_event() {
131129
.assume_checked(),
132130
Amount::from_sat(10_000),
133131
);
134-
let mut psbt = builder.finish().unwrap();
135-
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
132+
let psbt = builder.finish().unwrap();
136133
let new_tx = Arc::new(psbt.extract_tx().unwrap());
137134
let new_txid = new_tx.compute_txid();
138135

@@ -189,8 +186,7 @@ fn test_tx_confirmed_new_block_event() {
189186
.assume_checked(),
190187
Amount::from_sat(10_000),
191188
);
192-
let mut psbt = builder.finish().unwrap();
193-
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
189+
let psbt = builder.finish().unwrap();
194190
let new_tx = Arc::new(psbt.extract_tx().unwrap());
195191
let new_txid = new_tx.compute_txid();
196192

@@ -274,8 +270,7 @@ fn test_tx_dropped_event() {
274270
.assume_checked(),
275271
Amount::from_sat(10_000),
276272
);
277-
let mut psbt = builder.finish().unwrap();
278-
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
273+
let psbt = builder.finish().unwrap();
279274
let new_tx = Arc::new(psbt.extract_tx().unwrap());
280275
let new_txid = new_tx.compute_txid();
281276

0 commit comments

Comments
 (0)