From: Vihiga Tyonum Date: Mon, 22 Jun 2026 21:18:00 +0000 (+0100) Subject: test(online): Add test transaction full cycle X-Git-Url: http://internal-gitweb-vhost/blockdata/script/encode/-script/display/struct.String.html?a=commitdiff_plain;h=702d2594afa804aa7985a32dc55559fff7b06da4;p=bdk-cli 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 --- diff --git a/Cargo.lock b/Cargo.lock index 82ef472..e0933ef 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.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", +] diff --git a/Cargo.toml b/Cargo.toml index 7ffc611..1f91dd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,3 +78,4 @@ claims = "0.8.0" 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 6faa880..1e06133 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 4dbd261..19be698 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 }