]> Untitled Git - bdk-cli/commitdiff
test(online): Add test transaction full cycle
authorVihiga Tyonum <withtvpeter@gmail.com>
Mon, 22 Jun 2026 21:18:00 +0000 (22:18 +0100)
committerVihiga Tyonum <withtvpeter@gmail.com>
Wed, 8 Jul 2026 21:49:31 +0000 (22:49 +0100)
- Add test for creating, signing, broadcasting tx
- Add test for combining psbt, fee bump and other
psbt related commands
- Add bip322 tests

Cargo.lock
Cargo.toml
tests/cli.rs
tests/common/mod.rs

index 82ef4721c260ff090b79b5647d245c4ff94dcea0..e0933eff83eb37910b8bdb1bd3fbf87548688d3a 100644 (file)
@@ -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.103"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
+
 [[package]]
 name = "arrayvec"
 version = "0.7.8"
@@ -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",
  "bitcoin-payment-instructions",
  "claims",
@@ -302,7 +332,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "00a9846105bf6e751adbb6946b000ff919ce24a15a941cbfe68485549b552a9c"
 dependencies = [
  "bdk_core",
- "electrum-client",
+ "electrum-client 0.25.0",
 ]
 
 [[package]]
@@ -348,6 +378,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.4.0"
@@ -603,6 +643,24 @@ 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.101",
+ "bitcoincore-rpc",
+ "flate2",
+ "log",
+ "minreq",
+ "tar",
+ "tempfile",
+ "which",
+ "zip",
+]
+
 [[package]]
 name = "bitflags"
 version = "1.3.2"
@@ -662,6 +720,26 @@ version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
 
+[[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.66"
@@ -902,6 +980,12 @@ dependencies = [
  "memchr",
 ]
 
+[[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.10.1"
@@ -945,6 +1029,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"
@@ -1033,6 +1132,12 @@ dependencies = [
  "thiserror 2.0.18",
 ]
 
+[[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"
@@ -1107,6 +1212,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.101",
+ "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.25.0"
@@ -1197,12 +1336,32 @@ version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
 
+[[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.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
 
+[[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"
@@ -1505,6 +1664,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.2"
@@ -1952,6 +2120,12 @@ dependencies = [
  "bitcoin",
 ]
 
+[[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.12.1"
@@ -1991,6 +2165,15 @@ version = "2.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
 
+[[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.7"
@@ -2002,6 +2185,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"
@@ -2044,6 +2237,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"
@@ -2059,6 +2266,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"
@@ -2158,6 +2371,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 = "0.25.0"
@@ -2178,6 +2402,18 @@ dependencies = [
  "web-time",
 ]
 
+[[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"
@@ -2196,6 +2432,12 @@ version = "0.2.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
 
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
 [[package]]
 name = "pkg-config"
 version = "0.3.33"
@@ -2269,6 +2511,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"
@@ -2628,6 +2876,19 @@ dependencies = [
  "semver",
 ]
 
+[[package]]
+name = "rustix"
+version = "0.38.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+dependencies = [
+ "bitflags 2.13.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.15",
+ "windows-sys 0.59.0",
+]
+
 [[package]]
 name = "rustix"
 version = "1.1.4"
@@ -2637,7 +2898,7 @@ dependencies = [
  "bitflags 2.13.0",
  "errno",
  "libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.12.1",
  "windows-sys 0.61.2",
 ]
 
@@ -2900,6 +3161,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"
@@ -2955,6 +3227,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 = "simd_cesu8"
 version = "1.1.1"
@@ -3048,6 +3326,17 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
 
+[[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.27.0"
@@ -3057,7 +3346,7 @@ dependencies = [
  "fastrand",
  "getrandom 0.4.3",
  "once_cell",
- "rustix",
+ "rustix 1.1.4",
  "windows-sys 0.61.2",
 ]
 
@@ -3125,6 +3414,25 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "time"
+version = "0.3.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50"
+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.3"
@@ -3592,6 +3900,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"
@@ -3638,6 +3958,15 @@ dependencies = [
  "windows-targets",
 ]
 
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets",
+]
+
 [[package]]
 name = "windows-sys"
 version = "0.61.2"
@@ -3729,6 +4058,16 @@ version = "0.6.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
 
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix 1.1.4",
+]
+
 [[package]]
 name = "yoke"
 version = "0.8.3"
@@ -3846,8 +4185,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.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[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",
+]
index 7ffc611f86a63b3e21d1b9c4c58d52f6f4845340..1f91dd4f7419bd1e2dd94447ef59e51f766c4bf9 100644 (file)
@@ -78,3 +78,4 @@ claims = "0.8.0"
 predicates = "3.0"
 tempfile = "3.8"
 assert_cmd = "2.2.2"
+bdk_testenv = "0.13.1"
index 6faa880160aa995e95eec506bd2c9d1656a32b2c..1e06133244a5efaad4850b4c4ee08834c0a08ad2 100644 (file)
@@ -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 <WALLET_NAME> <args>`, 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}"
+        );
+    }
 }
index 4dbd261f2505c7b17f094f82206c82ba46b12f90..19be69891472fdba02358024a1e06cf51f1f3e36 100644 (file)
@@ -22,6 +22,7 @@ pub struct BdkCli {
     pub verbosity: bool,
     pub recv_desc: Option<String>,
     pub change_desc: Option<String>,
+    pub server_url: Option<String>,
 }
 
 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
     }