Skip to content

Commit 62060fd

Browse files
committed
feat: add utilities to test persistence of wallet
Added the following functions:- - `persist_wallet_changeset`: tests if each field of wallet ChangeSet is persisted. - `persist_multiple_wallet_changesets`: tests if multiple wallets can be persisted in a single file. - `persist_network`: tests if network is persisted - `persist_keychains`: tests if descriptors are persisted - `persist_single_keychain`: tests if descriptor in single keychain wallet is persisted.
1 parent c21ff21 commit 62060fd

File tree

3 files changed

+375
-1
lines changed

3 files changed

+375
-1
lines changed

wallet/Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ bitcoin = { version = "0.32.6", features = [ "serde", "base64" ], default-featur
2222
serde = { version = "^1.0", features = ["derive"] }
2323
serde_json = { version = "^1.0" }
2424
bdk_chain = { version = "0.23.1", features = [ "miniscript", "serde" ], default-features = false }
25+
anyhow = { version = "1.0.98", optional = true }
26+
tempfile = { version = "3.20.0", optional = true }
27+
bdk_testenv = { version = "0.13.0", optional = true}
2528

2629
# Optional dependencies
2730
bip39 = { version = "2.0", optional = true }
@@ -35,7 +38,7 @@ all-keys = ["keys-bip39"]
3538
keys-bip39 = ["bip39"]
3639
rusqlite = ["bdk_chain/rusqlite"]
3740
file_store = ["bdk_file_store"]
38-
test-utils = ["std"]
41+
test-utils = ["std", "anyhow", "tempfile", "bdk_testenv"]
3942

4043
[dev-dependencies]
4144
assert_matches = "1.5.0"

wallet/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ pub mod keys;
3131
pub mod psbt;
3232
#[cfg(feature = "test-utils")]
3333
pub mod test_utils;
34+
35+
#[cfg(feature = "test-utils")]
36+
pub mod persist_test_utils;
37+
3438
mod types;
3539
mod wallet;
3640

wallet/src/persist_test_utils.rs

Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
use crate::{
2+
bitcoin::{
3+
absolute, key::Secp256k1, transaction, Address, Amount, Network, OutPoint, ScriptBuf,
4+
Transaction, TxIn, TxOut, Txid,
5+
},
6+
chain::{
7+
keychain_txout::{self},
8+
local_chain, tx_graph, ConfirmationBlockTime, DescriptorExt, Merge, SpkIterator,
9+
},
10+
miniscript::descriptor::{Descriptor, DescriptorPublicKey},
11+
ChangeSet, WalletPersister,
12+
};
13+
use bdk_testenv::{block_id, hash};
14+
use std::fmt::Debug;
15+
use std::path::Path;
16+
use std::str::FromStr;
17+
use std::sync::Arc;
18+
19+
const DESCRIPTORS: [&str; 4] = [
20+
"tr([5940b9b9/86'/0'/0']tpubDDVNqmq75GNPWQ9UNKfP43UwjaHU4GYfoPavojQbfpyfZp2KetWgjGBRRAy4tYCrAA6SB11mhQAkqxjh1VtQHyKwT4oYxpwLaGHvoKmtxZf/0/*)#44aqnlam",
21+
"tr([5940b9b9/86'/0'/0']tpubDDVNqmq75GNPWQ9UNKfP43UwjaHU4GYfoPavojQbfpyfZp2KetWgjGBRRAy4tYCrAA6SB11mhQAkqxjh1VtQHyKwT4oYxpwLaGHvoKmtxZf/1/*)#ypcpw2dr",
22+
"wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/0/*)#g0w0ymmw",
23+
"wpkh([41f2aed0/84h/1h/0h]tpubDDFSdQWw75hk1ewbwnNpPp5DvXFRKt68ioPoyJDY752cNHKkFxPWqkqCyCf4hxrEfpuxh46QisehL3m8Bi6MsAv394QVLopwbtfvryFQNUH/1/*)#emtwewtk",
24+
];
25+
26+
fn create_one_inp_one_out_tx(txid: Txid, amount: u64) -> Transaction {
27+
Transaction {
28+
version: transaction::Version::ONE,
29+
lock_time: absolute::LockTime::ZERO,
30+
input: vec![TxIn {
31+
previous_output: OutPoint::new(txid, 0),
32+
..TxIn::default()
33+
}],
34+
output: vec![TxOut {
35+
value: Amount::from_sat(amount),
36+
script_pubkey: Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
37+
.unwrap()
38+
.assume_checked()
39+
.script_pubkey(),
40+
}],
41+
}
42+
}
43+
44+
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> ScriptBuf {
45+
descriptor
46+
.derived_descriptor(&Secp256k1::verification_only(), index)
47+
.expect("must derive")
48+
.script_pubkey()
49+
}
50+
51+
pub fn persist_wallet_changeset<Store, CreateStore>(filename: &str, create_store: CreateStore)
52+
where
53+
CreateStore: Fn(&Path) -> anyhow::Result<Store>,
54+
Store: WalletPersister,
55+
Store::Error: Debug,
56+
{
57+
// create store
58+
let temp_dir = tempfile::tempdir().expect("must create tempdir");
59+
let file_path = temp_dir.path().join(filename);
60+
let mut store = create_store(&file_path).expect("store should get created");
61+
62+
// initialize store
63+
let changeset =
64+
WalletPersister::initialize(&mut store).expect("empty changeset should get loaded");
65+
assert_eq!(changeset, ChangeSet::default());
66+
67+
// create changeset
68+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[0].parse().unwrap();
69+
let change_descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[1].parse().unwrap();
70+
71+
let local_chain_changeset = local_chain::ChangeSet {
72+
blocks: [
73+
(910234, Some(hash!("B"))),
74+
(910233, Some(hash!("T"))),
75+
(910235, Some(hash!("C"))),
76+
]
77+
.into(),
78+
};
79+
80+
let tx1 = Arc::new(create_one_inp_one_out_tx(
81+
hash!("We_are_all_Satoshi"),
82+
30_000,
83+
));
84+
let tx2 = Arc::new(create_one_inp_one_out_tx(tx1.compute_txid(), 20_000));
85+
86+
let conf_anchor: ConfirmationBlockTime = ConfirmationBlockTime {
87+
block_id: block_id!(910234, "B"),
88+
confirmation_time: 1755317160,
89+
};
90+
91+
let tx_graph_changeset = tx_graph::ChangeSet::<ConfirmationBlockTime> {
92+
txs: [tx1.clone()].into(),
93+
txouts: [
94+
(
95+
OutPoint::new(hash!("Rust"), 0),
96+
TxOut {
97+
value: Amount::from_sat(1300),
98+
script_pubkey: spk_at_index(&descriptor, 4),
99+
},
100+
),
101+
(
102+
OutPoint::new(hash!("REDB"), 0),
103+
TxOut {
104+
value: Amount::from_sat(1400),
105+
script_pubkey: spk_at_index(&descriptor, 10),
106+
},
107+
),
108+
]
109+
.into(),
110+
anchors: [(conf_anchor, tx1.compute_txid())].into(),
111+
last_seen: [(tx1.compute_txid(), 1755317760)].into(),
112+
first_seen: [(tx1.compute_txid(), 1755317750)].into(),
113+
last_evicted: [(tx1.compute_txid(), 1755317760)].into(),
114+
};
115+
116+
let keychain_txout_changeset = keychain_txout::ChangeSet {
117+
last_revealed: [
118+
(descriptor.descriptor_id(), 12),
119+
(change_descriptor.descriptor_id(), 10),
120+
]
121+
.into(),
122+
spk_cache: [
123+
(
124+
descriptor.descriptor_id(),
125+
SpkIterator::new_with_range(&descriptor, 0..=37).collect(),
126+
),
127+
(
128+
change_descriptor.descriptor_id(),
129+
SpkIterator::new_with_range(&change_descriptor, 0..=35).collect(),
130+
),
131+
]
132+
.into(),
133+
};
134+
135+
let mut changeset = ChangeSet {
136+
descriptor: Some(descriptor.clone()),
137+
change_descriptor: Some(change_descriptor.clone()),
138+
network: Some(Network::Testnet),
139+
local_chain: local_chain_changeset,
140+
tx_graph: tx_graph_changeset,
141+
indexer: keychain_txout_changeset,
142+
};
143+
144+
// persist and load
145+
WalletPersister::persist(&mut store, &changeset).expect("changeset should get persisted");
146+
147+
let changeset_read =
148+
WalletPersister::initialize(&mut store).expect("changeset should get loaded");
149+
150+
assert_eq!(changeset, changeset_read);
151+
152+
// create another changeset
153+
let local_chain_changeset = local_chain::ChangeSet {
154+
blocks: [(910236, Some(hash!("BDK")))].into(),
155+
};
156+
157+
let conf_anchor: ConfirmationBlockTime = ConfirmationBlockTime {
158+
block_id: block_id!(910236, "BDK"),
159+
confirmation_time: 1755317760,
160+
};
161+
162+
let tx_graph_changeset = tx_graph::ChangeSet::<ConfirmationBlockTime> {
163+
txs: [tx2.clone()].into(),
164+
txouts: [(
165+
OutPoint::new(hash!("Bitcoin_fixes_things"), 0),
166+
TxOut {
167+
value: Amount::from_sat(10000),
168+
script_pubkey: spk_at_index(&descriptor, 21),
169+
},
170+
)]
171+
.into(),
172+
anchors: [(conf_anchor, tx2.compute_txid())].into(),
173+
last_seen: [(tx2.compute_txid(), 1755317700)].into(),
174+
first_seen: [(tx2.compute_txid(), 1755317700)].into(),
175+
last_evicted: [(tx2.compute_txid(), 1755317760)].into(),
176+
};
177+
178+
let keychain_txout_changeset = keychain_txout::ChangeSet {
179+
last_revealed: [(descriptor.descriptor_id(), 14)].into(),
180+
spk_cache: [(
181+
descriptor.descriptor_id(),
182+
SpkIterator::new_with_range(&descriptor, 37..=39).collect(),
183+
)]
184+
.into(),
185+
};
186+
187+
let changeset_new = ChangeSet {
188+
descriptor: None,
189+
change_descriptor: None,
190+
network: None,
191+
local_chain: local_chain_changeset,
192+
tx_graph: tx_graph_changeset,
193+
indexer: keychain_txout_changeset,
194+
};
195+
196+
// persist, load and check if same as merged
197+
WalletPersister::persist(&mut store, &changeset_new).expect("changeset should get persisted");
198+
let changeset_read_new = WalletPersister::initialize(&mut store).unwrap();
199+
200+
changeset.merge(changeset_new);
201+
202+
assert_eq!(changeset, changeset_read_new);
203+
}
204+
205+
pub fn persist_multiple_wallet_changesets<Store, CreateStores>(
206+
filename: &str,
207+
create_dbs: CreateStores,
208+
) where
209+
CreateStores: Fn(&Path) -> anyhow::Result<(Store, Store)>,
210+
Store: WalletPersister,
211+
Store::Error: Debug,
212+
{
213+
// create stores
214+
let temp_dir = tempfile::tempdir().expect("must create tempdir");
215+
let file_path = temp_dir.path().join(filename);
216+
217+
let (mut store_first, mut store_sec) =
218+
create_dbs(&file_path).expect("store should get created");
219+
220+
// initialize first store
221+
let changeset =
222+
WalletPersister::initialize(&mut store_first).expect("should load empty changeset");
223+
assert_eq!(changeset, ChangeSet::default());
224+
225+
// create first changeset
226+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[0].parse().unwrap();
227+
let change_descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[1].parse().unwrap();
228+
229+
let changeset1 = ChangeSet {
230+
descriptor: Some(descriptor.clone()),
231+
change_descriptor: Some(change_descriptor.clone()),
232+
network: Some(Network::Testnet),
233+
..ChangeSet::default()
234+
};
235+
236+
// persist first changeset
237+
WalletPersister::persist(&mut store_first, &changeset1).expect("should persist changeset");
238+
239+
// initialize second store
240+
let changeset =
241+
WalletPersister::initialize(&mut store_sec).expect("should load empty changeset");
242+
assert_eq!(changeset, ChangeSet::default());
243+
244+
// create second changeset
245+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[2].parse().unwrap();
246+
let change_descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[3].parse().unwrap();
247+
248+
let changeset2 = ChangeSet {
249+
descriptor: Some(descriptor.clone()),
250+
change_descriptor: Some(change_descriptor.clone()),
251+
network: Some(Network::Testnet),
252+
..ChangeSet::default()
253+
};
254+
255+
// persist second changeset
256+
WalletPersister::persist(&mut store_sec, &changeset2).expect("should persist changeset");
257+
258+
// load first changeset
259+
let changeset_read =
260+
WalletPersister::initialize(&mut store_first).expect("should load persisted changeset1");
261+
assert_eq!(changeset_read, changeset1);
262+
263+
// load second changeset
264+
let changeset_read =
265+
WalletPersister::initialize(&mut store_sec).expect("should load persisted changeset2");
266+
assert_eq!(changeset_read, changeset2);
267+
}
268+
269+
pub fn persist_network<Store, CreateStore>(filename: &str, create_store: CreateStore)
270+
where
271+
CreateStore: Fn(&Path) -> anyhow::Result<Store>,
272+
Store: WalletPersister,
273+
Store::Error: Debug,
274+
{
275+
// create store
276+
let temp_dir = tempfile::tempdir().expect("must create tempdir");
277+
let file_path = temp_dir.path().join(filename);
278+
let mut store = create_store(&file_path).expect("store should get created");
279+
280+
// initialize store
281+
let changeset = WalletPersister::initialize(&mut store)
282+
.expect("should initialize and load empty changeset");
283+
assert_eq!(changeset, ChangeSet::default());
284+
285+
// persist the network
286+
let changeset = ChangeSet {
287+
network: Some(Network::Bitcoin),
288+
..ChangeSet::default()
289+
};
290+
WalletPersister::persist(&mut store, &changeset).expect("should persist changeset");
291+
292+
// read the persisted network
293+
let changeset_read =
294+
WalletPersister::initialize(&mut store).expect("should load persisted changeset");
295+
296+
assert_eq!(changeset_read.network, Some(Network::Bitcoin));
297+
}
298+
299+
pub fn persist_keychains<Store, CreateStore>(filename: &str, create_store: CreateStore)
300+
where
301+
CreateStore: Fn(&Path) -> anyhow::Result<Store>,
302+
Store: WalletPersister,
303+
Store::Error: Debug,
304+
{
305+
// create store
306+
let temp_dir = tempfile::tempdir().expect("must create tempdir");
307+
let file_path = temp_dir.path().join(filename);
308+
let mut store = create_store(&file_path).expect("store should get created");
309+
310+
// initialize store
311+
let changeset = WalletPersister::initialize(&mut store)
312+
.expect("should initialize and load empty changeset");
313+
assert_eq!(changeset, ChangeSet::default());
314+
315+
// persist the descriptors
316+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[1].parse().unwrap();
317+
let change_descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[0].parse().unwrap();
318+
319+
let changeset = ChangeSet {
320+
descriptor: Some(descriptor.clone()),
321+
change_descriptor: Some(change_descriptor.clone()),
322+
..ChangeSet::default()
323+
};
324+
325+
WalletPersister::persist(&mut store, &changeset).expect("should persist descriptors");
326+
327+
// load the descriptors
328+
let changeset_read =
329+
WalletPersister::initialize(&mut store).expect("should read persisted changeset");
330+
331+
assert_eq!(changeset_read.descriptor.unwrap(), descriptor);
332+
assert_eq!(changeset_read.change_descriptor.unwrap(), change_descriptor);
333+
}
334+
335+
pub fn persist_single_keychain<Store, CreateStore>(filename: &str, create_store: CreateStore)
336+
where
337+
CreateStore: Fn(&Path) -> anyhow::Result<Store>,
338+
Store: WalletPersister,
339+
Store::Error: Debug,
340+
{
341+
// create store
342+
let temp_dir = tempfile::tempdir().expect("must create tempdir");
343+
let file_path = temp_dir.path().join(filename);
344+
let mut store = create_store(&file_path).expect("store should get created");
345+
346+
// initialize store
347+
let changeset = WalletPersister::initialize(&mut store)
348+
.expect("should initialize and load empty changeset");
349+
assert_eq!(changeset, ChangeSet::default());
350+
351+
// persist descriptor
352+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[0].parse().unwrap();
353+
354+
let changeset = ChangeSet {
355+
descriptor: Some(descriptor.clone()),
356+
..ChangeSet::default()
357+
};
358+
359+
WalletPersister::persist(&mut store, &changeset).expect("should persist descriptors");
360+
361+
// load the descriptor
362+
let changeset_read =
363+
WalletPersister::initialize(&mut store).expect("should read persisted changeset");
364+
365+
assert_eq!(changeset_read.descriptor.unwrap(), descriptor);
366+
assert_eq!(changeset_read.change_descriptor, None);
367+
}

0 commit comments

Comments
 (0)