Skip to content

Commit ec3d1e0

Browse files
notmandatorytnullValuedMammal
committed
feat(wallet): Add Wallet::apply_update_events
`WalletEvent` is an enum of user facing events that are generated when an `Update` is applied to the wallet. - Add `Wallet::apply_block_events` and `apply_block_connected_to_events` Co-authored-by: Elias Rohrer <dev@tnull.de> Co-authored-by: valued mammal <valuedmammal@protonmail.com>
1 parent 6467969 commit ec3d1e0

2 files changed

Lines changed: 328 additions & 1 deletion

File tree

src/wallet/event.rs

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
//! User facing wallet events.
22
3+
use crate::collections::BTreeMap;
4+
use crate::wallet::ChainPosition::{Confirmed, Unconfirmed};
5+
use crate::Wallet;
36
use alloc::sync::Arc;
47
use alloc::vec::Vec;
58
use bitcoin::{Transaction, Txid};
6-
use chain::{BlockId, ConfirmationBlockTime};
9+
use chain::{BlockId, ChainPosition, ConfirmationBlockTime};
710

811
/// Events representing changes to wallet transactions.
912
///
@@ -78,3 +81,106 @@ pub enum WalletEvent {
7881
tx: Arc<Transaction>,
7982
},
8083
}
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: &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: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2380,6 +2380,126 @@ impl Wallet {
23802380
Ok(())
23812381
}
23822382

2383+
/// Applies an update to the wallet, stages the changes, and returns events.
2384+
///
2385+
/// Usually you create an `update` by interacting with some blockchain data source and inserting
2386+
/// transactions related to your wallet into it. Staged changes are NOT persisted.
2387+
///
2388+
/// After applying updates you should process the events in your app before persisting the
2389+
/// staged wallet changes. For an example of how to persist staged wallet changes see
2390+
/// [`Wallet::reveal_next_address`].
2391+
///
2392+
/// ```rust,no_run
2393+
/// # use bitcoin::*;
2394+
/// # use bdk_wallet::*;
2395+
/// use bdk_wallet::WalletEvent;
2396+
/// # let wallet_update = Update::default();
2397+
/// # let mut wallet = doctest_wallet!();
2398+
/// let events = wallet.apply_update_events(wallet_update)?;
2399+
/// // Handle wallet relevant events from this update.
2400+
/// events.iter().for_each(|event| {
2401+
/// match event {
2402+
/// // The chain tip changed.
2403+
/// WalletEvent::ChainTipChanged { old_tip, new_tip } => {
2404+
/// todo!() // handle event
2405+
/// }
2406+
/// // An unconfirmed tx is now confirmed in a block.
2407+
/// WalletEvent::TxConfirmed {
2408+
/// txid,
2409+
/// tx,
2410+
/// block_time,
2411+
/// old_block_time: None,
2412+
/// } => {
2413+
/// todo!() // handle event
2414+
/// }
2415+
/// // A confirmed tx is now confirmed in a new block (reorg).
2416+
/// WalletEvent::TxConfirmed {
2417+
/// txid,
2418+
/// tx,
2419+
/// block_time,
2420+
/// old_block_time: Some(old_block_time),
2421+
/// } => {
2422+
/// todo!() // handle event
2423+
/// }
2424+
/// // A new unconfirmed tx was seen in the mempool.
2425+
/// WalletEvent::TxUnconfirmed {
2426+
/// txid,
2427+
/// tx,
2428+
/// old_block_time: None,
2429+
/// } => {
2430+
/// todo!() // handle event
2431+
/// }
2432+
/// // A previously confirmed tx in now unconfirmed in the mempool (reorg).
2433+
/// WalletEvent::TxUnconfirmed {
2434+
/// txid,
2435+
/// tx,
2436+
/// old_block_time: Some(old_block_time),
2437+
/// } => {
2438+
/// todo!() // handle event
2439+
/// }
2440+
/// // An unconfirmed tx was replaced in the mempool (RBF or double spent input).
2441+
/// WalletEvent::TxReplaced {
2442+
/// txid,
2443+
/// tx,
2444+
/// conflicts,
2445+
/// } => {
2446+
/// todo!() // handle event
2447+
/// }
2448+
/// // An unconfirmed tx was dropped from the mempool (fee too low).
2449+
/// WalletEvent::TxDropped { txid, tx } => {
2450+
/// todo!() // handle event
2451+
/// }
2452+
/// _ => {
2453+
/// // unexpected event, do nothing
2454+
/// }
2455+
/// }
2456+
/// // take staged wallet changes
2457+
/// let staged = wallet.take_staged();
2458+
/// // persist staged changes
2459+
/// });
2460+
/// # Ok::<(), anyhow::Error>(())
2461+
/// ```
2462+
/// [`TxBuilder`]: crate::TxBuilder
2463+
pub fn apply_update_events(
2464+
&mut self,
2465+
update: impl Into<Update>,
2466+
) -> Result<Vec<WalletEvent>, CannotConnectError> {
2467+
// snapshot of chain tip and transactions before update
2468+
let chain_tip1 = self.chain.tip().block_id();
2469+
let wallet_txs1 = self
2470+
.transactions()
2471+
.map(|wtx| {
2472+
(
2473+
wtx.tx_node.txid,
2474+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2475+
)
2476+
})
2477+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2478+
2479+
// apply update
2480+
self.apply_update(update)?;
2481+
2482+
// chain tip and transactions after update
2483+
let chain_tip2 = self.chain.tip().block_id();
2484+
let wallet_txs2 = self
2485+
.transactions()
2486+
.map(|wtx| {
2487+
(
2488+
wtx.tx_node.txid,
2489+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2490+
)
2491+
})
2492+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2493+
2494+
Ok(wallet_events(
2495+
self,
2496+
chain_tip1,
2497+
chain_tip2,
2498+
wallet_txs1,
2499+
wallet_txs2,
2500+
))
2501+
}
2502+
23832503
/// Get a reference of the staged [`ChangeSet`] that is yet to be committed (if any).
23842504
pub fn staged(&self) -> Option<&ChangeSet> {
23852505
if self.stage.is_empty() {
@@ -2490,6 +2610,57 @@ impl Wallet {
24902610
})
24912611
}
24922612

