Skip to content

Commit c63d511

Browse files
committed
feat(multipath): Add multipath utils unit tests
1 parent 2424425 commit c63d511

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
@@ -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

Comments
 (0)