# 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"
"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"
checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f"
dependencies = [
"aead 0.4.3",
- "aes",
+ "aes 0.7.5",
"cipher 0.3.0",
"ctr",
"ghash",
"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"
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"
"bdk_kyoto",
"bdk_redb",
"bdk_sp",
+ "bdk_testenv",
"bdk_wallet",
"bitcoin-payment-instructions",
"claims",
checksum = "00a9846105bf6e751adbb6946b000ff919ce24a15a941cbfe68485549b552a9c"
dependencies = [
"bdk_core",
- "electrum-client",
+ "electrum-client 0.25.0",
]
[[package]]
"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"
"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"
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"
"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"
"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"
"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"
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"
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"
"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"
"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"
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"
"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"
"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"
"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"
"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"
"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"
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"
"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"
"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"
"bitflags 2.13.0",
"errno",
"libc",
- "linux-raw-sys",
+ "linux-raw-sys 0.12.1",
"windows-sys 0.61.2",
]
"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"
"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"
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"
"fastrand",
"getrandom 0.4.3",
"once_cell",
- "rustix",
+ "rustix 1.1.4",
"windows-sys 0.61.2",
]
"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"
"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"
"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"
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"
"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",
+]
.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 ---
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";
.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}"
+ );
+ }
}