@@ -241,10 +241,9 @@ where
241241 let ext_descriptor = wallet_opts. ext_descriptor . clone ( ) ;
242242 let int_descriptor = wallet_opts. int_descriptor . clone ( ) ;
243243
244- let is_multipath = ext_descriptor. contains ( '<' ) && ext_descriptor. contains ( ';' ) ;
245- if is_multipath && int_descriptor. is_some ( ) {
244+ if is_multipath_desc ( & ext_descriptor) && int_descriptor. is_some ( ) {
246245 return Err ( Error :: AmbiguousDescriptors ) ;
247- }
246+ } ;
248247
249248 let mut wallet_load_params = Wallet :: load ( ) ;
250249 wallet_load_params =
@@ -266,7 +265,7 @@ where
266265 None => {
267266 let builder = if let Some ( int_descriptor) = int_descriptor {
268267 Wallet :: create ( ext_descriptor, int_descriptor)
269- } else if ext_descriptor . contains ( '<' ) && ext_descriptor. contains ( ';' ) {
268+ } else if is_multipath_desc ( & ext_descriptor) {
270269 Wallet :: create_from_two_path_descriptor ( ext_descriptor)
271270 } else {
272271 Wallet :: create_single ( ext_descriptor)
@@ -288,14 +287,13 @@ pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result<W
288287 let ext_descriptor = wallet_opts. ext_descriptor . clone ( ) ;
289288 let int_descriptor = wallet_opts. int_descriptor . clone ( ) ;
290289
291- let is_multipath = ext_descriptor. contains ( '<' ) && ext_descriptor. contains ( ';' ) ;
292- if is_multipath && int_descriptor. is_some ( ) {
290+ if is_multipath_desc ( & ext_descriptor) && int_descriptor. is_some ( ) {
293291 return Err ( Error :: AmbiguousDescriptors ) ;
294292 }
295293
296294 let builder = if let Some ( int_descriptor) = int_descriptor {
297295 Wallet :: create ( ext_descriptor, int_descriptor)
298- } else if ext_descriptor . contains ( '<' ) && ext_descriptor . contains ( ';' ) {
296+ } else if is_multipath_desc ( & desc_str ) {
299297 Wallet :: create_from_two_path_descriptor ( ext_descriptor)
300298 } else {
301299 Wallet :: create_single ( ext_descriptor)
@@ -658,3 +656,112 @@ pub fn load_wallet_config(
658656
659657 Ok ( ( wallet_opts, network) )
660658}
659+
660+ /// Helper to check if a descriptor string contains a BIP389 multipath expression.
661+ fn is_multipath_desc ( desc_str : & str ) -> bool {
662+ let desc_str = desc_str. split ( '#' ) . next ( ) . unwrap_or ( desc_str) . trim ( ) ;
663+
664+ desc_str. contains ( '<' ) && desc_str. contains ( ';' ) && desc_str. contains ( '>' )
665+ }
666+
667+ #[ cfg( test) ]
668+ mod tests {
669+ use super :: * ;
670+ use crate :: commands:: WalletOpts ;
671+ use bdk_wallet:: { bitcoin:: Network , rusqlite:: Connection } ;
672+
673+ #[ test]
674+ fn test_is_multipath_descriptor ( ) {
675+ let multipath_desc = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)" ;
676+ let desc = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg" ;
677+ let multi_path = is_multipath_desc ( multipath_desc) ;
678+ let result = is_multipath_desc ( desc) ;
679+ assert ! ( multi_path) ;
680+ assert ! ( !result) ;
681+ }
682+
683+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
684+ #[ test]
685+ fn test_multipath_detection_and_initialization ( ) {
686+ let mut db = Connection :: open_in_memory ( ) . expect ( "should open in memory db" ) ;
687+ let wallet_config = crate :: config:: WalletConfigInner {
688+ wallet : "test_wallet" . to_string ( ) ,
689+ network : "testnet4" . to_string ( ) ,
690+ ext_descriptor : "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)" . to_string ( ) ,
691+ int_descriptor : None ,
692+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
693+ database_type : "sqlite" . to_string ( ) ,
694+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" , feature = "cbf" ) ) ]
695+ client_type : Some ( "esplora" . to_string ( ) ) ,
696+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" ) ) ]
697+ server_url : Some ( " https://blockstream.info/testnet4/api" . to_string ( ) ) ,
698+ #[ cfg( feature = "electrum" ) ]
699+ batch_size : None ,
700+ #[ cfg( feature = "esplora" ) ]
701+ parallel_requests : None ,
702+ #[ cfg( feature = "rpc" ) ]
703+ rpc_user : None ,
704+ #[ cfg( feature = "rpc" ) ]
705+ rpc_password : None ,
706+ #[ cfg( feature = "rpc" ) ]
707+ cookie : None ,
708+ } ;
709+
710+ let opts: WalletOpts = ( & wallet_config)
711+ . try_into ( )
712+ . expect ( "Conversion should succeed" ) ;
713+
714+ let result = new_persisted_wallet ( Network :: Testnet , & mut db, & opts) ;
715+ assert ! ( result. is_ok( ) , "Multipath initialization should succeed" ) ;
716+
717+ let wallet = result. unwrap ( ) ;
718+ let ext_desc = wallet. public_descriptor ( KeychainKind :: External ) . to_string ( ) ;
719+ let int_desc = wallet. public_descriptor ( KeychainKind :: Internal ) . to_string ( ) ;
720+
721+ assert ! ( ext_desc. contains( "/0/*" ) , "External should use index 0" ) ;
722+ assert ! ( int_desc. contains( "/1/*" ) , "Internal should use index 1" ) ;
723+
724+ assert ! ( ext_desc. contains( "9a6a2580" ) ) ;
725+ assert ! ( int_desc. contains( "9a6a2580" ) ) ;
726+ }
727+
728+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
729+ #[ test]
730+ fn test_error_on_ambiguous_descriptors ( ) {
731+ let network = Network :: Testnet ;
732+ let mut db = Connection :: open_in_memory ( ) . expect ( "should open in memory db" ) ;
733+ let wallet_config = crate :: config:: WalletConfigInner {
734+ wallet : "test_wallet" . to_string ( ) ,
735+ network : "testnet4" . to_string ( ) ,
736+ ext_descriptor : "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)" . to_string ( ) ,
737+ int_descriptor : Some ( "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/1/*)#y7qjdnts" . to_string ( ) ) ,
738+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
739+ database_type : "sqlite" . to_string ( ) ,
740+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" , feature = "cbf" ) ) ]
741+ client_type : Some ( "esplora" . to_string ( ) ) ,
742+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" ) ) ]
743+ server_url : Some ( " https://blockstream.info/testnet4/api" . to_string ( ) ) ,
744+ #[ cfg( feature = "electrum" ) ]
745+ batch_size : None ,
746+ #[ cfg( feature = "esplora" ) ]
747+ parallel_requests : None ,
748+ #[ cfg( feature = "rpc" ) ]
749+ rpc_user : None ,
750+ #[ cfg( feature = "rpc" ) ]
751+ rpc_password : None ,
752+ #[ cfg( feature = "rpc" ) ]
753+ cookie : None ,
754+ } ;
755+
756+ let opts: WalletOpts = ( & wallet_config)
757+ . try_into ( )
758+ . expect ( "Conversion should succeed" ) ;
759+
760+ let result = new_persisted_wallet ( network, & mut db, & opts) ;
761+
762+ match result {
763+ Err ( Error :: AmbiguousDescriptors ) => ( ) ,
764+ _ => panic ! ( "Should have returned AmbiguousDescriptors error" ) ,
765+ }
766+ }
767+ }
0 commit comments