From 62e01e52bb9a09405864a0bf2b3a94589048f6e5 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Mon, 14 Jul 2025 20:26:40 +0100 Subject: [PATCH 1/4] Improve getindexinfo test The current test passes if the returned map is empty. Update the test to access the values and ensure the deserialization worked. --- integration_test/tests/util.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/integration_test/tests/util.rs b/integration_test/tests/util.rs index def18471..66cf1019 100644 --- a/integration_test/tests/util.rs +++ b/integration_test/tests/util.rs @@ -66,8 +66,11 @@ fn util__get_descriptor_info() { #[cfg(not(feature = "v20_and_below"))] #[test] fn util__get_index_info() { - let node = Node::with_wallet(Wallet::Default, &[]); - let _: GetIndexInfo = node.client.get_index_info().expect("getindexinfo"); + let node = Node::with_wallet(Wallet::Default, &["-txindex"]); + let index_info: GetIndexInfo = node.client.get_index_info().expect("getindexinfo"); + + let txindex_info = index_info.0.get("txindex").unwrap(); + assert!(txindex_info.best_block_height < u32::MAX, "best_block_height should be a valid block height"); } #[test] From 4f534f3058f3fd76ca90f68603f61737fd25c626 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Tue, 15 Jul 2025 16:42:56 +0100 Subject: [PATCH 2/4] Rewrite create descriptor wallet client macro Change the client macro for v21 to match the style of the v23 definition. Only use this for v21 and v22 where the default wallet is a legacy wallet. Change the test to use the new function. Add a comment to the v23 definition clarifying why the legacy wallet function is needed. --- client/src/client_sync/v21/mod.rs | 3 +- client/src/client_sync/v21/wallet.rs | 55 +++++++++++++++++++++------- client/src/client_sync/v22/mod.rs | 3 +- client/src/client_sync/v23/mod.rs | 1 - client/src/client_sync/v23/wallet.rs | 3 ++ client/src/client_sync/v24/mod.rs | 1 - client/src/client_sync/v25/mod.rs | 1 - client/src/client_sync/v26/mod.rs | 1 - client/src/client_sync/v27/mod.rs | 1 - client/src/client_sync/v28/mod.rs | 1 - client/src/client_sync/v29/mod.rs | 1 - integration_test/tests/wallet.rs | 10 ++++- 12 files changed, 55 insertions(+), 26 deletions(-) diff --git a/client/src/client_sync/v21/mod.rs b/client/src/client_sync/v21/mod.rs index 8a038ab5..38ff8b40 100644 --- a/client/src/client_sync/v21/mod.rs +++ b/client/src/client_sync/v21/mod.rs @@ -128,8 +128,7 @@ crate::impl_client_v17__abort_rescan!(); crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); -crate::impl_client_v17__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); +crate::impl_client_v21__create_wallet!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v21/wallet.rs b/client/src/client_sync/v21/wallet.rs index eaf22573..7a832e6a 100644 --- a/client/src/client_sync/v21/wallet.rs +++ b/client/src/client_sync/v21/wallet.rs @@ -9,22 +9,51 @@ //! //! See or use the `define_jsonrpc_minreq_client!` macro to define a `Client`. -/// Implements Bitcoin Core JSON-RPC API method `createwallet` with descriptors=true (descriptor wallet). +/// Implements Bitcoin Core JSON-RPC API method `createwallet`. #[macro_export] -macro_rules! impl_client_v21__create_wallet_with_descriptors { +macro_rules! impl_client_v21__create_wallet { () => { impl Client { - pub fn create_wallet_with_descriptors(&self, wallet: &str) -> Result { - let args = [ - wallet.into(), - false.into(), // disable_private_keys - false.into(), // blank - serde_json::Value::Null, // passphrase - false.into(), // avoid_reuse - true.into(), // descriptors=true - serde_json::Value::Null, // load_on_startup - ]; - self.call("createwallet", &args) + /// Calls `createwallet` with `wallet` as the only argument. + /// + /// In v21 and v22 this creates a legacy wallet. Use `create_descriptor_wallet` to create + /// a descriptor wallet. + pub fn create_wallet(&self, wallet: &str) -> Result { + self.call("createwallet", &[wallet.into()]) + } + + /// Creates a wallet with descriptors=true (descriptor wallet). + /// + /// > createwallet "wallet_name" ( disable_private_keys blank "passphrase" avoid_reuse descriptors load_on_startup ) + /// > + /// > Creates and loads a new wallet. + /// > + /// > Arguments: + /// > 1. wallet_name (string, required) The name for the new wallet. If this is a path, the wallet will be created at the path location. + /// > 2. disable_private_keys (boolean, optional, default=false) Disable the possibility of private keys (only watchonlys are possible in this mode). + /// > 3. blank (boolean, optional, default=false) Create a blank wallet. A blank wallet has no keys or HD seed. One can be set using sethdseed. + /// > 4. passphrase (string, optional) Encrypt the wallet with this passphrase. + /// > 5. avoid_reuse (boolean, optional, default=false) Keep track of coin reuse, and treat dirty and clean coins differently with privacy considerations in mind. + /// > 6. descriptors (boolean, optional, default=true) Create a native descriptor wallet. The wallet will use descriptors internally to handle address creation + /// > 7. load_on_startup (boolean, optional) Save wallet name to persistent settings and load on startup. True to add wallet to startup list, false to remove, null to leave unchanged. + pub fn create_descriptor_wallet(&self, wallet: &str) -> Result { + let disable_private_keys = false; + let blank = false; + let passphrase = String::new(); + let avoid_reuse = false; + let descriptors = true; + + self.call( + "createwallet", + &[ + wallet.into(), + disable_private_keys.into(), + blank.into(), + passphrase.into(), + avoid_reuse.into(), + descriptors.into(), + ], + ) } } }; diff --git a/client/src/client_sync/v22/mod.rs b/client/src/client_sync/v22/mod.rs index f58586de..d99d8d97 100644 --- a/client/src/client_sync/v22/mod.rs +++ b/client/src/client_sync/v22/mod.rs @@ -130,8 +130,7 @@ crate::impl_client_v17__abort_rescan!(); crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); -crate::impl_client_v17__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); +crate::impl_client_v21__create_wallet!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v23/mod.rs b/client/src/client_sync/v23/mod.rs index 0160e6c7..8a921417 100644 --- a/client/src/client_sync/v23/mod.rs +++ b/client/src/client_sync/v23/mod.rs @@ -134,7 +134,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v23/wallet.rs b/client/src/client_sync/v23/wallet.rs index 99648bf4..f1d0b3de 100644 --- a/client/src/client_sync/v23/wallet.rs +++ b/client/src/client_sync/v23/wallet.rs @@ -15,6 +15,9 @@ macro_rules! impl_client_v23__create_wallet { () => { impl Client { /// Calls `createwallet` with `wallet` as the only argument. + /// + /// In v23 and later this creates a descriptor wallet. Use `create_legacy_wallet` to create + /// a legacy wallet. pub fn create_wallet(&self, wallet: &str) -> Result { self.call("createwallet", &[wallet.into()]) } diff --git a/client/src/client_sync/v24/mod.rs b/client/src/client_sync/v24/mod.rs index fd62f9f6..c408fb9d 100644 --- a/client/src/client_sync/v24/mod.rs +++ b/client/src/client_sync/v24/mod.rs @@ -131,7 +131,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v25/mod.rs b/client/src/client_sync/v25/mod.rs index 64cc34ef..daaef2fd 100644 --- a/client/src/client_sync/v25/mod.rs +++ b/client/src/client_sync/v25/mod.rs @@ -133,7 +133,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v26/mod.rs b/client/src/client_sync/v26/mod.rs index 154e31cf..ba648760 100644 --- a/client/src/client_sync/v26/mod.rs +++ b/client/src/client_sync/v26/mod.rs @@ -137,7 +137,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v27/mod.rs b/client/src/client_sync/v27/mod.rs index 79605609..0178602b 100644 --- a/client/src/client_sync/v27/mod.rs +++ b/client/src/client_sync/v27/mod.rs @@ -133,7 +133,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v28/mod.rs b/client/src/client_sync/v28/mod.rs index a2e879b4..4e962eda 100644 --- a/client/src/client_sync/v28/mod.rs +++ b/client/src/client_sync/v28/mod.rs @@ -135,7 +135,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/client/src/client_sync/v29/mod.rs b/client/src/client_sync/v29/mod.rs index 2c26f821..aaad386f 100644 --- a/client/src/client_sync/v29/mod.rs +++ b/client/src/client_sync/v29/mod.rs @@ -135,7 +135,6 @@ crate::impl_client_v17__add_multisig_address!(); crate::impl_client_v17__backup_wallet!(); crate::impl_client_v17__bump_fee!(); crate::impl_client_v23__create_wallet!(); -crate::impl_client_v21__create_wallet_with_descriptors!(); crate::impl_client_v17__dump_priv_key!(); crate::impl_client_v17__dump_wallet!(); crate::impl_client_v17__encrypt_wallet!(); diff --git a/integration_test/tests/wallet.rs b/integration_test/tests/wallet.rs index ca310b2e..42e99e9e 100644 --- a/integration_test/tests/wallet.rs +++ b/integration_test/tests/wallet.rs @@ -330,7 +330,13 @@ fn wallet__import_descriptors() { let node = Node::with_wallet(Wallet::None, &[]); let wallet_name = "desc_wallet"; - node.client.create_wallet_with_descriptors(wallet_name).expect("create descriptor wallet"); + + #[cfg(feature = "v22_and_below")] + node.client.create_descriptor_wallet(wallet_name).expect("create descriptor wallet"); + + // v23 onwards uses descriptor wallets by default. + #[cfg(not(feature = "v22_and_below"))] + node.client.create_wallet(wallet_name).expect("create wallet"); let address = node.client.new_address().expect("failed to get new address"); let descriptor = format!("addr({})", address); @@ -515,7 +521,7 @@ fn wallet__list_descriptors() { let wallet_name = "desc_wallet"; #[cfg(feature = "v22_and_below")] - node.client.create_wallet_with_descriptors(wallet_name).expect("create descriptor wallet"); + node.client.create_descriptor_wallet(wallet_name).expect("create descriptor wallet"); // v23 onwards uses descriptor wallets by default. #[cfg(not(feature = "v22_and_below"))] From 746c5dc66006b37928597ecbe4c5a56bca55767c Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Wed, 16 Jul 2025 09:02:33 +0100 Subject: [PATCH 3/4] Add a constructor for ImportDescriptorsRequest --- client/src/client_sync/v21/mod.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/client_sync/v21/mod.rs b/client/src/client_sync/v21/mod.rs index 38ff8b40..0853c934 100644 --- a/client/src/client_sync/v21/mod.rs +++ b/client/src/client_sync/v21/mod.rs @@ -192,3 +192,10 @@ pub struct ImportDescriptorsRequest { /// Time from which to start rescanning the blockchain for this descriptor, in UNIX epoch time or "now". pub timestamp: serde_json::Value, } + +impl ImportDescriptorsRequest { + /// Constructs a new ImportDescriptorsRequest. + pub fn new(descriptor: impl Into, timestamp: impl Into) -> Self { + ImportDescriptorsRequest { descriptor: descriptor.into(), timestamp: timestamp.into() } + } +} From e396ad08661bdfdf2b2d1c0a17899e7700ff5e30 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Mon, 14 Jul 2025 21:03:04 +0100 Subject: [PATCH 4/4] Improve import_descriptors test The current test of the RPC always returns true due to scanning for the timestamp `now`. Move imports to the top of the file and allow unused. Improve the test to be more thorough by creating a new address and then scanning for the descriptor at a later time. --- integration_test/tests/wallet.rs | 49 ++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/integration_test/tests/wallet.rs b/integration_test/tests/wallet.rs index 42e99e9e..daf19f1f 100644 --- a/integration_test/tests/wallet.rs +++ b/integration_test/tests/wallet.rs @@ -3,14 +3,19 @@ //! Tests for methods found under the `== Wallet ==` section of the API docs. #![allow(non_snake_case)] // Test names intentionally use double underscore. +#![allow(unused_imports)] // Some imports are only used in specific versions. -#[cfg(feature = "TODO")] -use bitcoin::address::{Address, NetworkChecked}; -use bitcoin::{Amount, FeeRate, PrivateKey, PublicKey}; +use bitcoin::address::{Address, KnownHrp, NetworkChecked}; +use bitcoin::{secp256k1, Amount, CompressedPublicKey, FeeRate, PrivateKey, PublicKey}; use integration_test::{Node, NodeExt as _, Wallet}; use node::{mtype, AddressType, ImportMultiRequest, ImportMultiScriptPubKey, ImportMultiTimestamp}; + +#[cfg(not(feature = "v20_and_below"))] +use node::ImportDescriptorsRequest; + use node::vtype::*; // All the version specific types. use std::fs; +use std::time::{SystemTime, UNIX_EPOCH}; #[test] fn wallet__abandon_transaction() { @@ -326,8 +331,6 @@ fn wallet__import_address() { #[test] #[cfg(not(feature = "v20_and_below"))] fn wallet__import_descriptors() { - use node::{serde_json, ImportDescriptorsRequest}; - let node = Node::with_wallet(Wallet::None, &[]); let wallet_name = "desc_wallet"; @@ -338,15 +341,37 @@ fn wallet__import_descriptors() { #[cfg(not(feature = "v22_and_below"))] node.client.create_wallet(wallet_name).expect("create wallet"); - let address = node.client.new_address().expect("failed to get new address"); - let descriptor = format!("addr({})", address); + node.fund_wallet(); - let request = ImportDescriptorsRequest { - descriptor, - timestamp: serde_json::Value::String("now".to_string()), - }; + // 1. Get the current time + let start_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("failed to get current time") + .as_secs(); - let _: ImportDescriptors = node.client.import_descriptors(&[request]).expect("importdescriptors"); + // 2. Use a known private key, derive the address from it and send some coins to it. + let privkey = + PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").unwrap(); + let secp = secp256k1::Secp256k1::new(); + let pubkey = privkey.public_key(&secp); + let address = Address::p2wpkh(&CompressedPublicKey(pubkey.inner), KnownHrp::Regtest); + let amount = Amount::from_sat(10_000); + let _txid = node.client.send_to_address(&address, amount).expect("sendtoaddress"); + + // 3. Get the descriptor from the private key. + let raw_descriptor = format!("wpkh({})", privkey.to_wif()); + let info = node.client.get_descriptor_info(&raw_descriptor).expect("get_descriptor_info"); + let descriptor = format!("{}#{}", raw_descriptor, info.checksum); + + // 4. Mine 100 blocks + let mining_address = node.client.new_address().expect("failed to get mining address"); + let _blocks = node.client.generate_to_address(100, &mining_address).expect("generatetoaddress"); + + // 5. Scan for the descriptor using the time from (1) + let request = ImportDescriptorsRequest::new(descriptor, start_time); + let result: ImportDescriptors = node.client.import_descriptors(&[request]).expect("importdescriptors"); + assert_eq!(result.0.len(), 1, "should have exactly one import result"); + assert!(result.0[0].success); } #[test]