2613+
/// Introduces a `block` of `height` to the wallet, and tries to connect it to the
2614+
/// `prev_blockhash` of the block's header.
2615+
///
2616+
/// This is a convenience method that is equivalent to calling
2617+
/// [`apply_block_connected_to_events`] with `prev_blockhash` and `height-1` as the
2618+
/// `connected_to` parameter.
2619+
///
2620+
/// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s.
2621+
///
2622+
/// [`apply_block_connected_to_events`]: Self::apply_block_connected_to_events
2623+
/// [`apply_update_events`]: Self::apply_update_events
2624+
pub fn apply_block_events(
2625+
&mut self,
2626+
block: &Block,
2627+
height: u32,
2628+
) -> Result<Vec<WalletEvent>, CannotConnectError> {
2629+
// snapshot of chain tip and transactions before update
2630+
let chain_tip1 = self.chain.tip().block_id();
2631+
let wallet_txs1 = self
2632+
.transactions()
2633+
.map(|wtx| {
2634+
(
2635+
wtx.tx_node.txid,
2636+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2637+
)
2638+
})
2639+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2640+
2641+
self.apply_block(block, height)?;
2642+
2643+
// chain tip and transactions after update
2644+
let chain_tip2 = self.chain.tip().block_id();
2645+
let wallet_txs2 = self
2646+
.transactions()
2647+
.map(|wtx| {
2648+
(
2649+
wtx.tx_node.txid,
2650+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2651+
)
2652+
})
2653+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2654+
2655+
Ok(wallet_events(
2656+
self,
2657+
chain_tip1,
2658+
chain_tip2,
2659+
wallet_txs1,
2660+
wallet_txs2,
2661+
))
2662+
}
2663+
24932664
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
24942665
/// block to the internal chain.
24952666
///
@@ -2521,6 +2692,56 @@ impl Wallet {
25212692
Ok(())
25222693
}
25232694

2695+
/// Applies relevant transactions from `block` of `height` to the wallet, and connects the
2696+
/// block to the internal chain.
2697+
///
2698+
/// See [`apply_block_connected_to`] for more information.
2699+
///
2700+
/// See [`apply_update_events`] for more information on the returned [`WalletEvent`]s.
2701+
///
2702+
/// [`apply_block_connected_to`]: Self::apply_block_connected_to
2703+
/// [`apply_update_events`]: Self::apply_update_events
2704+
pub fn apply_block_connected_to_events(
2705+
&mut self,
2706+
block: &Block,
2707+
height: u32,
2708+
connected_to: BlockId,
2709+
) -> Result<Vec<WalletEvent>, ApplyHeaderError> {
2710+
// snapshot of chain tip and transactions before update
2711+
let chain_tip1 = self.chain.tip().block_id();
2712+
let wallet_txs1 = self
2713+
.transactions()
2714+
.map(|wtx| {
2715+
(
2716+
wtx.tx_node.txid,
2717+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2718+
)
2719+
})
2720+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2721+
2722+
self.apply_block_connected_to(block, height, connected_to)?;
2723+
2724+
// chain tip and transactions after update
2725+
let chain_tip2 = self.chain.tip().block_id();
2726+
let wallet_txs2 = self
2727+
.transactions()
2728+
.map(|wtx| {
2729+
(
2730+
wtx.tx_node.txid,
2731+
(wtx.tx_node.tx.clone(), wtx.chain_position),
2732+
)
2733+
})
2734+
.collect::<BTreeMap<Txid, (Arc<Transaction>, ChainPosition<ConfirmationBlockTime>)>>();
2735+
2736+
Ok(wallet_events(
2737+
self,
2738+
chain_tip1,
2739+
chain_tip2,
2740+
wallet_txs1,
2741+
wallet_txs2,
2742+
))
2743+
}
2744+
25242745
/// Apply relevant unconfirmed transactions to the wallet.
25252746
///
25262747
/// Transactions that are not relevant are filtered out.

0 commit comments

Comments
 (0)