@@ -75,10 +75,7 @@ mod esplora_backend {
7575 Network :: Testnet4 => bdk_wallet:: bitcoin:: Network :: Testnet4 ,
7676 } ;
7777
78- let change_desc = change_descriptor. map ( |s| s. to_owned ( ) ) . unwrap_or_else ( || {
79- let base = descriptor. strip_suffix ( "/0/*" ) . unwrap_or ( descriptor) ;
80- format ! ( "{}1/*" , base)
81- } ) ;
78+ let change_desc = super :: derive_change_descriptor ( descriptor, change_descriptor) ;
8279
8380 let wallet = BdkWalletInner :: create ( descriptor. to_owned ( ) , change_desc)
8481 . network ( bdk_network)
@@ -452,6 +449,36 @@ mod bitcoind_backend {
452449 pub use BitcoindWallet as BitcoindWalletImpl ;
453450}
454451
452+ #[ cfg( feature = "esplora" ) ]
453+ pub ( crate ) fn parse_network ( name : Option < & str > ) -> Result < Network > {
454+ use anyhow:: anyhow;
455+ match name {
456+ Some ( "mainnet" ) | Some ( "bitcoin" ) | None => Ok ( Network :: Bitcoin ) ,
457+ Some ( "testnet" ) => Ok ( Network :: Testnet ) ,
458+ Some ( "testnet4" ) => Ok ( Network :: Testnet4 ) ,
459+ Some ( "signet" ) => Ok ( Network :: Signet ) ,
460+ Some ( "regtest" ) => Ok ( Network :: Regtest ) ,
461+ Some ( n) => Err ( anyhow ! ( "Unknown network: {}" , n) ) ,
462+ }
463+ }
464+
465+ /// Derive a change descriptor from the receive descriptor when one is not
466+ /// supplied. Assumes BIP44-family `.../0/*` external chain convention and
467+ /// rewrites the final step to `1/*`. If the descriptor does not end in
468+ /// `/0/*`, returns the original descriptor unchanged (BDK will then use the
469+ /// same descriptor for both keychains).
470+ #[ cfg( feature = "esplora" ) ]
471+ pub ( crate ) fn derive_change_descriptor ( descriptor : & str , change : Option < & str > ) -> String {
472+ if let Some ( c) = change {
473+ return c. to_owned ( ) ;
474+ }
475+ if let Some ( base) = descriptor. strip_suffix ( "/0/*" ) {
476+ format ! ( "{}/1/*" , base)
477+ } else {
478+ descriptor. to_owned ( )
479+ }
480+ }
481+
455482#[ cfg( feature = "esplora" ) ]
456483pub fn create_wallet ( config : & super :: config:: Config ) -> Result < Arc < dyn PayjoinWallet > > {
457484 use anyhow:: anyhow;
@@ -461,14 +488,7 @@ pub fn create_wallet(config: &super::config::Config) -> Result<Arc<dyn PayjoinWa
461488 . wallet
462489 . as_ref ( )
463490 . ok_or_else ( || anyhow ! ( "wallet config required. Set --descriptor and --esplora-url" ) ) ?;
464- let network = match wallet_config. network . as_deref ( ) {
465- Some ( "mainnet" ) | Some ( "bitcoin" ) | None => payjoin:: bitcoin:: Network :: Bitcoin ,
466- Some ( "testnet" ) => payjoin:: bitcoin:: Network :: Testnet ,
467- Some ( "testnet4" ) => payjoin:: bitcoin:: Network :: Testnet4 ,
468- Some ( "signet" ) => payjoin:: bitcoin:: Network :: Signet ,
469- Some ( "regtest" ) => payjoin:: bitcoin:: Network :: Regtest ,
470- Some ( n) => return Err ( anyhow ! ( "Unknown network: {}" , n) ) ,
471- } ;
491+ let network = parse_network ( wallet_config. network . as_deref ( ) ) ?;
472492 let descriptor = wallet_config
473493 . descriptor
474494 . as_ref ( )
@@ -485,6 +505,62 @@ pub fn create_wallet(config: &super::config::Config) -> Result<Arc<dyn PayjoinWa
485505 ) ?) )
486506}
487507
508+ #[ cfg( all( test, feature = "esplora" ) ) ]
509+ mod tests {
510+ use super :: * ;
511+
512+ #[ test]
513+ fn parse_network_known_values ( ) {
514+ assert_eq ! ( parse_network( None ) . unwrap( ) , Network :: Bitcoin ) ;
515+ assert_eq ! ( parse_network( Some ( "mainnet" ) ) . unwrap( ) , Network :: Bitcoin ) ;
516+ assert_eq ! ( parse_network( Some ( "bitcoin" ) ) . unwrap( ) , Network :: Bitcoin ) ;
517+ assert_eq ! ( parse_network( Some ( "testnet" ) ) . unwrap( ) , Network :: Testnet ) ;
518+ assert_eq ! ( parse_network( Some ( "testnet4" ) ) . unwrap( ) , Network :: Testnet4 ) ;
519+ assert_eq ! ( parse_network( Some ( "signet" ) ) . unwrap( ) , Network :: Signet ) ;
520+ assert_eq ! ( parse_network( Some ( "regtest" ) ) . unwrap( ) , Network :: Regtest ) ;
521+ }
522+
523+ #[ test]
524+ fn parse_network_rejects_unknown ( ) {
525+ let err = parse_network ( Some ( "liquid" ) ) . unwrap_err ( ) ;
526+ assert ! ( err. to_string( ) . contains( "Unknown network" ) ) ;
527+ }
528+
529+ #[ test]
530+ fn derive_change_descriptor_uses_explicit_when_provided ( ) {
531+ let receive = "wpkh(tprv.../0/*)" ;
532+ let explicit = "wpkh(tprv.../1/*)" ;
533+ assert_eq ! ( derive_change_descriptor( receive, Some ( explicit) ) , explicit) ;
534+ }
535+
536+ #[ test]
537+ fn derive_change_descriptor_swaps_external_to_internal ( ) {
538+ // The function operates on the raw descriptor string; we only assert
539+ // that a `/0/*` suffix gets rewritten to `/1/*`.
540+ // The helper operates on a raw `.../0/*` suffix; descriptors that
541+ // wrap the key in `wpkh(...)` end in `)` and fall through to the
542+ // passthrough branch (covered by the next test). Here we cover the
543+ // raw-suffix case.
544+ let receive = "[fingerprint/84h/1h/0h]tprvFOO/0/*" ;
545+ assert_eq ! ( derive_change_descriptor( receive, None ) , "[fingerprint/84h/1h/0h]tprvFOO/1/*" ) ;
546+ }
547+
548+ #[ test]
549+ fn derive_change_descriptor_passthrough_when_no_external_suffix ( ) {
550+ let receive = "wpkh(tprv.../*)" ;
551+ assert_eq ! ( derive_change_descriptor( receive, None ) , receive) ;
552+ }
553+
554+ #[ test]
555+ fn bdk_wallet_new_rejects_invalid_descriptor ( ) {
556+ // Constructing a BdkWallet with garbage should fail before any network
557+ // I/O is attempted, so this test is hermetic.
558+ use super :: esplora_backend:: BdkWallet ;
559+ let res = BdkWallet :: new ( "not a descriptor" , None , Network :: Regtest , "http://127.0.0.1:1" ) ;
560+ assert ! ( res. is_err( ) ) ;
561+ }
562+ }
563+
488564#[ cfg( all( feature = "bitcoind" , not( feature = "esplora" ) ) ) ]
489565pub fn create_wallet ( config : & super :: config:: Config ) -> Result < Arc < dyn PayjoinWallet > > {
490566 use crate :: app:: wallet:: bitcoind_backend:: BitcoindWalletImpl ;
0 commit comments