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/-parse.html?a=commitdiff_plain;h=dfeb580555bbf5609689fca676dc6886fc13bca0;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 ba29441..40ec3dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aead" version = "0.4.3" @@ -34,6 +40,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "aes-gcm" version = "0.9.2" @@ -41,7 +58,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc3be92e19a7ef47457b8e6f90707e12b6ac5d20c6f3866584fa3be0787d839f" dependencies = [ "aead 0.4.3", - "aes", + "aes 0.7.5", "cipher 0.3.0", "ctr", "ghash", @@ -119,6 +136,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + [[package]] name = "arrayvec" version = "0.7.6" @@ -213,6 +236,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bdk-cli" version = "3.0.0" @@ -225,6 +254,7 @@ dependencies = [ "bdk_kyoto", "bdk_redb", "bdk_sp", + "bdk_testenv", "bdk_wallet", "clap", "clap_complete", @@ -299,7 +329,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b59a3f7fbe678874fa34354097644a171276e02a49934c13b3d61c54610ddf39" dependencies = [ "bdk_core", - "electrum-client", + "electrum-client 0.24.1", ] [[package]] @@ -345,6 +375,16 @@ dependencies = [ "bitcoin", ] +[[package]] +name = "bdk_testenv" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "543dc273dab3b9ec329772bcb15741a948cc3510deae2a30af3a116f03505fee" +dependencies = [ + "bdk_chain", + "electrsd", +] + [[package]] name = "bdk_wallet" version = "2.1.0" @@ -576,6 +616,30 @@ dependencies = [ "serde_json", ] +[[package]] +name = "bitcoind" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6620b7c942dbe28cc49c21d95e792feb9ffd95a093205e7875ccfa69c2925" +dependencies = [ + "anyhow", + "bitcoin_hashes 0.14.1", + "bitcoincore-rpc", + "flate2", + "log", + "minreq", + "tar", + "tempfile", + "which", + "zip", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.10.0" @@ -629,6 +693,26 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "cc" version = "1.2.52" @@ -842,6 +926,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.4" @@ -876,6 +966,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crunchy" version = "0.2.4" @@ -933,6 +1038,12 @@ dependencies = [ "cipher 0.3.0", ] +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "difflib" version = "0.4.0" @@ -997,6 +1108,40 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "electrsd" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c3c57645202a05a47206ed81cc179cf32bf4c3ca5a9e60c06c49d0222d5844" +dependencies = [ + "bitcoin_hashes 0.14.1", + "bitcoind", + "electrum-client 0.20.0", + "log", + "minreq", + "nix", + "which", + "zip", +] + +[[package]] +name = "electrum-client" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c7b1f8783238bb18e6e137875b0a66f3dffe6c7ea84066e05d033cf180b150f" +dependencies = [ + "bitcoin", + "log", + "serde", + "serde_json", +] + [[package]] name = "electrum-client" version = "0.24.1" @@ -1086,12 +1231,32 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + [[package]] name = "find-msvc-tools" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "float-cmp" version = "0.10.0" @@ -1377,6 +1542,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "http" version = "1.4.0" @@ -1716,7 +1890,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" dependencies = [ - "bitflags", + "bitflags 2.10.0", "libc", ] @@ -1731,6 +1905,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1770,6 +1950,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + [[package]] name = "miniscript" version = "12.3.5" @@ -1781,6 +1970,16 @@ dependencies = [ "serde", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "minreq" version = "2.14.1" @@ -1823,6 +2022,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", + "pin-utils", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1838,6 +2051,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-traits" version = "0.2.19" @@ -1871,7 +2090,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags", + "bitflags 2.10.0", "cfg-if", "foreign-types", "libc", @@ -1938,6 +2157,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "payjoin" version = "1.0.0-rc.1" @@ -1957,6 +2187,18 @@ dependencies = [ "url", ] +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", + "hmac 0.12.1", + "password-hash", + "sha2 0.10.9", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -2045,6 +2287,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2237,7 +2485,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.10.0", ] [[package]] @@ -2370,7 +2618,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags", + "bitflags 2.10.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -2384,16 +2632,29 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + [[package]] name = "rustix" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ - "bitflags", + "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] @@ -2521,7 +2782,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", + "bitflags 2.10.0", "core-foundation", "core-foundation-sys", "libc", @@ -2602,6 +2863,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2651,6 +2923,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + [[package]] name = "slab" version = "0.4.11" @@ -2722,6 +3000,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "tempfile" version = "3.24.0" @@ -2731,7 +3020,7 @@ dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", - "rustix", + "rustix 1.1.3", "windows-sys 0.61.2", ] @@ -2799,6 +3088,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + [[package]] name = "tinystr" version = "0.8.2" @@ -2941,7 +3249,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags", + "bitflags 2.10.0", "bytes", "futures-util", "http", @@ -3251,6 +3559,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "winapi" version = "0.3.9" @@ -3462,6 +3782,16 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.3", +] + [[package]] name = "yoke" version = "0.8.1" @@ -3579,8 +3909,57 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes 0.8.4", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac 0.12.1", + "pbkdf2", + "sha1", + "time", + "zstd", +] + [[package]] name = "zmij" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd8f3f50b848df28f887acb68e41201b5aea6bc8a8dacc00fb40635ff9a72fea" + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 3bad77f..2295d66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,4 +74,5 @@ silent-payments = ["dep:bdk_sp"] predicates = "3.0" tempfile = "3.8" assert_cmd = "2.2.2" +bdk_testenv = "0.13.1" diff --git a/tests/cli.rs b/tests/cli.rs index 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 }