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+
312use 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
1726macro_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