Skip to content

Commit 8fc1fe2

Browse files
committed
feat(persist_test_utils): Add persist_wallet_changeset_async
1 parent 35502e0 commit 8fc1fe2

1 file changed

Lines changed: 262 additions & 1 deletion

File tree

src/persist_test_utils.rs

Lines changed: 262 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
//! Utilities for testing custom persistence backends for `bdk_wallet`
22
3+
// TODO:
4+
// - Remove tempfile, anyhow as they should stay dev-dependencies
5+
// - Extract reusable changeset helper functions
6+
7+
use alloc::boxed::Box;
8+
use core::fmt;
9+
#[cfg(feature = "std")]
10+
use std::error::Error as StdErr;
11+
312
use crate::{
413
bitcoin::{
514
absolute, key::Secp256k1, transaction, Address, Amount, Network, OutPoint, ScriptBuf,
@@ -11,7 +20,7 @@ use crate::{
1120
},
1221
locked_outpoints,
1322
miniscript::descriptor::{Descriptor, DescriptorPublicKey},
14-
ChangeSet, WalletPersister,
23+
AsyncWalletPersister, ChangeSet, WalletPersister,
1524
};
1625

1726
macro_rules! block_id {
@@ -432,3 +441,255 @@ where
432441
assert_eq!(changeset_read.descriptor.unwrap(), descriptor);
433442
assert_eq!(changeset_read.change_descriptor, None);
434443
}
444+
445+
/// Creates a [`ChangeSet`].
446+
fn get_changeset(tx1: Transaction) -> ChangeSet {
447+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[0].parse().unwrap();
448+
let change_descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[1].parse().unwrap();
449+
450+
let local_chain_changeset = local_chain::ChangeSet {
451+
blocks: [
452+
(910234, Some(hash!("B"))),
453+
(910233, Some(hash!("T"))),
454+
(910235, Some(hash!("C"))),
455+
]
456+
.into(),
457+
};
458+
459+
let txid1 = tx1.compute_txid();
460+
461+
let conf_anchor: ConfirmationBlockTime = ConfirmationBlockTime {
462+
block_id: block_id!(910234, "B"),
463+
confirmation_time: 1755317160,
464+
};
465+
466+
let outpoint = OutPoint::new(hash!("Rust"), 0);
467+
468+
let tx_graph_changeset = tx_graph::ChangeSet::<ConfirmationBlockTime> {
469+
txs: [Arc::new(tx1)].into(),
470+
txouts: [
471+
(
472+
outpoint,
473+
TxOut {
474+
value: Amount::from_sat(1300),
475+
script_pubkey: spk_at_index(&descriptor, 4),
476+
},
477+
),
478+
(
479+
OutPoint::new(hash!("REDB"), 0),
480+
TxOut {
481+
value: Amount::from_sat(1400),
482+
script_pubkey: spk_at_index(&descriptor, 10),
483+
},
484+
),
485+
]
486+
.into(),
487+
anchors: [(conf_anchor, txid1)].into(),
488+
last_seen: [(txid1, 1755317760)].into(),
489+
first_seen: [(txid1, 1755317750)].into(),
490+
last_evicted: [(txid1, 1755317760)].into(),
491+
};
492+
493+
let keychain_txout_changeset = keychain_txout::ChangeSet {
494+
last_revealed: [
495+
(descriptor.descriptor_id(), 12),
496+
(change_descriptor.descriptor_id(), 10),
497+
]
498+
.into(),
499+
spk_cache: [
500+
(
501+
descriptor.descriptor_id(),
502+
SpkIterator::new_with_range(&descriptor, 0..=37).collect(),
503+
),
504+
(
505+
change_descriptor.descriptor_id(),
506+
SpkIterator::new_with_range(&change_descriptor, 0..=35).collect(),
507+
),
508+
]
509+
.into(),
510+
};
511+
512+
let locked_outpoints_changeset = locked_outpoints::ChangeSet {
513+
outpoints: [(outpoint, true)].into(),
514+
};
515+
516+
ChangeSet {
517+
descriptor: Some(descriptor.clone()),
518+
change_descriptor: Some(change_descriptor.clone()),
519+
network: Some(Network::Testnet),
520+
local_chain: local_chain_changeset,
521+
tx_graph: tx_graph_changeset,
522+
indexer: keychain_txout_changeset,
523+
locked_outpoints: locked_outpoints_changeset,
524+
}
525+
}
526+
527+
/// Creates another [`ChangeSet`].
528+
fn get_changeset_two(tx2: Transaction) -> ChangeSet {
529+
let descriptor: Descriptor<DescriptorPublicKey> = DESCRIPTORS[0].parse().unwrap();
530+
531+
let local_chain_changeset = local_chain::ChangeSet {
532+
blocks: [(910236, Some(hash!("BDK")))].into(),
533+
};
534+
535+
let conf_anchor: ConfirmationBlockTime = ConfirmationBlockTime {
536+
block_id: block_id!(910236, "BDK"),
537+
confirmation_time: 1755317760,
538+
};
539+
540+
let txid2 = tx2.compute_txid();
541+
542+
let outpoint = OutPoint::new(hash!("Bitcoin_fixes_things"), 0);
543+
544+
let tx_graph_changeset = tx_graph::ChangeSet::<ConfirmationBlockTime> {
545+
txs: [Arc::new(tx2)].into(),
546+
txouts: [(
547+
outpoint,
548+
TxOut {
549+
value: Amount::from_sat(10000),
550+
script_pubkey: spk_at_index(&descriptor, 21),
551+
},
552+
)]
553+
.into(),
554+
anchors: [(conf_anchor, txid2)].into(),
555+
last_seen: [(txid2, 1755317700)].into(),
556+
first_seen: [(txid2, 1755317700)].into(),
557+
last_evicted: [(txid2, 1755317760)].into(),
558+
};
559+
560+
let keychain_txout_changeset = keychain_txout::ChangeSet {
561+
last_revealed: [(descriptor.descriptor_id(), 14)].into(),
562+
spk_cache: [(
563+
descriptor.descriptor_id(),
564+
SpkIterator::new_with_range(&descriptor, 37..=39).collect(),
565+
)]
566+
.into(),
567+
};
568+
569+
let locked_outpoints_changeset = locked_outpoints::ChangeSet {
570+
outpoints: [(outpoint, true)].into(),
571+
};
572+
573+
ChangeSet {
574+
descriptor: None,
575+
change_descriptor: None,
576+
network: None,
577+
local_chain: local_chain_changeset,
578+
tx_graph: tx_graph_changeset,
579+
indexer: keychain_txout_changeset,
580+
locked_outpoints: locked_outpoints_changeset,
581+
}
582+
}
583+
584+
/// Errors caused by a failed wallet persister test.
585+
#[derive(Debug)]
586+
pub enum PersistError {
587+
/// Change set mismatch
588+
ChangeSetMismatch {
589+
/// the resulting changeset
590+
got: Box<ChangeSet>,
591+
/// the expected changeset
592+
expected: Box<ChangeSet>,
593+
},
594+
/// The wallet persister implementation failed
595+
Persister(Box<dyn StdErr + 'static>),
596+
}
597+
598+
impl fmt::Display for PersistError {
599+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
600+
match self {
601+
Self::Persister(e) => write!(f, "{e}"),
602+
Self::ChangeSetMismatch { got, expected } => {
603+
write!(f, "expected: {expected:?}, got: {got:?}")
604+
}
605+
}
606+
}
607+
}
608+
609+
#[cfg(feature = "std")]
610+
impl StdErr for PersistError {}
611+
612+
impl PersistError {
613+
/// Converts `e` to a [`PersistError::Persister`].
614+
fn persister<E>(e: E) -> Self
615+
where
616+
E: StdErr + 'static,
617+
{
618+
Self::Persister(Box::new(e))
619+
}
620+
}
621+
622+
/// Test the functionality of an [`AsyncWalletPersister`].
623+
///
624+
/// # Errors
625+
///
626+
/// If any of the following doesn't occur:
627+
///
628+
/// - The store must initially be empty
629+
/// - The store must persist non-empty changesets
630+
/// - The store must return the expected changeset after being persisted
631+
pub async fn persist_wallet_changeset_async<F, P>(create_store: F) -> Result<(), PersistError>
632+
where
633+
F: AsyncFn() -> Result<P, P::Error>,
634+
P: AsyncWalletPersister,
635+
P::Error: StdErr + 'static,
636+
{
637+
use PersistError as E;
638+
639+
// Create store
640+
let mut store = create_store().await.map_err(E::persister)?;
641+
let changeset = AsyncWalletPersister::initialize(&mut store)
642+
.await
643+
.map_err(E::persister)?;
644+
645+
// A newly created store must return an empty changeset
646+
if !changeset.is_empty() {
647+
return Err(PersistError::ChangeSetMismatch {
648+
got: Box::new(changeset),
649+
expected: Box::new(ChangeSet::default()),
650+
});
651+
}
652+
653+
let tx1 = create_one_inp_one_out_tx(hash!("We_are_all_Satoshi"), 30_000);
654+
let tx2 = create_one_inp_one_out_tx(tx1.compute_txid(), 20_000);
655+
656+
// Persist changeset
657+
let mut expected_changeset = get_changeset(tx1);
658+
659+
AsyncWalletPersister::persist(&mut store, &expected_changeset)
660+
.await
661+
.map_err(E::persister)?;
662+
663+
let changeset_read = AsyncWalletPersister::initialize(&mut store)
664+
.await
665+
.map_err(E::persister)?;
666+
667+
if expected_changeset != changeset_read {
668+
return Err(E::ChangeSetMismatch {
669+
got: Box::new(changeset_read),
670+
expected: Box::new(expected_changeset),
671+
});
672+
}
673+
674+
// Persist another changeset
675+
let changeset_2 = get_changeset_two(tx2);
676+
677+
AsyncWalletPersister::persist(&mut store, &changeset_2)
678+
.await
679+
.map_err(E::persister)?;
680+
681+
let changeset_read = AsyncWalletPersister::initialize(&mut store)
682+
.await
683+
.map_err(E::persister)?;
684+
685+
expected_changeset.merge(changeset_2);
686+
687+
if changeset_read != changeset {
688+
return Err(E::ChangeSetMismatch {
689+
got: Box::new(changeset_read),
690+
expected: Box::new(expected_changeset),
691+
});
692+
}
693+
694+
Ok(())
695+
}

0 commit comments

Comments
 (0)