Skip to content

Commit 0c3361c

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 b183d51 commit 0c3361c

File tree

4 files changed

+626
-65
lines changed

4 files changed

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

0 commit comments

Comments
 (0)