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