bdk_wallet/wallet/event.rs
1//! User facing wallet events.
2
3use crate::collections::BTreeMap;
4use crate::wallet::ChainPosition::{Confirmed, Unconfirmed};
5use crate::Wallet;
6use alloc::sync::Arc;
7use alloc::vec::Vec;
8use bitcoin::{Transaction, Txid};
9use 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]
17pub 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
85pub(crate) fn wallet_events(
86 wallet: &mut Wallet,
87 chain_tip1: BlockId,
88 chain_tip2: BlockId,
89 wallet_txs1: BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>,
90 wallet_txs2: BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>,
91) -> Vec<WalletEvent> {
92 let mut events: Vec<WalletEvent> = Vec::new();
93
94 if chain_tip1 != chain_tip2 {
95 events.push(WalletEvent::ChainTipChanged {
96 old_tip: chain_tip1,
97 new_tip: chain_tip2,
98 });
99 }
100
101 wallet_txs2.iter().for_each(|(txid2, (tx2, cp2))| {
102 if let Some((tx1, cp1)) = wallet_txs1.get(txid2) {
103 assert_eq!(tx1.compute_txid(), *txid2);
104 match (cp1, cp2) {
105 (Unconfirmed { .. }, Confirmed { anchor, .. }) => {
106 events.push(WalletEvent::TxConfirmed {
107 txid: *txid2,
108 tx: tx2.clone(),
109 block_time: *anchor,
110 old_block_time: None,
111 });
112 }
113 (Confirmed { anchor, .. }, Unconfirmed { .. }) => {
114 events.push(WalletEvent::TxUnconfirmed {
115 txid: *txid2,
116 tx: tx2.clone(),
117 old_block_time: Some(*anchor),
118 });
119 }
120 (
121 Confirmed {
122 anchor: anchor1, ..
123 },
124 Confirmed {
125 anchor: anchor2, ..
126 },
127 ) => {
128 if *anchor1 != *anchor2 {
129 events.push(WalletEvent::TxConfirmed {
130 txid: *txid2,
131 tx: tx2.clone(),
132 block_time: *anchor2,
133 old_block_time: Some(*anchor1),
134 });
135 }
136 }
137 (Unconfirmed { .. }, Unconfirmed { .. }) => {
138 // do nothing if still unconfirmed
139 }
140 }
141 } else {
142 match cp2 {
143 Confirmed { anchor, .. } => {
144 events.push(WalletEvent::TxConfirmed {
145 txid: *txid2,
146 tx: tx2.clone(),
147 block_time: *anchor,
148 old_block_time: None,
149 });
150 }
151 Unconfirmed { .. } => {
152 events.push(WalletEvent::TxUnconfirmed {
153 txid: *txid2,
154 tx: tx2.clone(),
155 old_block_time: None,
156 });
157 }
158 }
159 }
160 });
161
162 // find tx that are no longer canonical
163 wallet_txs1.iter().for_each(|(txid1, (tx1, _))| {
164 if !wallet_txs2.contains_key(txid1) {
165 let conflicts = wallet.tx_graph().direct_conflicts(tx1).collect::<Vec<_>>();
166 if !conflicts.is_empty() {
167 events.push(WalletEvent::TxReplaced {
168 txid: *txid1,
169 tx: tx1.clone(),
170 conflicts,
171 });
172 } else {
173 events.push(WalletEvent::TxDropped {
174 txid: *txid1,
175 tx: tx1.clone(),
176 });
177 }
178 }
179 });
180
181 events
182}