Skip to content

Commit b03ebc7

Browse files
committed
feat(multipath): Add multipath utils unit tests
1 parent 047f9e2 commit b03ebc7

File tree

1 file changed

+114
-7
lines changed

1 file changed

+114
-7
lines changed

src/utils.rs

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)