Skip to content

Commit 6921b2b

Browse files
committed
feat(wallet): add WalletEvent and Wallet::apply_update_events
WalletEvent is a enum of user facing events that are generated when a sync update is applied to a wallet using the Wallet::apply_update_events function.
1 parent dc82330 commit 6921b2b

File tree

4 files changed

+593
-65
lines changed

4 files changed

+593
-65
lines changed

wallet/src/test_utils.rs

Lines changed: 62 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use alloc::string::ToString;
44
use alloc::sync::Arc;
55
use core::str::FromStr;
66

7-
use bdk_chain::{BlockId, ConfirmationBlockTime, TxUpdate};
7+
use bdk_chain::{BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate};
88
use bitcoin::{
99
absolute, hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint,
1010
Transaction, TxIn, TxOut, Txid,
@@ -22,13 +22,42 @@ pub fn get_funded_wallet(descriptor: &str, change_descriptor: &str) -> (Wallet,
2222
}
2323

2424
fn new_funded_wallet(descriptor: &str, change_descriptor: Option<&str>) -> (Wallet, Txid) {
25+
let (mut wallet, txid, update) = new_wallet_and_funding_update(descriptor, change_descriptor);
26+
wallet.apply_update(update).unwrap();
27+
(wallet, txid)
28+
}
29+
30+
/// Return a fake wallet that appears to be funded for testing.
31+
///
32+
/// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
33+
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
34+
/// sats are the transaction fee.
35+
pub fn get_funded_wallet_single(descriptor: &str) -> (Wallet, Txid) {
36+
new_funded_wallet(descriptor, None)
37+
}
38+
39+
/// Get funded segwit wallet
40+
pub fn get_funded_wallet_wpkh() -> (Wallet, Txid) {
41+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
42+
get_funded_wallet(desc, change_desc)
43+
}
44+
45+
/// Get unfunded wallet and wallet update that funds it
46+
///
47+
/// The funding update contains a tx with a 76_000 sats input and two outputs, one spending
48+
/// 25_000 to a foreign address and one returning 50_000 back to the wallet as
49+
/// change. The remaining 1000 sats are the transaction fee.
50+
pub fn new_wallet_and_funding_update(
51+
descriptor: &str,
52+
change_descriptor: Option<&str>,
53+
) -> (Wallet, Txid, Update) {
2554
let params = if let Some(change_desc) = change_descriptor {
2655
Wallet::create(descriptor.to_string(), change_desc.to_string())
2756
} else {
2857
Wallet::create_single(descriptor.to_string())
2958
};
3059

31-
let mut wallet = params
60+
let wallet = params
3261
.network(Network::Regtest)
3362
.create_wallet_no_persist()
3463
.expect("descriptors must be valid");
@@ -39,6 +68,8 @@ fn new_funded_wallet(descriptor: &str, change_descriptor: Option<&str>) -> (Wall
3968
.require_network(Network::Regtest)
4069
.unwrap();
4170

71+
let mut update = Update::default();
72+
4273
let tx0 = Transaction {
4374
output: vec![TxOut {
4475
value: Amount::from_sat(76_000),
@@ -67,71 +98,37 @@ fn new_funded_wallet(descriptor: &str, change_descriptor: Option<&str>) -> (Wall
6798
],
6899
..new_tx(0)
69100
};
101+
let txid1 = tx1.compute_txid();
70102

71-
insert_checkpoint(
72-
&mut wallet,
73-
BlockId {
74-
height: 42,
75-
hash: BlockHash::all_zeros(),
76-
},
77-
);
78-
insert_checkpoint(
79-
&mut wallet,
80-
BlockId {
81-
height: 1_000,
82-
hash: BlockHash::all_zeros(),
83-
},
84-
);
85-
insert_checkpoint(
86-
&mut wallet,
87-
BlockId {
88-
height: 2_000,
89-
hash: BlockHash::all_zeros(),
90-
},
91-
);
92-
93-
insert_tx(&mut wallet, tx0.clone());
94-
insert_anchor(
95-
&mut wallet,
96-
tx0.compute_txid(),
97-
ConfirmationBlockTime {
98-
block_id: BlockId {
99-
height: 1_000,
100-
hash: BlockHash::all_zeros(),
101-
},
102-
confirmation_time: 100,
103-
},
104-
);
105-
106-
insert_tx(&mut wallet, tx1.clone());
107-
insert_anchor(
108-
&mut wallet,
109-
tx1.compute_txid(),
110-
ConfirmationBlockTime {
111-
block_id: BlockId {
112-
height: 2_000,
113-
hash: BlockHash::all_zeros(),
114-
},
115-
confirmation_time: 200,
116-
},
117-
);
118-
119-
(wallet, tx1.compute_txid())
120-
}
121-
122-
/// Return a fake wallet that appears to be funded for testing.
123-
///
124-
/// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
125-
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
126-
/// sats are the transaction fee.
127-
pub fn get_funded_wallet_single(descriptor: &str) -> (Wallet, Txid) {
128-
new_funded_wallet(descriptor, None)
129-
}
103+
let b0 = BlockId {
104+
height: 0,
105+
hash: BlockHash::from_slice(wallet.network().chain_hash().as_bytes()).unwrap(),
106+
};
107+
let b1 = BlockId {
108+
height: 42,
109+
hash: BlockHash::all_zeros(),
110+
};
111+
let b2 = BlockId {
112+
height: 1000,
113+
hash: BlockHash::all_zeros(),
114+
};
115+
let a2 = ConfirmationBlockTime {
116+
block_id: b2,
117+
confirmation_time: 100,
118+
};
119+
let b3 = BlockId {
120+
height: 2000,
121+
hash: BlockHash::all_zeros(),
122+
};
123+
let a3 = ConfirmationBlockTime {
124+
block_id: b3,
125+
confirmation_time: 200,
126+
};
127+
update.chain = CheckPoint::from_block_ids([b0, b1, b2, b3]).ok();
128+
update.tx_update.anchors = [(a2, tx0.compute_txid()), (a3, tx1.compute_txid())].into();
129+
update.tx_update.txs = [Arc::new(tx0), Arc::new(tx1)].into();
130130

131-
/// Get funded segwit wallet
132-
pub fn get_funded_wallet_wpkh() -> (Wallet, Txid) {
133-
let (desc, change_desc) = get_test_wpkh_and_change_desc();
134-
get_funded_wallet(desc, change_desc)
131+
(wallet, txid1, update)
135132
}
136133

137134
/// `pkh` single key descriptor

wallet/src/wallet/event.rs

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

0 commit comments

Comments
 (0)