From 38a709f5127a107da15083b76b3a2e99e14ab9a0 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 11 Jun 2026 13:38:52 +0100 Subject: [PATCH 01/11] test: Add helper fns & integration tests for key - add BdkCli helper struct and impl methods - add integration tests for the key subcommand operations --- Cargo.lock | 110 +++++++++++++++++++ Cargo.toml | 6 ++ tests/cli.rs | 115 ++++++++++++++++++++ tests/common/mod.rs | 77 ++++++++++++++ tests/integration.rs | 247 ------------------------------------------- 5 files changed, 308 insertions(+), 247 deletions(-) create mode 100644 tests/cli.rs create mode 100644 tests/common/mod.rs delete mode 100644 tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 93134ad3..ba294410 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,21 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -142,6 +157,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -196,6 +217,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" name = "bdk-cli" version = "3.0.0" dependencies = [ + "assert_cmd", "bdk_bip322", "bdk_bitcoind_rpc", "bdk_electrum", @@ -211,10 +233,12 @@ dependencies = [ "env_logger", "log", "payjoin", + "predicates", "reqwest 0.13.2", "serde", "serde_json", "shlex", + "tempfile", "thiserror 2.0.18", "tokio", "toml 1.1.0+spec-1.1.0", @@ -576,6 +600,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -898,6 +933,12 @@ dependencies = [ "cipher 0.3.0", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.9.0" @@ -1051,6 +1092,15 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "foreign-types" version = "0.3.2" @@ -1773,6 +1823,12 @@ dependencies = [ "tempfile", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1782,6 +1838,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1989,6 +2054,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2649,6 +2744,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -3023,6 +3124,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "want" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index 8f76de0d..3bad77f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,3 +69,9 @@ compiler = [] # Experimental silent payment sending capabilities silent-payments = ["dep:bdk_sp"] + +[dev-dependencies] +predicates = "3.0" +tempfile = "3.8" +assert_cmd = "2.2.2" + diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 00000000..db97d656 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,115 @@ +// Copyright (c) 2020-2026 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! bdk-cli Integration Test Framework +//! +//! This modules performs the necessary integration test for bdk-cli +//! The tests can be run using `cargo test` + +mod common; + +// --- KEY COMMAND TESTS --- +mod test_key { + use crate::common::BdkCli; + use predicates::prelude::*; + use serde_json::Value; + + #[test] + fn test_cli_key_generate() { + // compile binary + let cli = BdkCli::new("testnet", None); + + cli.key_cmd(&["generate"]) + .assert() + .success() + .stdout(predicate::str::contains("\"xprv\":")) + .stdout(predicate::str::contains("\"mnemonic\":")) + .stdout(predicate::str::contains("\"fingerprint\":")); + } + + #[test] + fn test_cli_key_derive() { + let cli = BdkCli::new("testnet", None); + + let generate_output = cli + .key_cmd(&["generate"]) + .output() + .expect("Failed to execute generate command"); + assert!(generate_output.status.success(), "Generate command failed"); + + let generate_json: Value = + serde_json::from_slice(&generate_output.stdout).expect("Invalid JSON"); + let xprv = generate_json["xprv"].as_str().expect("Missing XPRV"); + + let mut cmd = cli.key_cmd(&[ + "derive", + "--xprv", + xprv, + "--derivation_path", + "m/84'/1'/0'/0", + ]); + + cmd.assert() + .success() + .stdout(predicate::str::contains("\"xprv\":")) + .stdout(predicate::str::contains("\"xpub\":")); + } + + #[test] + fn test_cli_key_restore() { + // Generate key + let cli = BdkCli::new("testnet", None); + + // Execute the command and capture the output + let generate_cmd = cli + .key_cmd(&["generate"]) + .output() + .expect("Failed to execute generate command"); + assert!(generate_cmd.status.success(), "Generate command failed"); + + // Parse the JSON to extract the mnemonic + let generate_json: Value = + serde_json::from_slice(&generate_cmd.stdout).expect("Failed to parse JSON"); + + let mnemonic = generate_json["mnemonic"] + .as_str() + .expect("Mnemonic missing"); + let xprv = generate_json["xprv"].as_str().expect("XPRV missing"); + let finger_print = generate_json["fingerprint"] + .as_str() + .expect("Fingerprint missing"); + + // Restore using the mnemonic + let output_restore = cli.key_cmd(&["restore", "--mnemonic", mnemonic]) + .output() + .expect("Failed to execute restore command"); + assert!(output_restore.status.success(), "Restore command failed"); + + // Parse the JSON from the restore command + let restore_json: Value = + serde_json::from_slice(&output_restore.stdout).expect("Failed to parse JSON"); + + let restored_xprv = restore_json["xprv"] + .as_str() + .expect("Restored XPRV missing"); + let restored_fingerprint = restore_json["fingerprint"] + .as_str() + .expect("Restored fingerprint missing"); + + // Assert that the restored data exactly matches the generated data + assert_eq!( + xprv, restored_xprv, + "The restored XPRV does not match the generated XPRV!" + ); + + assert_eq!( + finger_print, restored_fingerprint, + "The restored fingerprint does not match the generated fingerprint!" + ); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 00000000..e957584f --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,77 @@ +// Copyright (c) 2020-2026 Bitcoin Dev Kit Developers +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! bdk-cli Integration Test Framework +//! +//! This module provides the necessary helper methods for tests +//! +use assert_cmd::Command; +use std::path::PathBuf; + +/// The bdk-cli command struct +#[allow(dead_code)] +#[derive(Debug)] +pub struct BdkCli { + pub network: String, + pub datadir: Option, + pub verbosity: bool, + pub recv_desc: Option, + pub change_desc: Option, +} + +impl BdkCli { + /// Construct a new test environment configuration + pub fn new(network: &str, datadir: Option) -> Self { + Self { + network: network.to_string(), + datadir, + verbosity: false, + recv_desc: None, + change_desc: None, + } + } + + /// Creates the base assert_cmd::Command with the global flags pre-loaded + fn build_base_cmd(&self) -> Command { + let mut cmd = Command::cargo_bin("bdk-cli").expect("bdk-cli binary must compile"); + + cmd.arg("--network").arg(&self.network); + + if let Some(dir) = &self.datadir { + cmd.arg("--datadir").arg(dir); + } + + if self.verbosity { + cmd.arg("--verbose"); + } + + cmd + } + + /// Returns a pre-configured Command builder for `key` operations + pub fn key_cmd(&self, args: &[&str]) -> Command { + let mut cmd = self.build_base_cmd(); + cmd.arg("key"); + cmd.args(args); + cmd + } + + /// Returns a pre-configured Command builder for `wallet` operations + pub fn wallet_cmd(&self, args: &[&str]) -> Command { + let mut cmd = self.build_base_cmd(); + cmd.arg("wallet"); + + // Automatically inject descriptors if they are set in the helper state + if let Some(recv) = &self.recv_desc { + cmd.arg("--descriptor").arg(recv); + } + + cmd.args(args); + cmd + } +} diff --git a/tests/integration.rs b/tests/integration.rs deleted file mode 100644 index 79c887f3..00000000 --- a/tests/integration.rs +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (c) 2020-2025 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! bdk-cli Integration Test Framework -//! -//! This modules performs the necessary integration test for bdk-cli -//! The tests can be run using `cargo test` - -#[cfg(feature = "rpc")] -mod test { - use serde_json::{Value, json}; - use std::convert::From; - use std::env::temp_dir; - use std::path::PathBuf; - use std::process::Command; - - /// Testing errors for integration tests - #[allow(dead_code)] - #[derive(Debug)] - enum IntTestError { - // IO error - IO(std::io::Error), - // Command execution error - CmdExec(String), - // Json Data error - JsonData(String), - } - - impl From for IntTestError { - fn from(e: std::io::Error) -> Self { - IntTestError::IO(e) - } - } - - // Helper function - // Runs a system command with given args - #[allow(dead_code)] - fn run_cmd_with_args(cmd: &str, args: &[&str]) -> Result { - let output = Command::new(cmd).args(args).output().unwrap(); - let mut value = output.stdout; - let error = output.stderr; - if value.is_empty() { - return Err(IntTestError::CmdExec(String::from_utf8(error).unwrap())); - } - value.pop(); // remove `\n` at end - let output_string = std::str::from_utf8(&value).unwrap(); - let json_value: serde_json::Value = match serde_json::from_str(output_string) { - Ok(value) => value, - Err(_) => json!(output_string), // bitcoin-cli will sometime return raw string - }; - Ok(json_value) - } - - // Helper Function - // Transforms a json value to string - #[allow(dead_code)] - fn value_to_string(value: &Value) -> Result { - match value { - Value::Bool(bool) => match bool { - true => Ok("true".to_string()), - false => Ok("false".to_string()), - }, - Value::Number(n) => Ok(n.to_string()), - Value::String(s) => Ok(s.to_string()), - _ => Err(IntTestError::JsonData( - "Value parsing not implemented for this type".to_string(), - )), - } - } - - // Helper Function - // Extracts value from a given json object and key - #[allow(dead_code)] - fn get_value(json: &Value, key: &str) -> Result { - let map = json - .as_object() - .ok_or(IntTestError::JsonData("Json is not an object".to_string()))?; - let value = map - .get(key) - .ok_or(IntTestError::JsonData("Invalid key".to_string()))? - .to_owned(); - let string_value = value_to_string(&value)?; - Ok(string_value) - } - - /// The bdk-cli command struct - /// Use it to perform all bdk-cli operations - #[allow(dead_code)] - #[derive(Debug)] - struct BdkCli { - target: String, - network: String, - verbosity: bool, - recv_desc: Option, - chang_desc: Option, - node_datadir: Option, - } - - impl BdkCli { - /// Construct a new [`BdkCli`] struct - fn new( - network: &str, - node_datadir: Option, - verbosity: bool, - features: &[&str], - ) -> Result { - // Build bdk-cli with given features - let mut feat = "--features=".to_string(); - for item in features { - feat.push_str(item); - feat.push(','); - } - feat.pop(); // remove the last comma - let _build = Command::new("cargo").args(["build", &feat]).output()?; - - let mut bdk_cli = Self { - target: "./target/debug/bdk-cli".to_string(), - network: network.to_string(), - verbosity, - recv_desc: None, - chang_desc: None, - node_datadir, - }; - - println!("BDK-CLI Config : {bdk_cli:#?}"); - let bdk_master_key = bdk_cli.key_exec(&["generate"])?; - let bdk_xprv = get_value(&bdk_master_key, "xprv")?; - - let bdk_recv_desc = - bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/0", "--xprv", &bdk_xprv])?; - let bdk_recv_desc = get_value(&bdk_recv_desc, "xprv")?; - let bdk_recv_desc = format!("wpkh({bdk_recv_desc})"); - - let bdk_chng_desc = - bdk_cli.key_exec(&["derive", "--path", "m/84h/1h/0h/1", "--xprv", &bdk_xprv])?; - let bdk_chng_desc = get_value(&bdk_chng_desc, "xprv")?; - let bdk_chng_desc = format!("wpkh({bdk_chng_desc})"); - - bdk_cli.recv_desc = Some(bdk_recv_desc); - bdk_cli.chang_desc = Some(bdk_chng_desc); - - Ok(bdk_cli) - } - - /// Execute bdk-cli wallet commands with given args - fn wallet_exec(&self, args: &[&str]) -> Result { - // Check if data directory is specified - let mut wallet_args = if let Some(datadir) = &self.node_datadir { - let datadir = datadir.as_os_str().to_str().unwrap(); - ["--network", &self.network, "--datadir", datadir, "wallet"].to_vec() - } else { - ["--network", &self.network, "wallet"].to_vec() - }; - - if self.verbosity { - wallet_args.push("-v"); - } - - wallet_args.push("-d"); - wallet_args.push(self.recv_desc.as_ref().unwrap()); - wallet_args.push("-c"); - wallet_args.push(self.chang_desc.as_ref().unwrap()); - - for arg in args { - wallet_args.push(arg); - } - run_cmd_with_args(&self.target, &wallet_args) - } - - /// Execute bdk-cli key commands with given args - fn key_exec(&self, args: &[&str]) -> Result { - let mut key_args = ["key"].to_vec(); - for arg in args { - key_args.push(arg); - } - run_cmd_with_args(&self.target, &key_args) - } - - /// Execute bdk-cli node command - fn node_exec(&self, args: &[&str]) -> Result { - // Check if data directory is specified - let mut node_args = if let Some(datadir) = &self.node_datadir { - let datadir = datadir.as_os_str().to_str().unwrap(); - ["--network", &self.network, "--datadir", datadir, "node"].to_vec() - } else { - ["--network", &self.network, "node"].to_vec() - }; - - for arg in args { - node_args.push(arg); - } - run_cmd_with_args(&self.target, &node_args) - } - } - - // Run A Basic wallet operation test, with given feature - #[cfg(test)] - #[allow(dead_code)] - fn basic_wallet_ops(feature: &str) { - // Create a temporary directory for testing env - let mut test_dir = std::env::current_dir().unwrap(); - test_dir.push("bdk-testing"); - - let test_dir = temp_dir(); - // let test_dir = test_temp_dir.into_path().to_path_buf(); - - // Create bdk-cli instance - let bdk_cli = BdkCli::new("regtest", Some(test_dir), false, &[feature]).unwrap(); - - // Generate 101 blocks - bdk_cli.node_exec(&["generate", "101"]).unwrap(); - - // Get a bdk address - let bdk_addr_json = bdk_cli.wallet_exec(&["get_new_address"]).unwrap(); - let bdk_addr = get_value(&bdk_addr_json, "address").unwrap(); - - // Send coins from core to bdk - bdk_cli - .node_exec(&["sendtoaddress", &bdk_addr, "1000000000"]) - .unwrap(); - - bdk_cli.node_exec(&["generate", "1"]).unwrap(); - - // Sync the bdk wallet - bdk_cli.wallet_exec(&["sync"]).unwrap(); - - // Get the balance - let balance_json = bdk_cli.wallet_exec(&["get_balance"]).unwrap(); - let confirmed_balance = balance_json - .as_object() - .unwrap() - .get("satoshi") - .unwrap() - .as_object() - .unwrap() - .get("confirmed") - .unwrap() - .as_u64() - .unwrap(); - assert_eq!(confirmed_balance, 1000000000u64); - } -} From b8761bc944fb776870d92e208848f7050b35193d Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 12 Jun 2026 08:05:06 +0100 Subject: [PATCH 02/11] test: Add wallets, descriptor, compile & config - Add integration tests for wallets, descriptor, compile and config modules --- tests/cli.rs | 167 +++++++++++++++++++++++++++++++++++++++++--- tests/common/mod.rs | 13 +++- 2 files changed, 170 insertions(+), 10 deletions(-) diff --git a/tests/cli.rs b/tests/cli.rs index db97d656..955d958e 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -13,16 +13,18 @@ mod common; +use crate::common::BdkCli; +use predicates::prelude::*; +use tempfile::TempDir; // --- KEY COMMAND TESTS --- mod test_key { - use crate::common::BdkCli; - use predicates::prelude::*; + use super::*; use serde_json::Value; #[test] fn test_cli_key_generate() { - // compile binary - let cli = BdkCli::new("testnet", None); + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); cli.key_cmd(&["generate"]) .assert() @@ -34,7 +36,8 @@ mod test_key { #[test] fn test_cli_key_derive() { - let cli = BdkCli::new("testnet", None); + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); let generate_output = cli .key_cmd(&["generate"]) @@ -62,8 +65,8 @@ mod test_key { #[test] fn test_cli_key_restore() { - // Generate key - let cli = BdkCli::new("testnet", None); + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); // Execute the command and capture the output let generate_cmd = cli @@ -85,7 +88,8 @@ mod test_key { .expect("Fingerprint missing"); // Restore using the mnemonic - let output_restore = cli.key_cmd(&["restore", "--mnemonic", mnemonic]) + let output_restore = cli + .key_cmd(&["restore", "--mnemonic", mnemonic]) .output() .expect("Failed to execute restore command"); assert!(output_restore.status.success(), "Restore command failed"); @@ -113,3 +117,150 @@ mod test_key { ); } } + +// --- WALLETS COMMAND TESTS --- +mod test_wallets { + use super::*; + + #[test] + fn test_list_wallets_empty() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + let mut cmd = cli.build_base_cmd(); + cmd.arg("wallets"); + + cmd.assert() + .failure() + .stderr(predicate::str::contains("No wallets configured yet.")); + } +} + +// --- DESCRIPTOR COMMAND TESTS --- +mod test_descriptor { + use super::*; + + #[test] + fn test_generate_new_descriptor() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + // Run `bdk-cli descriptor --type tr` + cli.cmd("descriptor", &["--type", "tr"]) + .assert() + .success() + .stdout(predicate::str::contains("\"public_descriptors\":")) + .stdout(predicate::str::contains("\"private_descriptors\":")) + .stdout(predicate::str::contains("\"mnemonic\":")) + .stdout(predicate::str::contains("\"fingerprint\":")); + } +} + +// --- COMPILE COMMAND TESTS --- +#[cfg(feature = "compiler")] +mod test_compile { + use super::*; + + #[test] + fn test_compile_valid_policy() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + let policy = "pk(02e5b88fdb71c696e1a473f309a47535b7190e21a22bd25e7fc8bd055db3bba12f)"; + + cli.cmd("compile", &[policy, "--type", "wsh"]) + .assert() + .success() + .stdout(predicate::str::contains("\"descriptor\":")) + .stdout(predicate::str::contains("wsh(")); + } + + #[test] + fn test_compile_invalid_policy() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + cli.cmd("compile", &["invalid_policy", "--type", "wsh"]) + .assert() + .failure() + .stderr(predicate::str::contains("Invalid policy")); + } +} + +// --- CONFIG COMMAND TESTS --- +#[cfg(feature = "rpc")] +mod test_config { + use super::*; + use serde_json::Value; + + #[test] + fn test_save_and_read_wallet_config() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + + let desc = cli + .cmd("descriptor", &["--type", "tr"]) + .output() + .expect("Command to generate descriptors failed"); + + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("Invalid JSON from output descriptor"); + + let pub_desc = &desc_values["public_descriptors"]; + + let ext_desc = pub_desc["external"].as_str().unwrap(); + let int_desc = pub_desc["internal"].as_str().unwrap(); + let wallet_name = "test_config_wallet"; + let client_type = "rpc"; + let db = "sqlite"; + let url = "http://localhost:18443"; + + let mut cmd_init = cli.build_base_cmd(); + cmd_init + .arg("wallet") + .arg("--wallet") + .arg(wallet_name) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg(client_type) + .arg("--database-type") + .arg(db) + .arg("--url") + .arg(url); + + cmd_init.assert().success(); + + // verify saved config + let mut cmd = cli.build_base_cmd(); + cmd.arg("wallets"); + + let output = cmd.output().expect("Failed to execute wallets command"); + + assert!( + output.status.success(), + "The wallets command failed to execute" + ); + + let json_output: Value = + serde_json::from_slice(&output.stdout).expect("CLI did not output valid JSON"); + + let config = &json_output[wallet_name]; + + assert!( + !config.is_null(), + "The wallet {wallet_name} was not found in the root JSON object" + ); + + assert_eq!(config["wallet"].as_str().unwrap(), wallet_name); + assert_eq!(config["network"].as_str().unwrap(), "regtest"); + assert_eq!(config["database_type"].as_str().unwrap(), db); + assert_eq!(config["client_type"].as_str().unwrap(), client_type); + assert_eq!(config["server_url"].as_str().unwrap(), url); + assert_eq!(config["ext_descriptor"].as_str().unwrap(), ext_desc); + assert_eq!(config["int_descriptor"].as_str().unwrap(), int_desc); + } +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index e957584f..4dbd261f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -37,7 +37,7 @@ impl BdkCli { } /// Creates the base assert_cmd::Command with the global flags pre-loaded - fn build_base_cmd(&self) -> Command { + pub fn build_base_cmd(&self) -> Command { let mut cmd = Command::cargo_bin("bdk-cli").expect("bdk-cli binary must compile"); cmd.arg("--network").arg(&self.network); @@ -53,15 +53,24 @@ impl BdkCli { cmd } + /// Returns a pre-configured Command builder for any top-level subcommand + pub fn cmd(&self, subcommand: &str, args: &[&str]) -> Command { + let mut cmd = self.build_base_cmd(); + cmd.arg(subcommand); + cmd.args(args); + cmd + } + /// Returns a pre-configured Command builder for `key` operations pub fn key_cmd(&self, args: &[&str]) -> Command { let mut cmd = self.build_base_cmd(); cmd.arg("key"); - cmd.args(args); + cmd.args(args); cmd } /// Returns a pre-configured Command builder for `wallet` operations + #[allow(unused)] pub fn wallet_cmd(&self, args: &[&str]) -> Command { let mut cmd = self.build_base_cmd(); cmd.arg("wallet"); From ae0c32acfa5ef9729ad62a5acb7fc13e969719cd Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Fri, 12 Jun 2026 14:10:20 +0100 Subject: [PATCH 03/11] test: Add integration tests for offline wallet ops - add integration tests for offline wallet operations --- tests/cli.rs | 152 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/tests/cli.rs b/tests/cli.rs index 955d958e..6faa8801 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -264,3 +264,155 @@ mod test_config { assert_eq!(config["int_descriptor"].as_str().unwrap(), int_desc); } } + +#[cfg(feature = "rpc")] +mod test_offline { + use super::*; + use assert_cmd::Command; + use serde_json::Value; + + static WALLET_NAME: &str = "test_config_wallet"; + + /// Helper to spin up a sandboxed CLI with the generated descriptors + fn setup_wallet_config() -> (BdkCli, Command) { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + + // generate descriptors + let desc = cli + .cmd("descriptor", &["--type", "tr"]) + .output() + .expect("Command to generate descriptors failed"); + + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("Invalid JSON from output descriptor"); + + let pub_desc = &desc_values["public_descriptors"]; + + let ext_desc = pub_desc["external"].as_str().unwrap(); + let int_desc = pub_desc["internal"].as_str().unwrap(); + let client_type = "rpc"; + let db: &str = "sqlite"; + let url = "http://localhost:18443"; + + let mut cmd_init = cli.build_base_cmd(); + cmd_init + .arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg(client_type) + .arg("--database-type") + .arg(db) + .arg("--url") + .arg(url); + (cli, cmd_init) + } + + #[test] + fn test_new_and_unused_address() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + // new address + cli.wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .assert() + .success() + .stdout(predicate::str::contains("\"address\":")); + + // `unused-address` + cli.wallet_cmd(&["--wallet", WALLET_NAME, "unused_address"]) + .assert() + .success() + .stdout(predicate::str::contains("\"address\":")); + } + + #[test] + fn test_empty_wallet_balances_and_lists() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + // balance + cli.wallet_cmd(&["--wallet", WALLET_NAME, "balance"]) + .assert() + .success() + .stdout(predicate::str::contains("\"total\":")) + .stdout(predicate::str::contains("\"trusted_pending\":")) + .stdout(predicate::str::contains("\"untrusted_pending\":")) + .stdout(predicate::str::contains("\"immature\":")) + .stdout(predicate::str::contains("\"confirmed\":")); + + // Unspent UTXOs + cli.wallet_cmd(&["--wallet", WALLET_NAME, "unspent"]) + .assert() + .success() + .stdout(predicate::str::contains("[]")); + + // Transactions + cli.wallet_cmd(&["--wallet", WALLET_NAME, "transactions"]) + .assert() + .success() + .stdout(predicate::str::contains("[]")); + } + + #[test] + fn test_policies_and_public_descriptor() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + // Policies + cli.wallet_cmd(&["--wallet", WALLET_NAME, "policies"]) + .assert() + .success() + .stdout(predicate::str::contains("\"external\":")) + .stdout(predicate::str::contains("\"internal\":")); + + // Public Descriptor + cli.wallet_cmd(&["--wallet", WALLET_NAME, "public_descriptor"]) + .assert() + .success() + .stdout(predicate::str::contains("external")) + .stdout(predicate::str::contains("internal")); + } + + #[test] + fn test_create_tx_insufficient_funds() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + // create transaction + cli.wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "create_tx", + "--to", + "tb1p4tp4l6glyr2gs94neqcpr5gha7344nfyznfkc8szkreflscsdkgqsdent4:1000", + ]) + .assert() + .failure() + .stderr(predicate::str::contains("Insufficient")); + } + + #[test] + fn test_combine_psbt_invalid_input() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + // Invalid create-tx. + cli.wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "combine_psbt", + "invalid_psbt", + "another_invalid_psbt", + ]) + .assert() + .failure() + .stderr(predicate::str::contains("Invalid")); + } +} From dfeb580555bbf5609689fca676dc6886fc13bca0 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Mon, 22 Jun 2026 22:18:00 +0100 Subject: [PATCH 04/11] test(online): Add test transaction full cycle - Add test for creating, signing, broadcasting tx - Add test for combining psbt, fee bump and other psbt related commands - Add bip322 tests --- Cargo.lock | 401 ++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + tests/cli.rs | 494 +++++++++++++++++++++++++++++++++++++++++++- tests/common/mod.rs | 6 + 4 files changed, 888 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba294410..40ec3dc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.4.3" @@ -34,6 +40,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "aes-gcm" version = "0.9.2" @@ -41,7 +58,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead 0.4.3", - "aes", + "aes 0.7.5", "cipher 0.3.0", "ctr", "ghash", @@ -119,6 +136,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayvec" version = "0.7.6" @@ -213,6 +236,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bdk-cli" version = "3.0.0" @@ -225,6 +254,7 @@ dependencies = [ "bdk_kyoto", "bdk_redb", "bdk_sp", + "bdk_testenv", "bdk_wallet", "clap", "clap_complete", @@ -299,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b59a3f7fbe678874fa34354097644a171276e02a49934c13b3d61c54610ddf39" dependencies = [ "bdk_core", - "electrum-client", + "electrum-client 0.24.1", ] [[package]] @@ -345,6 +375,16 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "bdk_testenv" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543dc273dab3b9ec329772bcb15741a948cc3510deae2a30af3a116f03505fee" +dependencies = [ + "bdk_chain", + "electrsd", +] + [[package]] name = "bdk_wallet" version = "2.1.0" @@ -576,6 +616,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.1", + "bitcoincore-rpc", + "flate2", + "log", + "minreq", + "tar", + "tempfile", + "which", + "zip", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -629,6 +693,26 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.52" @@ -842,6 +926,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.4" @@ -876,6 +966,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.4" @@ -933,6 +1038,12 @@ dependencies = [ "cipher 0.3.0", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "difflib" version = "0.4.0" @@ -997,6 +1108,40 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "electrsd" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c3c57645202a05a47206ed81cc179cf32bf4c3ca5a9e60c06c49d0222d5844" +dependencies = [ + "bitcoin_hashes 0.14.1", + "bitcoind", + "electrum-client 0.20.0", + "log", + "minreq", + "nix", + "which", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7b1f8783238bb18e6e137875b0a66f3dffe6c7ea84066e05d033cf180b150f" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + [[package]] name = "electrum-client" version = "0.24.1" @@ -1086,12 +1231,32 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -1377,6 +1542,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1716,7 +1890,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] @@ -1731,6 +1905,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1770,6 +1950,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniscript" version = "12.3.5" @@ -1781,6 +1970,16 @@ dependencies = [ "serde", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "minreq" version = "2.14.1" @@ -1823,6 +2022,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1838,6 +2051,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -1871,7 +2090,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1938,6 +2157,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "payjoin" version = "1.0.0-rc.1" @@ -1957,6 +2187,18 @@ dependencies = [ "url", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.9", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2045,6 +2287,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2237,7 +2485,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -2370,7 +2618,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2384,16 +2632,29 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -2521,7 +2782,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -2602,6 +2863,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2651,6 +2923,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.11" @@ -2722,6 +3000,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -2731,7 +3020,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -2799,6 +3088,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "tinystr" version = "0.8.2" @@ -2941,7 +3249,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -3251,6 +3559,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3462,6 +3782,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + [[package]] name = "yoke" version = "0.8.1" @@ -3579,8 +3909,57 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes 0.8.4", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac 0.12.1", + "pbkdf2", + "sha1", + "time", + "zstd", +] + [[package]] name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 3bad77f7..2295d666 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,4 +74,5 @@ silent-payments = ["dep:bdk_sp"] predicates = "3.0" tempfile = "3.8" assert_cmd = "2.2.2" +bdk_testenv = "0.13.1" diff --git a/tests/cli.rs b/tests/cli.rs index 6faa8801..1e061332 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -134,6 +134,49 @@ mod test_wallets { .failure() .stderr(predicate::str::contains("No wallets configured yet.")); } + + #[cfg(feature = "rpc")] + #[test] + fn test_list_wallets_with_entries() { + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("testnet", Some(temp_dir.path().to_path_buf())); + + for wallet_name in ["wallet_one", "wallet_two"] { + let desc = cli + .cmd("descriptor", &["--type", "tr"]) + .output() + .expect("Command to generate descriptors failed"); + let desc_values: serde_json::Value = serde_json::from_slice(&desc.stdout).unwrap(); + let pub_desc = &desc_values["public_descriptors"]; + let ext_desc = pub_desc["external"].as_str().unwrap(); + let int_desc = pub_desc["internal"].as_str().unwrap(); + + cli.build_base_cmd() + .arg("wallet") + .arg("--wallet") + .arg(wallet_name) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg("rpc") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg("http://localhost:18443") + .assert() + .success(); + } + + cli.build_base_cmd() + .arg("wallets") + .assert() + .success() + .stdout(predicate::str::contains("wallet_one")) + .stdout(predicate::str::contains("wallet_two")); + } } // --- DESCRIPTOR COMMAND TESTS --- @@ -287,10 +330,10 @@ mod test_offline { let desc_values: Value = serde_json::from_slice(&desc.stdout).expect("Invalid JSON from output descriptor"); - let pub_desc = &desc_values["public_descriptors"]; + let priv_desc = &desc_values["private_descriptors"]; - let ext_desc = pub_desc["external"].as_str().unwrap(); - let int_desc = pub_desc["internal"].as_str().unwrap(); + let ext_desc = priv_desc["external"].as_str().unwrap(); + let int_desc = priv_desc["internal"].as_str().unwrap(); let client_type = "rpc"; let db: &str = "sqlite"; let url = "http://localhost:18443"; @@ -415,4 +458,449 @@ mod test_offline { .failure() .stderr(predicate::str::contains("Invalid")); } + + #[cfg(feature = "bip322")] + #[test] + fn test_sign_message_and_verify_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let message = "bdk-cli integration test"; + + // Reveal exactly one address and reuse it for both sign and verify. + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + assert!( + addr_output.status.success(), + "new_address failed: {}", + String::from_utf8_lossy(&addr_output.stderr) + ); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"] + .as_str() + .expect("missing address") + .to_string(); + println!("DEBUG signing address: {address}"); + + // Sign (default signature_type = "simple"). + let sign_output = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "sign_message", + "--message", + message, + "--address", + &address, + ]) + .output() + .expect("Failed to sign message"); + assert!( + sign_output.status.success(), + "sign_message failed: {}", + String::from_utf8_lossy(&sign_output.stderr) + ); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().expect("missing proof"); + println!("DEBUG proof: {proof}"); + println!("DEBUG verifying same address: {address}"); + + // Verify with the identical address + message. + cli.wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "verify_message", + "--address", + &address, + "--message", + message, + "--proof", + proof, + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": true")); + } + + #[cfg(feature = "bip322")] + #[test] + fn test_verify_message_rejects_tampered_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"].as_str().unwrap(); + + let sign_output = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "sign_message", + "--message", + "original message", + "--address", + address, + ]) + .output() + .expect("Failed to sign message"); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().unwrap(); + + // A tampered message should fail verification gracefully (valid: false) + cli.wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "verify_message", + "--proof", + proof, + "--message", + "tampered message", + "--address", + address, + ]) + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": false")); + } +} + +#[cfg(feature = "electrum")] +mod test_online { + use crate::common::BdkCli; + use assert_cmd::Command; + use bdk_testenv::{TestEnv, bitcoincore_rpc::RpcApi}; + use bdk_wallet::bitcoin::{Address, Amount, Network, Txid}; + use serde_json::Value; + use std::str::FromStr; + use std::time::Duration; + use tempfile::TempDir; + + static WALLET_NAME: &str = "test_online_wallet"; + static RECIPIENT: &str = "tb1p4tp4l6glyr2gs94neqcpr5gha7344nfyznfkc8szkreflscsdkgqsdent4"; + + /// Spins up the envt, a Regtest node + electrs, and saves the wallet config. + fn setup_online_wallet() -> (BdkCli, Command, TestEnv) { + let env = TestEnv::new().expect("Failed to start bdk_testenv"); + let server_url = env.electrsd.electrum_url.as_str(); + + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); + + let desc = cli + .cmd("descriptor", &["--type", "tr"]) + .output() + .expect("Command to generate descriptors failed"); + let desc_values: Value = + serde_json::from_slice(&desc.stdout).expect("Invalid JSON from output descriptor"); + let priv_desc = &desc_values["private_descriptors"]; + let ext_desc = priv_desc["external"].as_str().unwrap(); + let int_desc = priv_desc["internal"].as_str().unwrap(); + + let mut cmd_init = cli.build_base_cmd(); + cmd_init + .arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg("electrum") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg(server_url); + + (cli, cmd_init, env) + } + + /// Mines blocks, funds wallet with 0.5 BTC, confirms it, and + /// runs `full_scan` so the wallet's persisted state reflects the funding. + /// Asserts the resulting confirmed balance is exactly 50,000,000 sats. + fn fund_and_sync_wallet(cli: &BdkCli, env: &TestEnv) { + let address = cli_new_address(cli); + + let node_address = env + .rpc_client() + .get_new_address(None, None) + .unwrap() + .assume_checked(); + + let mined_blocks = env + .mine_blocks(101, Some(node_address)) + .expect("Failed to mine initial blocks"); + assert_eq!(mined_blocks.len(), 101, "expected exactly 101 blocks mined"); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .expect("Electrum did not catch up to initial blocks"); + + let txid = env + .send(&address, Amount::from_btc(0.5).unwrap()) + .expect("Failed to fund wallet address"); + env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) + .expect("Electrum did not see funding tx"); + + env.mine_blocks(3, None) + .expect("Failed to confirm funding tx"); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .expect("Electrum did not catch up to confirmation blocks"); + + cli_full_scan(cli); + assert_eq!( + cli_balance(cli), + 50_000_000, + "wallet should show funded balance after full_scan" + ); + } + + /// Runs `wallet --wallet `, asserts the process exited + /// successfully, and parses stdout as JSON. + fn run_wallet_json(cli: &BdkCli, args: &[&str]) -> Value { + let mut full_args = vec!["--wallet", WALLET_NAME]; + full_args.extend_from_slice(args); + + let output = cli + .wallet_cmd(&full_args) + .output() + .unwrap_or_else(|e| panic!("failed to spawn `{}`: {e}", args.join(" "))); + + assert!( + output.status.success(), + "`{}` failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ); + + serde_json::from_slice(&output.stdout).unwrap_or_else(|e| { + panic!( + "`{}` returned non-JSON stdout ({e}): {}", + args.join(" "), + String::from_utf8_lossy(&output.stdout) + ) + }) + } + + fn cli_new_address(cli: &BdkCli) -> Address { + let json = run_wallet_json(cli, &["new_address"]); + let addr = json["address"] + .as_str() + .expect("new_address: missing 'address' field"); + Address::from_str(addr) + .expect("new_address: invalid address string") + .require_network(Network::Regtest) + .expect("new_address: wrong network") + } + + fn cli_full_scan(cli: &BdkCli) { + run_wallet_json(cli, &["full_scan"]); + } + + fn cli_sync(cli: &BdkCli) { + run_wallet_json(cli, &["sync"]); + } + + fn cli_balance(cli: &BdkCli) -> u64 { + let json = run_wallet_json(cli, &["balance"]); + json["confirmed"] + .as_u64() + .expect("balance: missing 'confirmed' field") + } + + fn cli_create_tx(cli: &BdkCli, to: &str) -> String { + let json = run_wallet_json(cli, &["create_tx", "--to", to]); + json["psbt"] + .as_str() + .expect("create_tx: missing 'psbt' field") + .to_string() + } + + /// Returns (signed_psbt_base64, is_finalized). + fn cli_sign(cli: &BdkCli, psbt: &str) -> (String, bool) { + let json = run_wallet_json(cli, &["sign", psbt]); + let signed = json["psbt"] + .as_str() + .expect("sign: missing 'psbt' field") + .to_string(); + let finalized = json["is_finalized"] + .as_bool() + .expect("sign: missing 'is_finalized' field"); + (signed, finalized) + } + + fn cli_extract_psbt(cli: &BdkCli, psbt: &str) -> String { + let json = run_wallet_json(cli, &["extract_psbt", psbt]); + json["raw_tx"] + .as_str() + .expect("extract_psbt: missing 'raw_tx' field") + .to_string() + } + + fn cli_broadcast(cli: &BdkCli, raw_tx: &str) -> Txid { + let json = run_wallet_json(cli, &["broadcast", "--tx", raw_tx]); + let txid = json["txid"] + .as_str() + .expect("broadcast: missing 'txid' field"); + Txid::from_str(txid).expect("broadcast: invalid txid") + } + + #[test] + fn test_full_online_transaction_lifecycle() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + + fund_and_sync_wallet(&cli, &env); + + // Build, sign, extract, and broadcast a spend + let unsigned_psbt = cli_create_tx(&cli, &format!("{RECIPIENT}:20000")); + + let (signed_psbt, finalized) = cli_sign(&cli, &unsigned_psbt); + assert!(finalized, "PSBT should be finalized after signing"); + + let raw_tx = cli_extract_psbt(&cli, &signed_psbt); + + let spend_txid = cli_broadcast(&cli, &raw_tx); + env.rpc_client() + .get_mempool_entry(&spend_txid) + .expect("broadcast tx not found in node mempool"); + + // Confirm the spend and verify the final balance + env.mine_blocks(1, None) + .expect("Failed to mine spend confirmation block"); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .expect("Electrum did not catch up to spend confirmation"); + + cli_sync(&cli); + let final_balance = cli_balance(&cli); + assert!( + (49_900_000..50_000_000).contains(&final_balance), + "unexpected post-spend confirmed balance: {final_balance}" + ); + } + + #[test] + fn test_finalize_psbt_on_signed_tx() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + let unsigned_psbt = cli_create_tx(&cli, &format!("{RECIPIENT}:15000")); + let (signed_psbt, finalized) = cli_sign(&cli, &unsigned_psbt); + assert!(finalized, "PSBT should be finalized after signing"); + + // finalize_psbt should be idempotent: finalizing an already-finalized + // PSBT should succeed and still report is_finalized = true. + let finalize_json = run_wallet_json(&cli, &["finalize_psbt", &signed_psbt]); + assert_eq!( + finalize_json["is_finalized"].as_bool(), + Some(true), + "finalize_psbt should report the PSBT as finalized" + ); + } + + #[test] + fn test_combine_psbt_merges_signatures() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + let unsigned_psbt = cli_create_tx(&cli, &format!("{RECIPIENT}:15000")); + let (signed_psbt, finalized) = cli_sign(&cli, &unsigned_psbt); + assert!(finalized, "PSBT should be finalized after signing"); + + // Combine the unsigned and signed versions of the same transaction + let combined_json = run_wallet_json(&cli, &["combine_psbt", &unsigned_psbt, &signed_psbt]); + let combined_psbt = combined_json["psbt"] + .as_str() + .expect("combine_psbt: missing 'psbt' field"); + + // If signature data survived the combine, this should extract cleanly. + let raw_tx = cli_extract_psbt(&cli, combined_psbt); + assert!( + !raw_tx.is_empty(), + "combined PSBT did not extract to a valid raw transaction" + ); + } + + #[test] + fn test_bump_fee_replaces_unconfirmed_tx() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + // Create and broadcast a low-fee-rate, RBF-enabled transaction. + let unsigned_psbt = run_wallet_json( + &cli, + &[ + "create_tx", + "--to", + &format!("{RECIPIENT}:15000"), + "--fee_rate", + "1.0", + ], + )["psbt"] + .as_str() + .expect("create_tx: missing 'psbt' field") + .to_string(); + + let (signed_psbt, finalized) = cli_sign(&cli, &unsigned_psbt); + assert!(finalized); + let raw_tx = cli_extract_psbt(&cli, &signed_psbt); + let original_txid = cli_broadcast(&cli, &raw_tx); + + env.rpc_client() + .get_mempool_entry(&original_txid) + .expect("original tx should be in the mempool before bumping"); + + env.wait_until_electrum_sees_txid(original_txid, Duration::from_secs(10)) + .expect("electrs did not see the broadcast tx"); + cli_sync(&cli); + + // Bump to a much higher fee rate; this should replace the original tx. + let bumped_json = run_wallet_json( + &cli, + &[ + "bump_fee", + "--txid", + &original_txid.to_string(), + "--fee_rate", + "10.0", + ], + ); + let bumped_psbt = bumped_json["psbt"] + .as_str() + .expect("bump_fee: missing 'psbt' field"); + + let (signed_bumped, bumped_finalized) = cli_sign(&cli, bumped_psbt); + assert!( + bumped_finalized, + "bumped PSBT should be finalized after signing" + ); + + let bumped_raw_tx = cli_extract_psbt(&cli, &signed_bumped); + let bumped_txid = cli_broadcast(&cli, &bumped_raw_tx); + + assert_ne!( + original_txid, bumped_txid, + "fee-bumped transaction should have a different txid than the original" + ); + + env.mine_blocks(1, None) + .expect("Failed to confirm bumped tx"); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .expect("Electrum did not catch up after bump confirmation"); + cli_sync(&cli); + + let final_balance = cli_balance(&cli); + assert!( + final_balance < 50_000_000, + "balance should reflect exactly one confirmed spend after RBF, got {final_balance}" + ); + } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 4dbd261f..19be6989 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -22,6 +22,7 @@ pub struct BdkCli { pub verbosity: bool, pub recv_desc: Option, pub change_desc: Option, + pub server_url: Option, } impl BdkCli { @@ -33,6 +34,7 @@ impl BdkCli { verbosity: false, recv_desc: None, change_desc: None, + server_url: None, } } @@ -50,6 +52,10 @@ impl BdkCli { cmd.arg("--verbose"); } + if let Some(url) = &self.server_url { + cmd.arg("--server").arg(url); + } + cmd } From eec81e2cd5d42028da3e921bb979a9aa11c9e849 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Wed, 24 Jun 2026 20:10:27 +0100 Subject: [PATCH 05/11] ref(verbose): Dropped `verbose` flag from tests --- src/commands.rs | 3 --- src/config.rs | 1 - src/handlers/config.rs | 1 - src/handlers/offline.rs | 10 +++++----- src/main.rs | 12 +++++++++--- src/utils/types.rs | 10 +--------- tests/cli.rs | 2 +- tests/common/mod.rs | 6 ------ 8 files changed, 16 insertions(+), 29 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index d5ae053f..ac35877e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -223,9 +223,6 @@ pub struct WalletOpts { /// Selects the wallet to use. #[arg(skip)] pub wallet: Option, - /// Adds verbosity, returns PSBT in JSON format alongside serialized, displays expanded objects. - #[arg(env = "VERBOSE", short = 'v', long = "verbose")] - pub verbose: bool, /// Sets the descriptor to use for the external addresses. #[arg(env = "EXT_DESCRIPTOR", short = 'e', long, required = true)] pub ext_descriptor: String, diff --git a/src/config.rs b/src/config.rs index ec6adfaa..6865ee09 100644 --- a/src/config.rs +++ b/src/config.rs @@ -117,7 +117,6 @@ impl TryFrom<&WalletConfigInner> for WalletOpts { Ok(WalletOpts { wallet: Some(config.wallet.clone()), - verbose: false, ext_descriptor: config.ext_descriptor.clone(), int_descriptor: config.int_descriptor.clone(), diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 3bd302c0..8fe1c9d4 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -95,7 +95,6 @@ impl AppCommand> for SaveConfigCommand { network: ctx.network.to_string(), ext_descriptor: self.wallet_opts.ext_descriptor.clone(), int_descriptor: self.wallet_opts.int_descriptor.clone(), - #[cfg(any(feature = "sqlite", feature = "redb"))] database_type: match self.wallet_opts.database_type { #[cfg(feature = "sqlite")] diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index b4412301..6839b9b0 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -303,7 +303,7 @@ impl AppCommand>> for CreateTxCommand { // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); - Ok(PsbtResult::new(&psbt, false, Some(false))) + Ok(PsbtResult::new(&psbt, Some(false))) } } @@ -594,7 +594,7 @@ impl AppCommand>> for BumpFeeCommand { // let psbt_base64 = BASE64_STANDARD.encode(psbt.serialize()); - Ok(PsbtResult::new(&psbt, false, Some(false))) + Ok(PsbtResult::new(&psbt, Some(false))) } } @@ -662,7 +662,7 @@ impl AppCommand>> for SignCommand { ..Default::default() }; let finalized = wallet.sign(&mut psbt, signopt)?; - Ok(PsbtResult::new(&psbt, false, Some(finalized))) + Ok(PsbtResult::new(&psbt, Some(finalized))) } } @@ -718,7 +718,7 @@ impl AppCommand>> for FinalizePsbtCommand { let finalized = wallet.finalize_psbt(&mut psbt, signopt)?; - Ok(PsbtResult::new(&psbt, false, Some(finalized))) + Ok(PsbtResult::new(&psbt, Some(finalized))) } } @@ -753,7 +753,7 @@ impl AppCommand>> for CombinePsbtCommand { Ok(acc) })?; - Ok(PsbtResult::new(&final_psbt, false, None)) + Ok(PsbtResult::new(&final_psbt, Some(false))) } } diff --git a/src/main.rs b/src/main.rs index 3f399dc7..261d9fec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,7 +34,7 @@ use crate::handlers::{AppCommand, AppContext}; use crate::utils::output::FormatOutput; use crate::utils::runtime::WalletRuntime; use crate::utils::{command_requires_db, prepare_home_dir}; -use clap::Parser; +use clap::{CommandFactory, Parser}; #[tokio::main] async fn main() { @@ -187,8 +187,14 @@ async fn run(cli_opts: CliOpts) -> Result<(), Error> { cmd.execute(&mut ctx)?.write_out(std::io::stdout())?; } - CliSubCommand::Completions { shell: _ } => unimplemented!(), - + CliSubCommand::Completions { shell } => { + clap_complete::generate( + shell, + &mut CliOpts::command(), + "bdk-cli", + &mut std::io::stdout(), + ); + } #[cfg(feature = "silent-payments")] CliSubCommand::SilentPaymentCode(cmd) => { let mut ctx = AppContext::new(cli_opts.network, home_dir); diff --git a/src/utils/types.rs b/src/utils/types.rs index a8e29984..99bdcbd9 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -78,21 +78,13 @@ pub struct PsbtResult { #[serde(skip_serializing_if = "Option::is_none")] pub is_finalized: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub details: Option, } impl PsbtResult { - pub fn new(psbt: &Psbt, verbose: bool, finalized: Option) -> Self { + pub fn new(psbt: &Psbt, finalized: Option) -> Self { Self { psbt: bdk_wallet::bitcoin::base64::prelude::BASE64_STANDARD.encode(psbt.serialize()), is_finalized: finalized, - details: if verbose { - Some(serde_json::to_value(psbt).unwrap_or_default()) - } else { - None - }, } } } diff --git a/tests/cli.rs b/tests/cli.rs index 1e061332..8f597176 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -552,7 +552,7 @@ mod test_offline { let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); let proof = sign_json["proof"].as_str().unwrap(); - // A tampered message should fail verification gracefully (valid: false) + // A tampered message should fail cli.wallet_cmd(&[ "--wallet", WALLET_NAME, diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 19be6989..43220166 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -19,7 +19,6 @@ use std::path::PathBuf; pub struct BdkCli { pub network: String, pub datadir: Option, - pub verbosity: bool, pub recv_desc: Option, pub change_desc: Option, pub server_url: Option, @@ -31,7 +30,6 @@ impl BdkCli { Self { network: network.to_string(), datadir, - verbosity: false, recv_desc: None, change_desc: None, server_url: None, @@ -48,10 +46,6 @@ impl BdkCli { cmd.arg("--datadir").arg(dir); } - if self.verbosity { - cmd.arg("--verbose"); - } - if let Some(url) = &self.server_url { cmd.arg("--server").arg(url); } From cbee8a01d33ac4e99a2514084a70d95301b593e4 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 25 Jun 2026 18:58:41 +0100 Subject: [PATCH 06/11] fix(proxy_opts): add saving and reading proxy_opts - add saving proxy_opts for electrum and esplora clients to config and reading values - fix prepare_home_dir util fn --- src/commands.rs | 7 ++++-- src/config.rs | 52 +++++++++++++++++++++++++++++++++++++++-- src/handlers/config.rs | 16 +++++++++++++ src/handlers/offline.rs | 7 ++---- src/utils/common.rs | 31 ++++++++++-------------- 5 files changed, 85 insertions(+), 28 deletions(-) diff --git a/src/commands.rs b/src/commands.rs index ac35877e..7559d5d3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -274,6 +274,9 @@ pub struct WalletOpts { #[cfg(feature = "cbf")] #[clap(flatten)] pub compactfilter_opts: CompactFilterOpts, + #[cfg(any(feature = "electrum", feature = "esplora"))] + #[command(flatten)] + pub proxy_opts: ProxyOpts, } /// Options to configure a SOCKS5 proxy for a blockchain client connection. @@ -281,11 +284,11 @@ pub struct WalletOpts { #[derive(Debug, Args, Clone, PartialEq, Eq)] pub struct ProxyOpts { /// Sets the SOCKS5 proxy for a blockchain client. - #[arg(env = "PROXY_ADDRS:PORT", long = "proxy", short = 'p')] + #[arg(env = "PROXY_ADDRS:PORT", long = "proxy")] pub proxy: Option, /// Sets the SOCKS5 proxy credential. - #[arg(env = "PROXY_USER:PASSWD", long="proxy_auth", short='a', value_parser = parse_proxy_auth)] + #[arg(env = "PROXY_USER:PASSWD", long="proxy_auth", value_parser = parse_proxy_auth)] pub proxy_auth: Option<(String, String)>, /// Sets the SOCKS5 proxy retries for the blockchain client. diff --git a/src/config.rs b/src/config.rs index 6865ee09..60580037 100644 --- a/src/config.rs +++ b/src/config.rs @@ -50,6 +50,21 @@ pub struct WalletConfigInner { pub parallel_requests: Option, #[cfg(feature = "rpc")] pub cookie: Option, + #[cfg(any(feature = "electrum", feature = "esplora"))] + #[serde(default)] + pub proxy: Option, + #[cfg(any(feature = "electrum", feature = "esplora"))] + #[serde(default)] + pub proxy_auth: Option, + #[cfg(any(feature = "electrum", feature = "esplora"))] + #[serde(default)] + pub proxy_retries: Option, + #[cfg(any(feature = "electrum", feature = "esplora"))] + #[serde(default)] + pub proxy_timeout: Option, + #[cfg(feature = "cbf")] + #[serde(default)] + pub conn_count: Option, } impl WalletConfig { @@ -93,7 +108,7 @@ impl TryFrom<&WalletConfigInner> for WalletOpts { type Error = Error; fn try_from(config: &WalletConfigInner) -> Result { - let _network = Network::from_str(&config.network) + Network::from_str(&config.network) .map_err(|_| Error::Generic("Invalid network".to_string()))?; #[cfg(any(feature = "sqlite", feature = "redb"))] @@ -155,8 +170,21 @@ impl TryFrom<&WalletConfigInner> for WalletOpts { #[cfg(feature = "rpc")] cookie: config.cookie.clone(), + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_opts: crate::commands::ProxyOpts { + proxy: config.proxy.clone(), + proxy_auth: match &config.proxy_auth { + Some(s) => Some(crate::utils::parse_proxy_auth(s)?), + None => None, + }, + retries: config.proxy_retries.unwrap_or(5), + timeout: config.proxy_timeout, + }, + #[cfg(feature = "cbf")] - compactfilter_opts: crate::commands::CompactFilterOpts { conn_count: 2 }, + compactfilter_opts: crate::commands::CompactFilterOpts { + conn_count: config.conn_count.unwrap_or(2), + }, }) } } @@ -218,6 +246,16 @@ mod tests { rpc_password: None, #[cfg(feature = "rpc")] cookie: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_auth: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_retries: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_timeout: None, + #[cfg(feature = "cbf")] + conn_count: None, }; let opts: WalletOpts = (&wallet_config) @@ -293,6 +331,16 @@ mod tests { rpc_password: None, #[cfg(feature = "rpc")] cookie: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_auth: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_retries: None, + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_timeout: None, + #[cfg(feature = "cbf")] + conn_count: None, }; let result: Result = (&inner).try_into(); diff --git a/src/handlers/config.rs b/src/handlers/config.rs index 8fe1c9d4..5dd26dcf 100644 --- a/src/handlers/config.rs +++ b/src/handlers/config.rs @@ -124,6 +124,22 @@ impl AppCommand> for SaveConfigCommand { parallel_requests: Some(self.wallet_opts.parallel_requests), #[cfg(feature = "rpc")] cookie: self.wallet_opts.cookie.clone(), + + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy: self.wallet_opts.proxy_opts.proxy.clone(), + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_auth: self + .wallet_opts + .proxy_opts + .proxy_auth + .as_ref() + .map(|(u, p)| format!("{u}:{p}")), + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_retries: Some(self.wallet_opts.proxy_opts.retries), + #[cfg(any(feature = "electrum", feature = "esplora"))] + proxy_timeout: self.wallet_opts.proxy_opts.timeout, + #[cfg(feature = "cbf")] + conn_count: Some(self.wallet_opts.compactfilter_opts.conn_count), }; config.wallets.insert(wallet_name.clone(), wallet_config); diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 6839b9b0..92b1f338 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -16,7 +16,6 @@ use bdk_wallet::{KeychainKind, SignOptions}; use clap::Parser; use serde_json::json; use std::collections::BTreeMap; -use std::str::FromStr; #[cfg(feature = "silent-payments")] use { crate::utils::common::parse_sp_code_value_pairs, @@ -532,7 +531,7 @@ impl AppCommand>> for CreateSpTxCommand { pub struct BumpFeeCommand { /// TXID of the transaction to update. #[arg(env = "TXID", long = "txid")] - pub txid: String, + pub txid: Txid, /// Allows the wallet to reduce the amount to the specified address in order to increase fees. #[arg(env = "SHRINK_ADDRESS", long = "shrink", value_parser = parse_address)] @@ -566,9 +565,7 @@ impl AppCommand>> for BumpFeeCommand { fn execute(&self, ctx: &mut AppContext>) -> Result { let wallet = &mut ctx.state.wallet; - let txid = Txid::from_str(self.txid.as_str())?; - - let mut tx_builder = wallet.build_fee_bump(txid)?; + let mut tx_builder = wallet.build_fee_bump(self.txid)?; let fee_rate = FeeRate::from_sat_per_vb(self.fee_rate as u64).unwrap_or(FeeRate::BROADCAST_MIN); tx_builder.fee_rate(fee_rate); diff --git a/src/utils/common.rs b/src/utils/common.rs index 21be3ad4..051ebe03 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -95,19 +95,18 @@ pub(crate) fn parse_address(address_str: &str) -> Result { /// If not the default home directory is created at `~/.bdk-bitcoin`. #[allow(dead_code)] pub(crate) fn prepare_home_dir(home_path: Option) -> Result { - let dir = home_path.unwrap_or_else(|| { - let mut dir = PathBuf::new(); - dir.push( - dirs::home_dir() - .ok_or_else(|| Error::Generic("home dir not found".to_string())) - .unwrap(), - ); - dir.push(".bdk-bitcoin"); - dir - }); + let dir = match home_path { + Some(dir) => dir, + None => { + let mut dir = + dirs::home_dir().ok_or_else(|| Error::Generic("Home dir not found".to_string()))?; + dir.push(".bdk-bitcoin"); + dir + } + }; if !dir.exists() { - std::fs::create_dir(&dir).map_err(|e| Error::Generic(e.to_string()))?; + std::fs::create_dir_all(&dir).map_err(|e| Error::Generic(e.to_string()))?; } Ok(dir) @@ -171,14 +170,8 @@ pub fn load_wallet_config( "Wallet '{wallet_name}' not found in config" )))?; - let network = match wallet_config.network.as_str() { - "bitcoin" => Ok(Network::Bitcoin), - "testnet" => Ok(Network::Testnet), - "regtest" => Ok(Network::Regtest), - "signet" => Ok(Network::Signet), - "testnet4" => Ok(Network::Testnet4), - _ => Err(Error::Generic("Invalid network in config".to_string())), - }?; + let network = Network::from_str(&wallet_config.network) + .map_err(|_| Error::Generic("Invalid network in config".to_string()))?; Ok((wallet_opts, network)) } From d89a08d3f6f323da6ff9faf676d2a7a75642aca9 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 30 Jun 2026 13:39:52 +0100 Subject: [PATCH 07/11] feat(wallet-3.1.0): Update Wallet to v3.1.0 - Update bdk_wallet to v3.1.0 - Replaced Network enum with NetworkKind - Replaced `include_output_redeem_witness_script` with `add_global_xpubs` - update bdk_redb to v0.2.0 --- Cargo.lock | 34 ++++++++++++++++++++++++---------- Cargo.toml | 4 ++-- src/handlers/descriptor.rs | 6 +++--- src/handlers/key.rs | 4 ++-- src/handlers/offline.rs | 6 +++--- src/utils/descriptors.rs | 12 ++++++------ 6 files changed, 40 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 40ec3dc8..9425b2ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,7 +255,7 @@ dependencies = [ "bdk_redb", "bdk_sp", "bdk_testenv", - "bdk_wallet", + "bdk_wallet 3.1.0", "clap", "clap_complete", "cli-table", @@ -283,7 +283,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "262ee5f0c1056400b354391cabc541cb732c10488679de6389641e37f7e6c698" dependencies = [ - "bdk_wallet", + "bdk_wallet 2.1.0", "bitcoin", ] @@ -300,9 +300,9 @@ dependencies = [ [[package]] name = "bdk_chain" -version = "0.23.2" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5d691fd092aacec7e05046b7d04897d58d6d65ed3152cb6cf65dababcfabed" +checksum = "c290eff038799a8ac0c5a82b6160a9ca456baa299a6f22b262c771342d2846c0" dependencies = [ "bdk_core", "bitcoin", @@ -313,9 +313,9 @@ dependencies = [ [[package]] name = "bdk_core" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dbbe4aad0c898bfeb5253c222be3ea3dccfb380a07e72c87e3e4ed6664a6753" +checksum = "cb3028782f6bf14a6df987244333d34e6b272b5a40a53e4879ec2dfd82275a3a" dependencies = [ "bitcoin", "hashbrown 0.14.5", @@ -350,18 +350,18 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f35b9f8b3aa8c4647bec7a92b050c496742d955e0ac1edcb4e7c2deabf63c54" dependencies = [ - "bdk_wallet", + "bdk_wallet 2.1.0", "bip157", ] [[package]] name = "bdk_redb" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c21112531eccdcb15c120d6c39f704b0c733b12a3dff6bcce106984ca9f1011" +checksum = "69ac8b88c9594c615fcb5b763e7afa16937e94f7a7f5ca334bf59a7fee69fea1" dependencies = [ "bdk_chain", - "bdk_wallet", + "bdk_wallet 3.1.0", "ciborium", "redb", "thiserror 2.0.18", @@ -390,6 +390,20 @@ name = "bdk_wallet" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d30b5dba770184863b5d966ccbc6a11d12c145450be3b6a4435308297e6a12dc" +dependencies = [ + "bdk_chain", + "bitcoin", + "miniscript", + "rand_core 0.6.4", + "serde", + "serde_json", +] + +[[package]] +name = "bdk_wallet" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1284fb23acc3e3022673712b55f4d5ce7e38aadc2c49bbef830dc3935f0a3289" dependencies = [ "bdk_chain", "bip39", diff --git a/Cargo.toml b/Cargo.toml index 2295d666..c3c69385 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "MIT" [dependencies] -bdk_wallet = { version = "2.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } +bdk_wallet = { version = "3.1.0", features = ["rusqlite", "keys-bip39", "compiler", "std"] } clap = { version = "4.6", features = ["derive","env"] } clap_complete = "4.6" dirs = { version = "6.0.0" } @@ -32,7 +32,7 @@ bdk_bitcoind_rpc = { version = "0.21.0", features = ["std"], optional = true } bdk_electrum = { version = "0.23.2", optional = true } bdk_esplora = { version = "0.22.1", features = ["async-https", "tokio"], optional = true } bdk_kyoto = { version = "0.15.4", optional = true } -bdk_redb = { version = "0.1.1", optional = true } +bdk_redb = { version = "0.2.0", optional = true } bdk_sp = { version = "0.1.0", optional = true, git = "https://github.com/bitcoindevkit/bdk-sp", tag = "v0.1.0" } shlex = { version = "1.3.0", optional = true } payjoin = { version = "=1.0.0-rc.1", features = ["v1", "v2", "io", "_test-utils"], optional = true} diff --git a/src/handlers/descriptor.rs b/src/handlers/descriptor.rs index 1396c888..d76d47fc 100644 --- a/src/handlers/descriptor.rs +++ b/src/handlers/descriptor.rs @@ -49,12 +49,12 @@ impl AppCommand> for DescriptorCommand { match &self.key { Some(key) => { if is_mnemonic(key) { - generate_descriptor_from_mnemonic(key, ctx.network, &self.desc_type) + generate_descriptor_from_mnemonic(key, ctx.network.into(), &self.desc_type) } else { - generate_descriptors(&self.desc_type, key, ctx.network) + generate_descriptors(&self.desc_type, key, ctx.network.into()) } } - None => generate_descriptor_with_mnemonic(ctx.network, &self.desc_type), + None => generate_descriptor_with_mnemonic(ctx.network.into(), &self.desc_type), } } } diff --git a/src/handlers/key.rs b/src/handlers/key.rs index ce8c844e..ebff8a79 100644 --- a/src/handlers/key.rs +++ b/src/handlers/key.rs @@ -60,7 +60,7 @@ impl AppCommand> for GenerateKeyCommand { .map_err(|_| Error::Generic("Mnemonic generation error".to_string()))?; let mnemonic = mnemonic.into_key(); let xkey: ExtendedKey = (mnemonic.clone(), self.password.clone()).into_extended_key()?; - let xprv = xkey.into_xprv(ctx.network).ok_or_else(|| { + let xprv = xkey.into_xprv(ctx.network.into()).ok_or_else(|| { Error::Generic("Privatekey info not found (should not happen)".to_string()) })?; let fingerprint = xprv.fingerprint(&secp); @@ -143,7 +143,7 @@ impl AppCommand> for RestoreKeyCommand { let mnemonic = Mnemonic::parse_in(Language::English, &self.mnemonic)?; let xkey: ExtendedKey = (mnemonic.clone(), &self.password).0.into_extended_key()?; - let xprv = xkey.into_xprv(ctx.network).ok_or_else(|| { + let xprv = xkey.into_xprv(ctx.network.into()).ok_or_else(|| { Error::Generic("Privatekey info not found (should not happen)".to_string()) })?; let fingerprint = xprv.fingerprint(&secp); diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 92b1f338..5af69b59 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -259,7 +259,7 @@ impl AppCommand>> for CreateTxCommand { } if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(fee_rate) = self.fee_rate @@ -414,7 +414,7 @@ impl AppCommand>> for CreateSpTxCommand { tx_builder.set_exact_sequence(Sequence::MAX); if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(fee_rate) = self.fee_rate @@ -576,7 +576,7 @@ impl AppCommand>> for BumpFeeCommand { } if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(utxos) = &self.utxos { diff --git a/src/utils/descriptors.rs b/src/utils/descriptors.rs index 04391e50..f0689861 100644 --- a/src/utils/descriptors.rs +++ b/src/utils/descriptors.rs @@ -6,7 +6,7 @@ use bdk_wallet::{ KeychainKind, bip39::{Language, Mnemonic}, bitcoin::{ - Network, + NetworkKind, bip32::{DerivationPath, Xpriv, Xpub}, secp256k1::Secp256k1, }, @@ -25,7 +25,7 @@ use crate::utils::types::KeychainPair; pub fn generate_descriptors( desc_type: &str, key: &str, - network: Network, + network: NetworkKind, ) -> Result { let is_private = key.starts_with("xprv") || key.starts_with("tprv"); @@ -40,7 +40,7 @@ pub fn generate_descriptors( _ => 84u32, }; let coin_type = match network { - Network::Bitcoin => 0u32, + NetworkKind::Main => 0u32, _ => 1u32, }; let derivation_path = DerivationPath::from_str(&format!("m/{purpose}h/{coin_type}h/0h"))?; @@ -52,7 +52,7 @@ pub fn generate_descriptors( fn generate_private_descriptors( desc_type: &str, key: &str, - network: Network, + network: NetworkKind, ) -> Result { use bdk_wallet::template::{Bip44, Bip49, Bip84, Bip86}; @@ -166,7 +166,7 @@ pub fn build_public_descriptor( /// Generate new mnemonic and descriptors pub fn generate_descriptor_with_mnemonic( - network: Network, + network: NetworkKind, desc_type: &str, ) -> Result { let mnemonic: GeneratedKey = @@ -183,7 +183,7 @@ pub fn generate_descriptor_with_mnemonic( /// Generate descriptors from existing mnemonic pub fn generate_descriptor_from_mnemonic( mnemonic_str: &str, - network: Network, + network: NetworkKind, desc_type: &str, ) -> Result { let mnemonic = Mnemonic::parse_in(Language::English, mnemonic_str)?; From 205d46c15f4e6c54b021d1f5192f6985e1a067c4 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 30 Jun 2026 14:07:16 +0100 Subject: [PATCH 08/11] feat(cbf): Update bdk_kyoto to v0.17.0 - update bdk_kyoto to v0.17.0 - add ew broadcast/subscriber API for KyotoClient --- Cargo.lock | 37 ++++++++++++++++++++++++++++++++----- Cargo.toml | 2 +- src/client.rs | 27 ++++++++++----------------- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9425b2ec..1031c690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,11 +346,11 @@ dependencies = [ [[package]] name = "bdk_kyoto" -version = "0.15.4" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f35b9f8b3aa8c4647bec7a92b050c496742d955e0ac1edcb4e7c2deabf63c54" +checksum = "346c1d502ee4613cf2201db9c3b93a2317772d8eaa372114f34bf5f374be94fd" dependencies = [ - "bdk_wallet 2.1.0", + "bdk_wallet 3.1.0", "bip157", ] @@ -431,13 +431,14 @@ dependencies = [ [[package]] name = "bip157" -version = "0.3.4" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88df5c18baaea9be4219679afbd4fc26491606f89f6ecdaffcdcabd67635b07b" +checksum = "14485468baff5ec4114d186f1683635f3094de9f5a4b3953bf82ac6cccba40e8" dependencies = [ "bip324", "bitcoin", "bitcoin-address-book", + "bitcoin_hashes 0.20.0", "tokio", ] @@ -493,6 +494,15 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "bitcoin-consensus-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d7ca3dc8ff835693ad73bf1596240c06f974a31eeb3f611aaedf855f1f2725" +dependencies = [ + "bitcoin-internals 0.5.0", +] + [[package]] name = "bitcoin-hpke" version = "0.13.0" @@ -527,6 +537,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90bbbfa552b49101a230fb2668f3f9ef968c81e6f83cf577e1d4b80f689e1aa" +[[package]] +name = "bitcoin-internals" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30a22d1f112dde8e16be7b45c63645dc165cef254f835b3e1e9553e485cfa64" + [[package]] name = "bitcoin-io" version = "0.1.4" @@ -596,6 +612,17 @@ dependencies = [ "hex-conservative 0.3.2", ] +[[package]] +name = "bitcoin_hashes" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8a45c2b41c457a9a9e4670422fcbdf109afb3b22bc920b4045e8bdfd788a3d" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.5.0", + "hex-conservative 0.3.2", +] + [[package]] name = "bitcoin_uri" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index c3c69385..14184d4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ serde= {version = "1.0", features = ["derive"]} bdk_bitcoind_rpc = { version = "0.21.0", features = ["std"], optional = true } bdk_electrum = { version = "0.23.2", optional = true } bdk_esplora = { version = "0.22.1", features = ["async-https", "tokio"], optional = true } -bdk_kyoto = { version = "0.15.4", optional = true } +bdk_kyoto = { version = "0.17.0", optional = true } bdk_redb = { version = "0.2.0", optional = true } bdk_sp = { version = "0.1.0", optional = true, git = "https://github.com/bitcoindevkit/bdk-sp", tag = "v0.1.0" } shlex = { version = "1.3.0", optional = true } diff --git a/src/client.rs b/src/client.rs index ad51391e..9aed57fe 100644 --- a/src/client.rs +++ b/src/client.rs @@ -23,10 +23,7 @@ use { }; #[cfg(feature = "cbf")] -use { - crate::utils::trace_logger, - bdk_kyoto::{BuilderExt, LightClient}, -}; +use {crate::utils::trace_logger, bdk_kyoto::BuilderExt}; #[cfg(any( feature = "electrum", @@ -103,7 +100,7 @@ impl BlockchainClient { let txid = tx.compute_txid(); let wtxid = client .requester - .broadcast_random(tx.clone()) + .submit_package(tx) .await .map_err(|_| { tracing::warn!("Broadcast was unsuccessful"); @@ -205,7 +202,8 @@ impl BlockchainClient { #[cfg(feature = "cbf")] pub struct KyotoClientHandle { pub requester: bdk_kyoto::Requester, - pub update_subscriber: tokio::sync::Mutex, + pub update_subscriber: + tokio::sync::Mutex>, } #[cfg(any( @@ -267,21 +265,16 @@ pub(crate) fn new_blockchain_client( .data_dir(&_datadir) .build_with_wallet(_wallet, scan_type)?; - let LightClient { - requester, - info_subscriber, - warning_subscriber, - update_subscriber, - node, - } = light_client; + let (client, logging, update_subscriber) = light_client.subscribe(); + // `start()` spawns the node's run loop on a tokio task internally. + let requester = client.start().requester(); let subscriber = tracing_subscriber::FmtSubscriber::new(); let _ = tracing::subscriber::set_global_default(subscriber); - tokio::task::spawn(async move { node.run().await }); - tokio::task::spawn( - async move { trace_logger(info_subscriber, warning_subscriber).await }, - ); + tokio::task::spawn(async move { + trace_logger(logging.info_subscriber, logging.warning_subscriber).await + }); BlockchainClient::KyotoClient { client: Box::new(KyotoClientHandle { From 242d4da7feda9a1c2e564c8ebb811e2e018a6e4f Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Tue, 30 Jun 2026 15:19:29 +0100 Subject: [PATCH 09/11] feat(utxo-locking): Add wallet locking commands - add wallet commands to lock, unlock and list locked utxos - update unspent command to show status of each outpoint --- src/client.rs | 12 +++---- src/commands.rs | 11 ++++-- src/handlers/offline.rs | 77 +++++++++++++++++++++++++++++++++++++++-- src/utils/common.rs | 5 ++- src/utils/types.rs | 4 ++- 5 files changed, 94 insertions(+), 15 deletions(-) diff --git a/src/client.rs b/src/client.rs index 9aed57fe..53b3ecf7 100644 --- a/src/client.rs +++ b/src/client.rs @@ -98,14 +98,10 @@ impl BlockchainClient { #[cfg(feature = "cbf")] Self::KyotoClient { client } => { let txid = tx.compute_txid(); - let wtxid = client - .requester - .submit_package(tx) - .await - .map_err(|_| { - tracing::warn!("Broadcast was unsuccessful"); - Error::Generic("Transaction broadcast timed out after 30 seconds".into()) - })?; + let wtxid = client.requester.submit_package(tx).await.map_err(|_| { + tracing::warn!("Broadcast was unsuccessful"); + Error::Generic("Transaction broadcast timed out after 30 seconds".into()) + })?; tracing::info!("Successfully broadcast WTXID: {wtxid}"); Ok(txid) } diff --git a/src/commands.rs b/src/commands.rs index 7559d5d3..5ed726ed 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -21,8 +21,9 @@ use crate::handlers::{ key::{DeriveKeyCommand, GenerateKeyCommand, RestoreKeyCommand}, offline::{ BalanceCommand, BumpFeeCommand, CombinePsbtCommand, CreateTxCommand, ExtractPsbtCommand, - FinalizePsbtCommand, NewAddressCommand, PoliciesCommand, PublicDescriptorCommand, - SignCommand, TransactionsCommand, UnspentCommand, UnusedAddressCommand, + FinalizePsbtCommand, LockUtxoCommand, LockedUtxosCommand, NewAddressCommand, + PoliciesCommand, PublicDescriptorCommand, SignCommand, TransactionsCommand, + UnlockUtxoCommand, UnspentCommand, UnusedAddressCommand, }, }; @@ -359,6 +360,12 @@ pub enum OfflineWalletSubCommand { /// Verify a BIP322 signature #[cfg(feature = "bip322")] VerifyMessage(VerifyMessageCommand), + /// Lock UTXO(s) so they're excluded from coin selection. + LockUtxo(LockUtxoCommand), + /// Unlock previously locked UTXO(s). + UnlockUtxo(UnlockUtxoCommand), + /// List currently locked UTXOs. + LockedUtxos(LockedUtxosCommand), } /// Wallet subcommands that needs a blockchain backend. diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index 5af69b59..e0ee2847 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -81,6 +81,11 @@ impl OfflineWalletSubCommand { Self::VerifyMessage(verify_message_command) => verify_message_command .execute(ctx)? .write_out(std::io::stdout()), + Self::LockUtxo(lock_utxo) => lock_utxo.execute(ctx)?.write_out(std::io::stdout()), + Self::UnlockUtxo(unlock_utxo) => unlock_utxo.execute(ctx)?.write_out(std::io::stdout()), + Self::LockedUtxos(locked_utxos) => { + locked_utxos.execute(ctx)?.write_out(std::io::stdout()) + } } } } @@ -119,9 +124,13 @@ impl AppCommand>> for UnspentCommand { fn execute(&self, ctx: &mut AppContext>) -> Result { let wallet = &mut ctx.state.wallet; - let utxos = wallet - .list_unspent() - .map(|utxo| UnspentDetails::from_local_output(&utxo, ctx.network)) + let outputs: Vec<_> = wallet.list_unspent().collect(); + let utxos = outputs + .into_iter() + .map(|utxo| { + let is_locked = wallet.is_outpoint_locked(utxo.outpoint); + UnspentDetails::from_local_output(&utxo, ctx.network, is_locked) + }) .collect(); Ok(ListResult::new(utxos)) @@ -841,3 +850,65 @@ impl AppCommand>> for VerifyMessageCommand { }) } } + +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct LockUtxoCommand { + /// Outpoint(s) to lock, format TXID:VOUT. + #[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)] + pub utxos: Vec, +} + +impl AppCommand>> for LockUtxoCommand { + type Output = ListResult; + fn execute(&self, ctx: &mut AppContext>) -> Result { + let wallet = &mut ctx.state.wallet; + for out_point in &self.utxos { + if wallet.get_utxo(*out_point).is_none() { + eprintln!("warning: {out_point} is not a known wallet UTXO; locking anyway"); + } + wallet.lock_outpoint(*out_point); + } + let locked = wallet + .list_locked_outpoints() + .map(|o| o.to_string()) + .collect(); + Ok(ListResult::new(locked)) + } +} + +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct UnlockUtxoCommand { + /// Outpoint(s) to unlock, format TXID:VOUT. + #[arg(env = "TXID:VOUT", long = "utxo", required = true, value_parser = parse_outpoint)] + pub utxos: Vec, +} + +impl AppCommand>> for UnlockUtxoCommand { + type Output = ListResult; + fn execute(&self, ctx: &mut AppContext>) -> Result { + let wallet = &mut ctx.state.wallet; + for out_point in &self.utxos { + wallet.unlock_outpoint(*out_point); + } + let locked = wallet + .list_locked_outpoints() + .map(|o| o.to_string()) + .collect(); + Ok(ListResult::new(locked)) + } +} + +#[derive(Parser, Debug, Clone, PartialEq)] +pub struct LockedUtxosCommand {} + +impl AppCommand>> for LockedUtxosCommand { + type Output = ListResult; + fn execute(&self, ctx: &mut AppContext>) -> Result { + let wallet = &mut ctx.state.wallet; + let locked = wallet + .list_locked_outpoints() + .map(|o| o.to_string()) + .collect(); + Ok(ListResult::new(locked)) + } +} diff --git a/src/utils/common.rs b/src/utils/common.rs index 051ebe03..0fa3dca0 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -220,7 +220,10 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { | OfflineWalletSubCommand::BumpFee(_) | OfflineWalletSubCommand::NewAddress(_) | OfflineWalletSubCommand::UnusedAddress(_) - | OfflineWalletSubCommand::CreateTx(_) => true, + | OfflineWalletSubCommand::CreateTx(_) + | OfflineWalletSubCommand::LockUtxo(_) + | OfflineWalletSubCommand::UnlockUtxo(_) + | OfflineWalletSubCommand::LockedUtxos(_) => true, OfflineWalletSubCommand::Policies(_) | OfflineWalletSubCommand::PublicDescriptor(_) diff --git a/src/utils/types.rs b/src/utils/types.rs index 99bdcbd9..e25d3566 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -55,10 +55,11 @@ pub struct UnspentDetails { pub is_spent: bool, pub derivation_index: u32, pub chain_position: serde_json::Value, + pub is_locked: bool, } impl UnspentDetails { - pub fn from_local_output(utxo: &LocalOutput, _network: Network) -> Self { + pub fn from_local_output(utxo: &LocalOutput, _network: Network, is_locked: bool) -> Self { let outpoint_str = utxo.outpoint.to_string(); Self { @@ -68,6 +69,7 @@ impl UnspentDetails { is_spent: utxo.is_spent, derivation_index: utxo.derivation_index, chain_position: serde_json::to_value(utxo.chain_position).unwrap_or(json!({})), + is_locked, } } } From d3dde305b5b20f68c32dd72302f82cce0dc73097 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 9 Jul 2026 09:04:54 +0100 Subject: [PATCH 10/11] fix(bip322): disable bip322 feature -Temporary disable bip322 feature --- Cargo.lock | 31 +----- Cargo.toml | 4 +- src/commands.rs | 12 +-- src/error.rs | 7 +- src/handlers/dns/mod.rs | 2 +- src/handlers/offline.rs | 30 +++--- src/utils/common.rs | 42 ++++---- src/utils/types.rs | 24 ++--- tests/integration/offline.rs | 200 +++++++++++++++++----------------- tests/integration/online.rs | 202 +++++++++++++++++------------------ 10 files changed, 265 insertions(+), 289 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5557be10..66273d71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,7 +247,6 @@ name = "bdk-cli" version = "3.0.0" dependencies = [ "assert_cmd", - "bdk_bip322", "bdk_bitcoind_rpc", "bdk_electrum", "bdk_esplora", @@ -255,7 +254,7 @@ dependencies = [ "bdk_redb", "bdk_sp", "bdk_testenv", - "bdk_wallet 3.1.0", + "bdk_wallet", "bitcoin-payment-instructions", "clap", "clap_complete", @@ -279,16 +278,6 @@ dependencies = [ "url", ] -[[package]] -name = "bdk_bip322" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262ee5f0c1056400b354391cabc541cb732c10488679de6389641e37f7e6c698" -dependencies = [ - "bdk_wallet 2.4.0", - "bitcoin", -] - [[package]] name = "bdk_bitcoind_rpc" version = "0.22.0" @@ -352,7 +341,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346c1d502ee4613cf2201db9c3b93a2317772d8eaa372114f34bf5f374be94fd" dependencies = [ - "bdk_wallet 3.1.0", + "bdk_wallet", "bip157", ] @@ -363,7 +352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69ac8b88c9594c615fcb5b763e7afa16937e94f7a7f5ca334bf59a7fee69fea1" dependencies = [ "bdk_chain", - "bdk_wallet 3.1.0", + "bdk_wallet", "ciborium", "redb", "thiserror 2.0.18", @@ -387,20 +376,6 @@ dependencies = [ "electrsd", ] -[[package]] -name = "bdk_wallet" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c99821af39c7df004bd411ece2ef22d9fba5a631b4489f3d9a0ac0d19637e2d0" -dependencies = [ - "bdk_chain", - "bitcoin", - "miniscript", - "rand_core 0.6.4", - "serde", - "serde_json", -] - [[package]] name = "bdk_wallet" version = "3.1.0" diff --git a/Cargo.toml b/Cargo.toml index 3c553a86..3411ad43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ shlex = { version = "1.3.0", optional = true } payjoin = { version = "0.25.0", features = ["v1", "v2", "io", "_test-utils"], optional = true} reqwest = { version = "0.13.2", default-features = false, features = ["rustls"], optional = true } url = { version = "2.5.8", optional = true } -bdk_bip322 = { version = "0.1.0", optional = true } +# bdk_bip322 = { version = "0.1.0", optional = true } bitcoin-payment-instructions = { version = "0.7.0", optional = true} [features] @@ -61,7 +61,7 @@ dns_payment = ["bitcoin-payment-instructions"] # Internal features _payjoin-dependencies = ["payjoin", "reqwest", "url", "sqlite"] -bip322 = ["bdk_bip322"] +# bip322 = ["bdk_bip322"] # Use this to consensus verify transactions at sync time verify = [] diff --git a/src/commands.rs b/src/commands.rs index 613a4f6b..28911627 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -13,8 +13,8 @@ //! All subcommands are defined in the below enums. #![allow(clippy::large_enum_variant)] -#[cfg(feature = "bip322")] -use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; +// #[cfg(feature = "bip322")] +// use crate::handlers::offline::{SignMessageCommand, VerifyMessageCommand}; use crate::handlers::{ config::{ListWalletsCommand, SaveConfigCommand}, descriptor::DescriptorCommand, @@ -362,11 +362,11 @@ pub enum OfflineWalletSubCommand { /// Combines multiple PSBTs into one. CombinePsbt(CombinePsbtCommand), /// Sign a message using BIP322 - #[cfg(feature = "bip322")] - SignMessage(SignMessageCommand), + // #[cfg(feature = "bip322")] + // SignMessage(SignMessageCommand), /// Verify a BIP322 signature - #[cfg(feature = "bip322")] - VerifyMessage(VerifyMessageCommand), + // #[cfg(feature = "bip322")] + // VerifyMessage(VerifyMessageCommand), /// Lock UTXO(s) so they're excluded from coin selection. LockUtxo(LockUtxoCommand), /// Unlock previously locked UTXO(s). diff --git a/src/error.rs b/src/error.rs index 7f7eab04..96af63ad 100644 --- a/src/error.rs +++ b/src/error.rs @@ -155,10 +155,9 @@ pub enum BDKCliError { #[cfg(feature = "payjoin")] #[error("Payjoin database error: {0}")] PayjoinDb(#[from] crate::handlers::payjoin::db::Error), - - #[cfg(feature = "bip322")] - #[error("BIP-322 error: {0}")] - Bip322Error(#[from] bdk_bip322::error::Error), + // #[cfg(feature = "bip322")] + // #[error("BIP-322 error: {0}")] + // Bip322Error(#[from] bdk_bip322::error::Error), } impl From for BDKCliError { diff --git a/src/handlers/dns/mod.rs b/src/handlers/dns/mod.rs index 6e682bc6..628bfbdb 100644 --- a/src/handlers/dns/mod.rs +++ b/src/handlers/dns/mod.rs @@ -127,7 +127,7 @@ impl AsyncAppCommand>> for CreateDnsTxCommand { tx_builder.set_exact_sequence(Sequence::MAX); } if self.offline_signer { - tx_builder.include_output_redeem_witness_script(); + tx_builder.add_global_xpubs(); } if let Some(fee_rate) = self.fee_rate && let Some(fee_rate) = FeeRate::from_sat_per_vb(fee_rate as u64) diff --git a/src/handlers/offline.rs b/src/handlers/offline.rs index dde08deb..048dcfcb 100644 --- a/src/handlers/offline.rs +++ b/src/handlers/offline.rs @@ -28,12 +28,12 @@ use { bdk_wallet::keys::{DescriptorPublicKey, DescriptorSecretKey, SinglePubKey}, std::collections::HashMap, }; -#[cfg(feature = "bip322")] -use { - crate::utils::parse_signature_format, - crate::utils::types::MessageResult, - bdk_bip322::{BIP322, MessageProof}, -}; +// #[cfg(feature = "bip322")] +// use { +// crate::utils::parse_signature_format, +// crate::utils::types::MessageResult, +// bdk_bip322::{BIP322, MessageProof}, +// }; impl OfflineWalletSubCommand { pub fn execute(&self, ctx: &mut AppContext>) -> Result<(), Error> { @@ -73,14 +73,14 @@ impl OfflineWalletSubCommand { Self::CombinePsbt(combine_psbt_command) => combine_psbt_command .execute(ctx)? .write_out(std::io::stdout()), - #[cfg(feature = "bip322")] - Self::SignMessage(sign_message_command) => sign_message_command - .execute(ctx)? - .write_out(std::io::stdout()), - #[cfg(feature = "bip322")] - Self::VerifyMessage(verify_message_command) => verify_message_command - .execute(ctx)? - .write_out(std::io::stdout()), + // #[cfg(feature = "bip322")] + // Self::SignMessage(sign_message_command) => sign_message_command + // .execute(ctx)? + // .write_out(std::io::stdout()), + // #[cfg(feature = "bip322")] + // Self::VerifyMessage(verify_message_command) => verify_message_command + // .execute(ctx)? + // .write_out(std::io::stdout()), Self::LockUtxo(lock_utxo) => lock_utxo.execute(ctx)?.write_out(std::io::stdout()), Self::UnlockUtxo(unlock_utxo) => unlock_utxo.execute(ctx)?.write_out(std::io::stdout()), Self::LockedUtxos(locked_utxos) => { @@ -773,6 +773,7 @@ impl AppCommand>> for CombinePsbtCommand { } } +/** #[cfg(feature = "bip322")] #[derive(Debug, Parser, Clone, PartialEq)] pub struct SignMessageCommand { @@ -860,6 +861,7 @@ impl AppCommand>> for VerifyMessageCommand { }) } } +**/ #[derive(Parser, Debug, Clone, PartialEq)] pub struct LockUtxoCommand { diff --git a/src/utils/common.rs b/src/utils/common.rs index 25fa4393..0fdc2aa2 100644 --- a/src/utils/common.rs +++ b/src/utils/common.rs @@ -1,6 +1,6 @@ use crate::{commands::WalletOpts, config::WalletConfig, error::BDKCliError as Error}; -#[cfg(feature = "bip322")] -use bdk_bip322::SignatureFormat; +// #[cfg(feature = "bip322")] +// use bdk_bip322::SignatureFormat; #[cfg(feature = "cbf")] use bdk_kyoto::{Info, Receiver, UnboundedReceiver, Warning}; #[cfg(feature = "silent-payments")] @@ -12,7 +12,7 @@ use bdk_wallet::bitcoin::{Address, Network, OutPoint, ScriptBuf}; feature = "cbf", feature = "rpc" ))] -use bdk_wallet::{bitcoin::Psbt, event::WalletEvent}; +use bdk_wallet::{WalletEvent, bitcoin::Psbt}; use crate::commands::OfflineWalletSubCommand; use std::{ @@ -197,20 +197,20 @@ pub(crate) fn parse_sp_code_value_pairs(s: &str) -> Result<(SilentPaymentCode, u Ok((key, value)) } -/// Function to parse the signature format from a string -#[cfg(feature = "bip322")] -pub(crate) fn parse_signature_format(format_str: &str) -> Result { - match format_str.to_lowercase().as_str() { - "legacy" => Ok(SignatureFormat::Legacy), - "simple" => Ok(SignatureFormat::Simple), - "full" => Ok(SignatureFormat::Full), - "fullproofoffunds" => Ok(SignatureFormat::FullProofOfFunds), - _ => Err(Error::Generic( - "Invalid signature format. Use 'legacy', 'simple', 'full', or 'fullproofoffunds'" - .to_string(), - )), - } -} +// /// Function to parse the signature format from a string +// #[cfg(feature = "bip322")] +// pub(crate) fn parse_signature_format(format_str: &str) -> Result { +// match format_str.to_lowercase().as_str() { +// "legacy" => Ok(SignatureFormat::Legacy), +// "simple" => Ok(SignatureFormat::Simple), +// "full" => Ok(SignatureFormat::Full), +// "fullproofoffunds" => Ok(SignatureFormat::FullProofOfFunds), +// _ => Err(Error::Generic( +// "Invalid signature format. Use 'legacy', 'simple', 'full', or 'fullproofoffunds'" +// .to_string(), +// )), +// } +// } pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { match command { @@ -232,11 +232,11 @@ pub fn command_requires_db(command: &OfflineWalletSubCommand) -> bool { | OfflineWalletSubCommand::FinalizePsbt(_) | OfflineWalletSubCommand::CombinePsbt(_) => false, - #[cfg(feature = "bip322")] - OfflineWalletSubCommand::SignMessage(_) => true, + // #[cfg(feature = "bip322")] + // OfflineWalletSubCommand::SignMessage(_) => true, - #[cfg(feature = "bip322")] - OfflineWalletSubCommand::VerifyMessage(_) => true, + // #[cfg(feature = "bip322")] + // OfflineWalletSubCommand::VerifyMessage(_) => true, #[cfg(feature = "silent-payments")] OfflineWalletSubCommand::CreateSpTx(_) => true, #[cfg(feature = "dns_payment")] diff --git a/src/utils/types.rs b/src/utils/types.rs index 8a10d03f..51855b0d 100644 --- a/src/utils/types.rs +++ b/src/utils/types.rs @@ -110,18 +110,18 @@ pub struct KeychainPair { pub internal: T, } -#[cfg(feature = "bip322")] -#[derive(Serialize, Debug, Default)] -pub struct MessageResult { - #[serde(skip_serializing_if = "Option::is_none")] - pub proof: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub valid: Option, - - #[serde(skip_serializing_if = "Option::is_none")] - pub proven_amount: Option, -} +// #[cfg(feature = "bip322")] +// #[derive(Serialize, Debug, Default)] +// pub struct MessageResult { +// #[serde(skip_serializing_if = "Option::is_none")] +// pub proof: Option, + +// #[serde(skip_serializing_if = "Option::is_none")] +// pub valid: Option, + +// #[serde(skip_serializing_if = "Option::is_none")] +// pub proven_amount: Option, +// } #[derive(Serialize, Debug)] pub struct StatusResult { diff --git a/tests/integration/offline.rs b/tests/integration/offline.rs index f19f623c..24a57dab 100644 --- a/tests/integration/offline.rs +++ b/tests/integration/offline.rs @@ -150,117 +150,117 @@ mod test_offline { .failure() .stderr(predicate::str::contains("Invalid")); } - - #[cfg(feature = "bip322")] - #[test] - fn test_sign_message_and_verify_message() { - let (cli, mut cmd_init) = setup_wallet_config(); - cmd_init.assert().success(); - - let message = "bdk-cli integration test"; - - // Reveal exactly one address and reuse it for both sign and verify. - let addr_output = cli - .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) - .output() - .expect("Failed to generate address"); - assert!( - addr_output.status.success(), - "new_address failed: {}", - String::from_utf8_lossy(&addr_output.stderr) - ); - let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); - let address = addr_json["address"] - .as_str() - .expect("missing address") - .to_string(); - println!("DEBUG signing address: {address}"); - - // Sign (default signature_type = "simple"). - let sign_output = cli - .wallet_cmd(&[ + /** + #[cfg(feature = "bip322")] + #[test] + fn test_sign_message_and_verify_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let message = "bdk-cli integration test"; + + // Reveal exactly one address and reuse it for both sign and verify. + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + assert!( + addr_output.status.success(), + "new_address failed: {}", + String::from_utf8_lossy(&addr_output.stderr) + ); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"] + .as_str() + .expect("missing address") + .to_string(); + println!("DEBUG signing address: {address}"); + + // Sign (default signature_type = "simple"). + let sign_output = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "sign_message", + "--message", + message, + "--address", + &address, + ]) + .output() + .expect("Failed to sign message"); + assert!( + sign_output.status.success(), + "sign_message failed: {}", + String::from_utf8_lossy(&sign_output.stderr) + ); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().expect("missing proof"); + println!("DEBUG proof: {proof}"); + println!("DEBUG verifying same address: {address}"); + + // Verify with the identical address + message. + cli.wallet_cmd(&[ "--wallet", WALLET_NAME, - "sign_message", - "--message", - message, + "verify_message", "--address", &address, + "--message", + message, + "--proof", + proof, ]) - .output() - .expect("Failed to sign message"); - assert!( - sign_output.status.success(), - "sign_message failed: {}", - String::from_utf8_lossy(&sign_output.stderr) - ); - let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); - let proof = sign_json["proof"].as_str().expect("missing proof"); - println!("DEBUG proof: {proof}"); - println!("DEBUG verifying same address: {address}"); - - // Verify with the identical address + message. - cli.wallet_cmd(&[ - "--wallet", - WALLET_NAME, - "verify_message", - "--address", - &address, - "--message", - message, - "--proof", - proof, - ]) - .assert() - .success() - .stdout(predicate::str::contains("\"valid\": true")); - } - - #[cfg(feature = "bip322")] - #[test] - fn test_verify_message_rejects_tampered_message() { - let (cli, mut cmd_init) = setup_wallet_config(); - cmd_init.assert().success(); - - let addr_output = cli - .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) - .output() - .expect("Failed to generate address"); - let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); - let address = addr_json["address"].as_str().unwrap(); - - let sign_output = cli - .wallet_cmd(&[ + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": true")); + } + + #[cfg(feature = "bip322")] + #[test] + fn test_verify_message_rejects_tampered_message() { + let (cli, mut cmd_init) = setup_wallet_config(); + cmd_init.assert().success(); + + let addr_output = cli + .wallet_cmd(&["--wallet", WALLET_NAME, "new_address"]) + .output() + .expect("Failed to generate address"); + let addr_json: Value = serde_json::from_slice(&addr_output.stdout).unwrap(); + let address = addr_json["address"].as_str().unwrap(); + + let sign_output = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "sign_message", + "--message", + "original message", + "--address", + address, + ]) + .output() + .expect("Failed to sign message"); + let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); + let proof = sign_json["proof"].as_str().unwrap(); + + // A tampered message should fail + cli.wallet_cmd(&[ "--wallet", WALLET_NAME, - "sign_message", + "verify_message", + "--proof", + proof, "--message", - "original message", + "tampered message", "--address", address, ]) - .output() - .expect("Failed to sign message"); - let sign_json: Value = serde_json::from_slice(&sign_output.stdout).unwrap(); - let proof = sign_json["proof"].as_str().unwrap(); - - // A tampered message should fail - cli.wallet_cmd(&[ - "--wallet", - WALLET_NAME, - "verify_message", - "--proof", - proof, - "--message", - "tampered message", - "--address", - address, - ]) - .assert() - .success() - .stdout(predicate::str::contains("\"valid\": false")); - } - + .assert() + .success() + .stdout(predicate::str::contains("\"valid\": false")); + } + **/ #[test] fn test_create_tx_send_all_rejects_multiple_recipients() { let (cli, mut cmd_init) = setup_wallet_config(); diff --git a/tests/integration/online.rs b/tests/integration/online.rs index 0ba166d5..a0c61f1a 100644 --- a/tests/integration/online.rs +++ b/tests/integration/online.rs @@ -813,111 +813,111 @@ mod test_online { ); } } + /** + #[cfg(feature = "bip322")] + #[test] + fn test_verify_message_proof_of_funds_uses_persisted_utxos() { + let env = TestEnv::new().expect("Failed to start bdk_testenv"); + let server_url = env.electrsd.electrum_url.as_str(); + let temp_dir = TempDir::new().unwrap(); + let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); - #[cfg(feature = "bip322")] - #[test] - fn test_verify_message_proof_of_funds_uses_persisted_utxos() { - let env = TestEnv::new().expect("Failed to start bdk_testenv"); - let server_url = env.electrsd.electrum_url.as_str(); - let temp_dir = TempDir::new().unwrap(); - let cli = BdkCli::new("regtest", Some(temp_dir.path().to_path_buf())); - - // A wpkh wallet - let desc = cli.cmd("descriptor", &["--type", "wpkh"]).output().unwrap(); - let desc_val: Value = serde_json::from_slice(&desc.stdout).unwrap(); - let ext_desc = desc_val["private_descriptors"]["external"] - .as_str() - .unwrap(); - let int_desc = desc_val["private_descriptors"]["internal"] - .as_str() - .unwrap(); - cli.build_base_cmd() - .arg("wallet") - .arg("--wallet") - .arg(WALLET_NAME) - .arg("config") - .arg("--ext-descriptor") - .arg(ext_desc) - .arg("--int-descriptor") - .arg(int_desc) - .arg("--client-type") - .arg("electrum") - .arg("--database-type") - .arg("sqlite") - .arg("--url") - .arg(server_url) - .assert() - .success(); - - // Fund a single small UTXO - let address = cli_new_address(&cli); - let node_addr = env - .rpc_client() - .get_new_address(None, None) - .unwrap() - .assume_checked(); - env.mine_blocks(101, Some(node_addr)).unwrap(); - env.wait_until_electrum_sees_block(Duration::from_secs(10)) - .unwrap(); - let txid = env.send(&address, Amount::from_sat(5_000)).unwrap(); - env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) - .unwrap(); - env.mine_blocks(3, None).unwrap(); - env.wait_until_electrum_sees_block(Duration::from_secs(10)) - .unwrap(); - cli_full_scan(&cli); - - // The funded outpoint to prove control of. - let unspent = run_wallet_json(&cli, &["unspent"]); - let outpoint = unspent["items"][0]["outpoint"] - .as_str() - .expect("funded wallet should have one UTXO") - .to_string(); - let addr = address.to_string(); + // A wpkh wallet + let desc = cli.cmd("descriptor", &["--type", "wpkh"]).output().unwrap(); + let desc_val: Value = serde_json::from_slice(&desc.stdout).unwrap(); + let ext_desc = desc_val["private_descriptors"]["external"] + .as_str() + .unwrap(); + let int_desc = desc_val["private_descriptors"]["internal"] + .as_str() + .unwrap(); + cli.build_base_cmd() + .arg("wallet") + .arg("--wallet") + .arg(WALLET_NAME) + .arg("config") + .arg("--ext-descriptor") + .arg(ext_desc) + .arg("--int-descriptor") + .arg(int_desc) + .arg("--client-type") + .arg("electrum") + .arg("--database-type") + .arg("sqlite") + .arg("--url") + .arg(server_url) + .assert() + .success(); - // Produce a proof-of-funds over that UTXO. - let proof = run_wallet_json( - &cli, - &[ - "sign_message", - "--message", - "proof-of-funds", - "--address", - &addr, - "--signature-type", - "fullproofoffunds", - "--utxos", - &outpoint, - ], - )["proof"] - .as_str() - .expect("sign_message should return a proof") - .to_string(); + // Fund a single small UTXO + let address = cli_new_address(&cli); + let node_addr = env + .rpc_client() + .get_new_address(None, None) + .unwrap() + .assume_checked(); + env.mine_blocks(101, Some(node_addr)).unwrap(); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .unwrap(); + let txid = env.send(&address, Amount::from_sat(5_000)).unwrap(); + env.wait_until_electrum_sees_txid(txid, Duration::from_secs(10)) + .unwrap(); + env.mine_blocks(3, None).unwrap(); + env.wait_until_electrum_sees_block(Duration::from_secs(10)) + .unwrap(); + cli_full_scan(&cli); - let result = run_wallet_json( - &cli, - &[ - "verify_message", - "--proof", - &proof, - "--message", - "proof-of-funds", - "--address", - &addr, - ], - ); - assert_eq!( - result["valid"].as_bool(), - Some(true), - "proof-of-funds should verify against the persisted wallet: {result}" - ); - assert_eq!( - result["proven_amount"].as_u64(), - Some(5_000), - "proven_amount should equal the funded UTXO value: {result}" - ); - } + // The funded outpoint to prove control of. + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .expect("funded wallet should have one UTXO") + .to_string(); + let addr = address.to_string(); + + // Produce a proof-of-funds over that UTXO. + let proof = run_wallet_json( + &cli, + &[ + "sign_message", + "--message", + "proof-of-funds", + "--address", + &addr, + "--signature-type", + "fullproofoffunds", + "--utxos", + &outpoint, + ], + )["proof"] + .as_str() + .expect("sign_message should return a proof") + .to_string(); + let result = run_wallet_json( + &cli, + &[ + "verify_message", + "--proof", + &proof, + "--message", + "proof-of-funds", + "--address", + &addr, + ], + ); + assert_eq!( + result["valid"].as_bool(), + Some(true), + "proof-of-funds should verify against the persisted wallet: {result}" + ); + assert_eq!( + result["proven_amount"].as_u64(), + Some(5_000), + "proven_amount should equal the funded UTXO value: {result}" + ); + } + */ // `create_dns_tx` with a plain `--to` recipient #[cfg(feature = "dns_payment")] #[test] From aba6c6605bd2943dc6c831532d1a581e4bc1ace1 Mon Sep 17 00:00:00 2001 From: Vihiga Tyonum Date: Thu, 9 Jul 2026 09:22:20 +0100 Subject: [PATCH 11/11] feat(utxo-locking): Add tests for utxo locking -add test for locking, unlocking and listing locked utxos --- tests/integration/offline.rs | 4 +- tests/integration/online.rs | 86 ++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/tests/integration/offline.rs b/tests/integration/offline.rs index 24a57dab..1c0fed28 100644 --- a/tests/integration/offline.rs +++ b/tests/integration/offline.rs @@ -152,7 +152,7 @@ mod test_offline { } /** #[cfg(feature = "bip322")] - #[test] + // #[test] fn test_sign_message_and_verify_message() { let (cli, mut cmd_init) = setup_wallet_config(); cmd_init.assert().success(); @@ -217,7 +217,7 @@ mod test_offline { } #[cfg(feature = "bip322")] - #[test] + // #[test] fn test_verify_message_rejects_tampered_message() { let (cli, mut cmd_init) = setup_wallet_config(); cmd_init.assert().success(); diff --git a/tests/integration/online.rs b/tests/integration/online.rs index a0c61f1a..1a8f2eef 100644 --- a/tests/integration/online.rs +++ b/tests/integration/online.rs @@ -965,6 +965,92 @@ mod test_online { String::from_utf8_lossy(&out.stderr) ); } + + // lock → list → unlock cycle. + #[test] + fn test_lock_unlock_and_list_utxos() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .expect("one utxo") + .to_string(); + + let locked = run_wallet_json(&cli, &["lock_utxo", "--utxo", &outpoint]); + assert_eq!(locked["count"].as_u64(), Some(1), "lock output: {locked}"); + + // separate process — must still be locked (persistence) + let listed = run_wallet_json(&cli, &["locked_utxos"]); + assert_eq!( + listed["count"].as_u64(), + Some(1), + "lock must persist across processes: {listed}" + ); + assert!( + listed["items"] + .as_array() + .unwrap() + .iter() + .any(|i| i.as_str() == Some(outpoint.as_str())), + "locked_utxos should list {outpoint}: {listed}" + ); + + let after = run_wallet_json(&cli, &["unlock_utxo", "--utxo", &outpoint]); + assert_eq!( + after["count"].as_u64(), + Some(0), + "unlock should clear it: {after}" + ); + let final_list = run_wallet_json(&cli, &["locked_utxos"]); + assert_eq!( + final_list["count"].as_u64(), + Some(0), + "empty after unlock: {final_list}" + ); + } + + // a locked UTXO is excluded from coin selection. + #[test] + fn test_locked_utxo_excluded_from_coin_selection() { + let (cli, mut cmd_init, env) = setup_online_wallet(); + cmd_init.assert().success(); + fund_and_sync_wallet(&cli, &env); + + let unspent = run_wallet_json(&cli, &["unspent"]); + let outpoint = unspent["items"][0]["outpoint"] + .as_str() + .unwrap() + .to_string(); + + run_wallet_json(&cli, &["lock_utxo", "--utxo", &outpoint]); + + // only UTXO is locked → create_tx must fail + let out = cli + .wallet_cmd(&[ + "--wallet", + WALLET_NAME, + "create_tx", + "--to", + &format!("{RECIPIENT}:20000"), + ]) + .output() + .unwrap(); + assert!( + !out.status.success(), + "create_tx must fail when the only UTXO is locked" + ); + + // unlock → create_tx succeeds + run_wallet_json(&cli, &["unlock_utxo", "--utxo", &outpoint]); + let psbt = run_wallet_json(&cli, &["create_tx", "--to", &format!("{RECIPIENT}:20000")]); + assert!( + psbt["psbt"].as_str().is_some(), + "create_tx should succeed after unlock: {psbt}" + ); + } } #[cfg(feature = "esplora")]