@@ -268,10 +268,9 @@ where
268268 let ext_descriptor = wallet_opts. ext_descriptor . clone ( ) ;
269269 let int_descriptor = wallet_opts. int_descriptor . clone ( ) ;
270270
271- let is_multipath = ext_descriptor. contains ( '<' ) && ext_descriptor. contains ( ';' ) ;
272- if is_multipath && int_descriptor. is_some ( ) {
271+ if is_multipath_desc ( & ext_descriptor) && int_descriptor. is_some ( ) {
273272 return Err ( Error :: AmbiguousDescriptors ) ;
274- }
273+ } ;
275274
276275 let mut wallet_load_params = Wallet :: load ( ) ;
277276 wallet_load_params =
@@ -293,7 +292,7 @@ where
293292 None => {
294293 let builder = if let Some ( int_descriptor) = int_descriptor {
295294 Wallet :: create ( ext_descriptor, int_descriptor)
296- } else if ext_descriptor . contains ( '<' ) && ext_descriptor. contains ( ';' ) {
295+ } else if is_multipath_desc ( & ext_descriptor) {
297296 Wallet :: create_from_two_path_descriptor ( ext_descriptor)
298297 } else {
299298 Wallet :: create_single ( ext_descriptor)
@@ -315,14 +314,13 @@ pub(crate) fn new_wallet(network: Network, wallet_opts: &WalletOpts) -> Result<W
315314 let ext_descriptor = wallet_opts. ext_descriptor . clone ( ) ;
316315 let int_descriptor = wallet_opts. int_descriptor . clone ( ) ;
317316
318- let is_multipath = ext_descriptor. contains ( '<' ) && ext_descriptor. contains ( ';' ) ;
319- if is_multipath && int_descriptor. is_some ( ) {
317+ if is_multipath_desc ( & ext_descriptor) && int_descriptor. is_some ( ) {
320318 return Err ( Error :: AmbiguousDescriptors ) ;
321319 }
322320
323321 let builder = if let Some ( int_descriptor) = int_descriptor {
324322 Wallet :: create ( ext_descriptor, int_descriptor)
325- } else if ext_descriptor . contains ( '<' ) && ext_descriptor. contains ( ';' ) {
323+ } else if is_multipath_desc ( & ext_descriptor) {
326324 Wallet :: create_from_two_path_descriptor ( ext_descriptor)
327325 } else {
328326 Wallet :: create_single ( ext_descriptor)
@@ -673,3 +671,112 @@ pub fn load_wallet_config(
673671
674672 Ok ( ( wallet_opts, network) )
675673}
674+
675+ /// Helper to check if a descriptor string contains a BIP389 multipath expression.
676+ fn is_multipath_desc ( desc_str : & str ) -> bool {
677+ let desc_str = desc_str. split ( '#' ) . next ( ) . unwrap_or ( desc_str) . trim ( ) ;
678+
679+ desc_str. contains ( '<' ) && desc_str. contains ( ';' ) && desc_str. contains ( '>' )
680+ }
681+
682+ #[ cfg( test) ]
683+ mod tests {
684+ use super :: * ;
685+
686+ #[ test]
687+ fn test_is_multipath_descriptor ( ) {
688+ let multipath_desc = "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)" ;
689+ let desc = "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/0/*)#429nsxmg" ;
690+ let multi_path = is_multipath_desc ( multipath_desc) ;
691+ let result = is_multipath_desc ( desc) ;
692+ assert ! ( multi_path) ;
693+ assert ! ( !result) ;
694+ }
695+
696+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
697+ #[ test]
698+ fn test_multipath_detection_and_initialization ( ) {
699+ let mut db =
700+ bdk_wallet:: rusqlite:: Connection :: open_in_memory ( ) . expect ( "should open in memory db" ) ;
701+ let wallet_config = crate :: config:: WalletConfigInner {
702+ wallet : "test_wallet" . to_string ( ) ,
703+ network : "testnet4" . to_string ( ) ,
704+ ext_descriptor : "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)" . to_string ( ) ,
705+ int_descriptor : None ,
706+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
707+ database_type : "sqlite" . to_string ( ) ,
708+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" , feature = "cbf" ) ) ]
709+ client_type : Some ( "esplora" . to_string ( ) ) ,
710+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" ) ) ]
711+ server_url : Some ( " https://blockstream.info/testnet4/api" . to_string ( ) ) ,
712+ #[ cfg( feature = "electrum" ) ]
713+ batch_size : None ,
714+ #[ cfg( feature = "esplora" ) ]
715+ parallel_requests : None ,
716+ #[ cfg( feature = "rpc" ) ]
717+ rpc_user : None ,
718+ #[ cfg( feature = "rpc" ) ]
719+ rpc_password : None ,
720+ #[ cfg( feature = "rpc" ) ]
721+ cookie : None ,
722+ } ;
723+
724+ let opts: crate :: commands:: WalletOpts = ( & wallet_config)
725+ . try_into ( )
726+ . expect ( "Conversion should succeed" ) ;
727+
728+ let result = new_persisted_wallet ( bdk_wallet:: bitcoin:: Network :: Testnet , & mut db, & opts) ;
729+ assert ! ( result. is_ok( ) , "Multipath initialization should succeed" ) ;
730+
731+ let wallet = result. unwrap ( ) ;
732+ let ext_desc = wallet. public_descriptor ( KeychainKind :: External ) . to_string ( ) ;
733+ let int_desc = wallet. public_descriptor ( KeychainKind :: Internal ) . to_string ( ) ;
734+
735+ assert ! ( ext_desc. contains( "/0/*" ) , "External should use index 0" ) ;
736+ assert ! ( int_desc. contains( "/1/*" ) , "Internal should use index 1" ) ;
737+
738+ assert ! ( ext_desc. contains( "9a6a2580" ) ) ;
739+ assert ! ( int_desc. contains( "9a6a2580" ) ) ;
740+ }
741+
742+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
743+ #[ test]
744+ fn test_error_on_ambiguous_descriptors ( ) {
745+ let network = Network :: Testnet ;
746+ let mut db =
747+ bdk_wallet:: rusqlite:: Connection :: open_in_memory ( ) . expect ( "should open in memory db" ) ;
748+ let wallet_config = crate :: config:: WalletConfigInner {
749+ wallet : "test_wallet" . to_string ( ) ,
750+ network : "testnet4" . to_string ( ) ,
751+ ext_descriptor : "wpkh([9a6a2580/84'/1'/0']tpubDDnGNapGEY6AZAdQbfRJgMg9fvz8pUBrLwvyvUqEgcUfgzM6zc2eVK4vY9x9L5FJWdX8WumXuLEDV5zDZnTfbn87vLe9XceCFwTu9so9Kks/<0;1>/*)" . to_string ( ) ,
752+ int_descriptor : Some ( "wpkh([07234a14/84'/1'/0']tpubDCSgT6PaVLQH9h2TAxKryhvkEurUBcYRJc9dhTcMDyahhWiMWfEWvQQX89yaw7w7XU8bcVujoALfxq59VkFATri3Cxm5mkp9kfHfRFDckEh/1/*)#y7qjdnts" . to_string ( ) ) ,
753+ #[ cfg( any( feature = "sqlite" , feature = "redb" ) ) ]
754+ database_type : "sqlite" . to_string ( ) ,
755+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" , feature = "cbf" ) ) ]
756+ client_type : Some ( "esplora" . to_string ( ) ) ,
757+ #[ cfg( any( feature = "electrum" , feature = "esplora" , feature = "rpc" ) ) ]
758+ server_url : Some ( " https://blockstream.info/testnet4/api" . to_string ( ) ) ,
759+ #[ cfg( feature = "electrum" ) ]
760+ batch_size : None ,
761+ #[ cfg( feature = "esplora" ) ]
762+ parallel_requests : None ,
763+ #[ cfg( feature = "rpc" ) ]
764+ rpc_user : None ,
765+ #[ cfg( feature = "rpc" ) ]
766+ rpc_password : None ,
767+ #[ cfg( feature = "rpc" ) ]
768+ cookie : None ,
769+ } ;
770+
771+ let opts: WalletOpts = ( & wallet_config)
772+ . try_into ( )
773+ . expect ( "Conversion should succeed" ) ;
774+
775+ let result = new_persisted_wallet ( network, & mut db, & opts) ;
776+
777+ match result {
778+ Err ( Error :: AmbiguousDescriptors ) => ( ) ,
779+ _ => panic ! ( "Should have returned AmbiguousDescriptors error" ) ,
780+ }
781+ }
782+ }
0 commit comments