diff --git a/Cargo.lock b/Cargo.lock index 21e0ffd..b97511c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "arrayvec" version = "0.7.6" @@ -34,7 +46,8 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" name = "bdk-bitcoind-client" version = "0.1.0" dependencies = [ - "corepc-types", + "corepc-node", + "corepc-types 0.11.0", "jsonrpc", ] @@ -99,12 +112,38 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[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.47" @@ -115,6 +154,45 @@ dependencies = [ "shlex", ] +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "corepc-client" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7755b8b9219b23d166a5897b5e2d8266cbdd0de5861d351b96f6db26bcf415f3" +dependencies = [ + "bitcoin", + "corepc-types 0.10.1", + "jsonrpc", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "corepc-node" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768391062ec3812e223bb3031c5b2fcdd6e0e60b816157f21df82fd3e6617dc0" +dependencies = [ + "anyhow", + "bitcoin_hashes", + "corepc-client", + "flate2", + "log", + "minreq", + "serde_json", + "tar", + "tempfile", + "which", + "zip", +] + [[package]] name = "corepc-types" version = "0.10.1" @@ -126,12 +204,87 @@ dependencies = [ "serde_json", ] +[[package]] +name = "corepc-types" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6ea6101b2da248ff9c7e0ead02c6e0b8243db140d86c6190e1b043c306d97a" +dependencies = [ + "bitcoin", + "serde", + "serde_json", +] + +[[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 = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hex-conservative" version = "0.2.1" @@ -172,22 +325,70 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +[[package]] +name = "libredox" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05015102dad0f7d61691ca347e9d9d9006685a64aefb3d79eecf62665de2153d" dependencies = [ + "rustls", + "rustls-webpki", "serde", "serde_json", + "webpki-roots", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "proc-macro2" version = "1.0.103" @@ -206,12 +407,80 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -281,6 +550,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "socks" version = "0.3.4" @@ -303,12 +578,62 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -330,3 +655,188 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "crossbeam-utils", + "flate2", +] diff --git a/Cargo.toml b/Cargo.toml index 5b21b93..bde205e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,14 @@ authors = ["Bitcoin Dev Kit Developers"] readme = "README.md" [dependencies] -corepc-types = { version = "0.10.1", features = ["default"]} +corepc-types = { version = "0.11.0", features = ["default"]} jsonrpc = { version = "0.18.0", features = ["simple_http", "simple_tcp", "minreq_http", "simple_uds", "proxy"] } [features] +default = ["30_0"] 30_0 = [] +29_0 = [] +28_0 = [] + +[dev-dependencies] +corepc-node = { version = "0.10.1", features = ["download", "29_0"] } diff --git a/src/client.rs b/src/client.rs index 1427e06..5f0b70d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,19 +6,40 @@ use std::{ use crate::error::Error; use crate::jsonrpc::minreq_http::Builder; -use corepc_types::bitcoin::BlockHash; -use jsonrpc::{serde, serde_json, Transport}; +use corepc_types::{ + bitcoin::{ + block::Header, consensus::encode::deserialize_hex, Block, BlockHash, Transaction, Txid, + }, + model::{GetBlockCount, GetBlockFilter, GetRawMempool}, + v30, +}; +use jsonrpc::{ + serde, + serde_json::{self, json}, + Transport, +}; + +#[cfg(feature = "28_0")] +pub mod v28; -/// client authentication methods +/// Client authentication methods for the Bitcoin Core JSON-RPC server #[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] pub enum Auth { + /// No authentication (not recommended) None, + /// Username and password authentication (RPC user/pass) UserPass(String, String), + /// Authentication via a cookie file CookieFile(PathBuf), } impl Auth { - /// Convert into the arguments that jsonrpc::Client needs. + /// Converts `Auth` enum into the optional username and password strings + /// required by JSON-RPC client transport. + /// + /// # Errors + /// + /// Returns an error if the `CookieFile` cannot be read or invalid pub fn get_user_pass(self) -> Result<(Option, Option), Error> { match self { Auth::None => Ok((None, None)), @@ -35,7 +56,9 @@ impl Auth { } } -// RPC Client. +/// Bitcoin Core JSON-RPC Client. +/// +/// A wrapper for JSON-RPC client for interacting with the `bitcoind` RPC interface. #[derive(Debug)] pub struct Client { /// The inner JSON-RPC client. @@ -43,10 +66,21 @@ pub struct Client { } impl Client { - /// Creates a client to a bitcoind JSON-RPC server. + /// Creates a client connection to a bitcoind JSON-RPC server with authentication /// /// Requires authentication via username/password or cookie file. /// For connections without authentication, use `with_transport` instead. + /// + /// # Arguments + /// + /// * `url` - URL of the RPC server + /// * `auth` - authentication method (`UserPass` or `CookieFile`). + /// + /// # Errors + /// + /// * Returns `Error::MissingAuthentication` if `Auth::None` is provided. + /// * Returns `Error::InvalidResponse` if the URL is invalid. + /// * Returns errors related to reading the cookie file. pub fn with_auth(url: &str, auth: Auth) -> Result { if matches!(auth, Auth::None) { return Err(Error::MissingAuthentication); @@ -86,7 +120,9 @@ impl Client { } } - /// Calls the RPC `method` with a given `args` list. + /// Calls the underlying RPC `method` with given `args` list + /// + /// This is the generic function used by all specific RPC methods. pub fn call(&self, method: &str, args: &[serde_json::Value]) -> Result where T: for<'de> serde::Deserialize<'de>, @@ -99,12 +135,154 @@ impl Client { } } -// `bitcoind` RPC methods +/// `Bitcoind` RPC methods implementation for `Client` impl Client { - /// Get best block hash. + /// Retrieves the raw block data for a given block hash (verbosity 0) + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The deserialized `Block` struct. + pub fn get_block(&self, block_hash: &BlockHash) -> Result { + self.call::("getblock", &[json!(block_hash), json!(0)]) + .and_then(|block_hex| deserialize_hex(&block_hex).map_err(Error::DecodeHex)) + } + + /// Retrieves the hash of the tip of the best block chain. + /// + /// # Returns + /// + /// The `BlockHash` of the chain tip. pub fn get_best_block_hash(&self) -> Result { - let res: String = self.call("getbestblockhash", &[])?; - Ok(res.parse()?) + self.call::("getbestblockhash", &[]) + .and_then(|block_hex| block_hex.parse().map_err(Error::from)) + } + + /// Retrieves the number of blocks in the longest chain + /// + /// # Returns + /// + /// The block count as a `u32` + pub fn get_block_count(&self) -> Result { + self.call::("getblockcount", &[])? + .0 + .try_into() + .map_err(Error::Overflow) + } + + /// Retrieves the block hash at a given height + /// + /// # Arguments + /// + /// * `height`: The block height + /// + /// # Returns + /// + /// The `BlockHash` for the given height + pub fn get_block_hash(&self, height: u32) -> Result { + self.call::("getblockhash", &[json!(height)]) + .and_then(|block_hex| block_hex.parse().map_err(Error::from)) + } + + /// Retrieve the `basic` BIP 157 content filter for a particular block + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block whose filter is requested + /// + /// # Returns + /// + /// The `GetBlockFilter` structure containing the filter data + pub fn get_block_filter(&self, block_hash: &BlockHash) -> Result { + let block_filter: v30::GetBlockFilter = + self.call("getblockfilter", &[json!(block_hash)])?; + block_filter + .into_model() + .map_err(Error::GetBlockFilterError) + } + + /// Retrieves the raw block header for a given block hash. + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block whose header is requested. + /// + /// # Returns + /// + /// The deserialized `Header` struct + pub fn get_block_header(&self, block_hash: &BlockHash) -> Result { + self.call::("getblockheader", &[json!(block_hash), json!(false)]) + .and_then(|header_hex: String| deserialize_hex(&header_hex).map_err(Error::DecodeHex)) + } + + /// Retrieves the transaction IDs of all transactions currently in the mempool + /// + /// # Returns + /// + /// A vector of `Txid`s in the raw mempool + pub fn get_raw_mempool(&self) -> Result, Error> { + self.call::("getrawmempool", &[]) + .map(|txids| txids.0) + } + + /// Retrieves the raw transaction data for a given transaction ID + /// + /// # Arguments + /// + /// * `txid`: The transaction ID to retrieve. + /// + /// # Returns + /// + /// The deserialized `Transaction` struct + pub fn get_raw_transaction(&self, txid: &Txid) -> Result { + self.call::("getrawtransaction", &[json!(txid)]) + .and_then(|tx_hex| deserialize_hex(&tx_hex).map_err(Error::DecodeHex)) + } +} + +#[cfg(not(feature = "28_0"))] +use corepc_types::model::{GetBlockHeaderVerbose, GetBlockVerboseOne}; + +#[cfg(not(feature = "28_0"))] +impl Client { + /// Retrieves the verbose JSON representation of a block header (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose header as a `GetBlockHeaderVerbose` struct. + pub fn get_block_header_verbose( + &self, + block_hash: &BlockHash, + ) -> Result { + let header_info: v30::GetBlockHeaderVerbose = + self.call("getblockheader", &[json!(block_hash)])?; + header_info + .into_model() + .map_err(Error::GetBlockHeaderVerboseError) + } + + /// Retrieves the verbose JSON representation of a block (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose block data as a `GetBlockVerboseOne` struct. + pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { + let block_info: v30::GetBlockVerboseOne = + self.call("getblock", &[json!(block_hash), json!(1)])?; + block_info + .into_model() + .map_err(Error::GetBlockVerboseOneError) } } diff --git a/src/client/v28.rs b/src/client/v28.rs new file mode 100644 index 0000000..a822627 --- /dev/null +++ b/src/client/v28.rs @@ -0,0 +1,49 @@ +use bitcoin::BlockHash; +use corepc_types::{ + bitcoin, + model::{GetBlockHeaderVerbose, GetBlockVerboseOne}, + v28, +}; + +use jsonrpc::serde_json::json; + +use crate::{Client, Error}; + +impl Client { + /// Retrieves the verbose JSON representation of a block header (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose header as a `GetBlockHeaderVerbose` struct. + pub fn get_block_header_verbose( + &self, + block_hash: &BlockHash, + ) -> Result { + let header_info: v28::GetBlockHeaderVerbose = + self.call("getblockheader", &[json!(block_hash)])?; + header_info + .into_model() + .map_err(Error::GetBlockHeaderVerboseError) + } + + /// Retrieves the verbose JSON representation of a block (verbosity 1). + /// + /// # Arguments + /// + /// * `block_hash`: The hash of the block to retrieve. + /// + /// # Returns + /// + /// The verbose block data as a `GetBlockVerboseOne` struct. + pub fn get_block_verbose(&self, block_hash: &BlockHash) -> Result { + let block_info: v28::GetBlockVerboseOne = + self.call("getblock", &[json!(block_hash), json!(1)])?; + block_info + .into_model() + .map_err(Error::GetBlockVerboseOneError) + } +} diff --git a/src/error.rs b/src/error.rs index 1e11f5a..57c353b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,16 @@ //! Error types for the Bitcoin RPC client. -use std::{fmt, io}; - -use corepc_types::bitcoin::hex::HexToArrayError; +use bitcoin::{ + consensus::encode::FromHexError, + hex::{HexToArrayError, HexToBytesError}, +}; +#[cfg(feature = "28_0")] +use corepc_types::v17::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; +#[cfg(not(feature = "28_0"))] +use corepc_types::v30::{GetBlockHeaderVerboseError, GetBlockVerboseOneError}; +use corepc_types::{bitcoin, v30::GetBlockFilterError}; use jsonrpc::serde_json; +use std::{fmt, io, num::TryFromIntError}; /// Result type alias for the RPC client. pub type Result = std::result::Result; @@ -11,6 +18,18 @@ pub type Result = std::result::Result; /// Errors that can occur when using the Bitcoin RPC client. #[derive(Debug)] pub enum Error { + /// Hex deserialization error + DecodeHex(FromHexError), + + /// Error converting `GetBlockVersboseOne` type into the model type + GetBlockVerboseOneError(GetBlockVerboseOneError), + + /// Error modeling [`GetBlockHeaderVerbose`](corepc_types::model::GetBlockHeaderVerbose). + GetBlockHeaderVerboseError(GetBlockHeaderVerboseError), + + /// Error modeling [`GetBlockFilter`](corepc_types::model::GetBlockFilter) + GetBlockFilterError(GetBlockFilterError), + /// Missing authentication credentials. MissingAuthentication, @@ -23,6 +42,9 @@ pub enum Error { /// JSON-RPC error from the server. JsonRpc(jsonrpc::Error), + /// Hex decoding error for byte vectors (used in get_block, etc.) + HexToBytes(HexToBytesError), + /// Hash parsing error. HexToArray(HexToArrayError), @@ -31,6 +53,9 @@ pub enum Error { /// I/O error (e.g., reading cookie file, network issues). Io(io::Error), + + /// Error when converting an integer type to a smaller type due to overflow. + Overflow(TryFromIntError), } impl fmt::Display for Error { @@ -41,10 +66,16 @@ impl fmt::Display for Error { } Error::InvalidCookieFile => write!(f, "invalid cookie file"), Error::InvalidResponse(e) => write!(f, "invalid response: {e}"), + Error::HexToBytes(e) => write!(f, "Hex to bytes error: {e}"), Error::HexToArray(e) => write!(f, "Hash parsing eror: {e}"), Error::JsonRpc(e) => write!(f, "JSON-RPC error: {e}"), Error::Json(e) => write!(f, "JSON error: {e}"), Error::Io(e) => write!(f, "I/O error: {e}"), + Error::DecodeHex(e) => write!(f, "Hex deserialization error: {e}"), + Error::GetBlockHeaderVerboseError(e) => write!(f, "{e}"), + Error::GetBlockVerboseOneError(e) => write!(f, "{e}"), + Error::Overflow(e) => write!(f, "Integer conversion overflow error: {e}"), + Error::GetBlockFilterError(e) => write!(f, "{e}"), } } } @@ -55,7 +86,12 @@ impl std::error::Error for Error { Error::JsonRpc(e) => Some(e), Error::Json(e) => Some(e), Error::Io(e) => Some(e), + Error::HexToBytes(e) => Some(e), Error::HexToArray(e) => Some(e), + Error::DecodeHex(e) => Some(e), + Error::GetBlockVerboseOneError(e) => Some(e), + Error::Overflow(e) => Some(e), + Error::GetBlockFilterError(e) => Some(e), _ => None, } } @@ -85,3 +121,21 @@ impl From for Error { Error::Io(e) } } + +impl From for Error { + fn from(e: TryFromIntError) -> Self { + Error::Overflow(e) + } +} + +impl From for Error { + fn from(e: GetBlockVerboseOneError) -> Self { + Error::GetBlockVerboseOneError(e) + } +} + +impl From for Error { + fn from(e: FromHexError) -> Self { + Error::DecodeHex(e) + } +} diff --git a/tests/test_rpc_client.rs b/tests/test_rpc_client.rs index 89dde64..d95ab98 100644 --- a/tests/test_rpc_client.rs +++ b/tests/test_rpc_client.rs @@ -8,44 +8,74 @@ //! ``` use bdk_bitcoind_client::{Auth, Client, Error}; -use corepc_types::bitcoin::BlockHash; -use std::path::PathBuf; +use corepc_node::{exe_path, Conf, Node}; +use corepc_types::bitcoin::{BlockHash, Txid}; +use jsonrpc::serde_json::json; +use std::{path::PathBuf, str::FromStr}; -/// Helper to get the test RPC URL -fn test_url() -> String { - std::env::var("BITCOIN_RPC_URL").unwrap_or_else(|_| "http://localhost:18443".to_string()) +/// Helper to initialize the bitcoind executable path +fn init() -> String { + exe_path().expect("bitcoind executable not found. Set BITCOIND_EXE or enable download feature.") } -/// Helper to get test credentials -fn test_auth() -> Auth { - let user = std::env::var("BITCOIN_RPC_USER").unwrap_or_else(|_| "bitcoin".to_string()); - let pass = std::env::var("BITCOIN_RPC_PASS").unwrap_or_else(|_| "bitcoin".to_string()); - Auth::UserPass(user, pass) +/// Helper to set up a clean bitcoind node and return the client. +fn setup() -> (Client, Node) { + let exe = init(); + + let mut conf = Conf::default(); + + conf.args.push("-blockfilterindex=1"); + conf.args.push("-txindex=1"); + + let node = Node::with_conf(exe, &conf).expect("Failed to start node"); + + let rpc_url = node.rpc_url(); + let cookie = node + .params + .get_cookie_values() + .expect("Failed to read cookie") + .expect("Cookie file empty"); + + let auth = Auth::UserPass(cookie.user, cookie.password); + + let client = Client::with_auth(&rpc_url, auth).expect("failed to create client"); + + (client, node) +} + +/// Helper to mine blocks +fn mine_blocks(client: &Client, n: u64) -> Result, Error> { + let address: String = client.call("getnewaddress", &[])?; + client.call("generatetoaddress", &[json!(n), json!(address)]) } #[test] -#[ignore] fn test_client_with_user_pass() { - let client = Client::with_auth(&test_url(), test_auth()).expect("failed to create client"); + let (client, mut node) = setup(); - let result = client + let block_hash = client .get_best_block_hash() - .expect("failed to call getblockchaininfo"); + .expect("failed to call getbestblockhash"); assert_eq!( - result.to_string().len(), + block_hash.to_string().len(), 64, "block hash should be 64 characters" ); assert!( - result.to_string().chars().all(|c| c.is_ascii_hexdigit()), + block_hash + .to_string() + .chars() + .all(|c| c.is_ascii_hexdigit()), "hash should only contain hex digits" ); + + node.stop().expect("failed to stop node"); } #[test] fn test_auth_none_returns_error() { - let result = Client::with_auth(&test_url(), Auth::None); + let result = Client::with_auth("http://invalid-url", Auth::None); assert!(result.is_err()); match result { @@ -55,10 +85,10 @@ fn test_auth_none_returns_error() { } #[test] -#[ignore] fn test_invalid_credentials() { + let (_, mut node) = setup(); let client = Client::with_auth( - &test_url(), + &node.rpc_url(), Auth::UserPass("wrong".to_string(), "credentials".to_string()), ) .expect("client creation should succeed"); @@ -66,38 +96,55 @@ fn test_invalid_credentials() { let result: Result = client.get_best_block_hash(); assert!(result.is_err()); + + node.stop().expect("failed to stop node"); } #[test] fn test_invalid_cookie_file() { + let dummy_url = "http://127.0.0.1:18443"; let cookie_path = PathBuf::from("/nonexistent/path/to/cookie"); - let result = Client::with_auth(&test_url(), Auth::CookieFile(cookie_path)); - assert!(result.is_err()); + let result = Client::with_auth(dummy_url, Auth::CookieFile(cookie_path)); + + assert!( + result.is_err(), + "Client should fail when cookie file is missing" + ); + match result { Err(Error::InvalidCookieFile) => (), - Err(Error::Io(_)) => (), - _ => panic!("expected InvalidCookieFile or Io error"), + Err(Error::Io(ref e)) if e.kind() == std::io::ErrorKind::NotFound => (), + Err(e) => panic!("Expected InvalidCookieFile or NotFound Io error, got: {e:?}"), + _ => panic!("Expected an error but got Ok"), } } #[test] -#[ignore] fn test_client_with_custom_transport() { use jsonrpc::http::minreq_http::Builder; + let (_, node) = setup(); + + let rpc_url = node.rpc_url(); + let cookie = node + .params + .get_cookie_values() + .expect("Failed to read cookie") + .expect("Cookie file empty"); + let transport = Builder::new() - .url(&test_url()) + .url(&rpc_url) .expect("invalid URL") .timeout(std::time::Duration::from_secs(30)) - .basic_auth("bitcoin".to_string(), Some("bitcoin".to_string())) + .basic_auth(cookie.user, Some(cookie.password)) .build(); let client = Client::with_transport(transport); let result = client .get_best_block_hash() - .expect("failed to call getblockchaininfo"); + .expect("failed to call getbestblockhash"); assert_eq!( result.to_string().len(), @@ -105,3 +152,258 @@ fn test_client_with_custom_transport() { "block hash should be 64 characters" ); } + +#[test] +fn test_get_block_count() { + let (client, mut node) = setup(); + + let block_count = client.get_block_count().expect("failed to get block count"); + + assert_eq!(block_count, 0); + + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_hash() { + let (client, mut node) = setup(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis block hash"); + + assert_eq!(genesis_hash.to_string().len(), 64); + + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_hash_for_current_height() { + let (client, mut node) = setup(); + + let block_count = client.get_block_count().expect("failed to get block count"); + + let block_hash = client + .get_block_hash(block_count) + .expect("failed to get block hash"); + + assert_eq!(block_hash.to_string().len(), 64); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_hash_invalid_height() { + let (client, mut node) = setup(); + + let result = client.get_block_hash(999999999); + + assert!(result.is_err()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_best_block_hash() { + let (client, mut node) = setup(); + + let best_block_hash = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + assert_eq!(best_block_hash.to_string().len(), 64); + + let block_count = client.get_block_count().expect("failed to get block count"); + let block_hash = client + .get_block_hash(block_count) + .expect("failed to get block hash"); + + assert_eq!(best_block_hash, block_hash); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_best_block_hash_changes_after_mining() { + let (client, mut node) = setup(); + + let hash_before = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + mine_blocks(&client, 1).expect("failed to mine block"); + + let hash_after = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + assert_ne!(hash_before, hash_after); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block() { + let (client, mut node) = setup(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let block = client + .get_block(&genesis_hash) + .expect("failed to get block"); + + assert_eq!(block.block_hash(), genesis_hash); + assert!(!block.txdata.is_empty()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_after_mining() { + let (client, mut node) = setup(); + + let hashes = mine_blocks(&client, 1).expect("failed to mine block"); + let block_hash = BlockHash::from_str(&hashes[0]).expect("invalid hash"); + + let block = client.get_block(&block_hash).expect("failed to get block"); + + assert_eq!(block.block_hash(), block_hash); + assert!(!block.txdata.is_empty()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_invalid_hash() { + let (client, mut node) = setup(); + + let invalid_hash = + BlockHash::from_str("0000000000000000000000000000000000000000000000000000000000000000") + .unwrap(); + + let result = client.get_block(&invalid_hash); + + assert!(result.is_err()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_header() { + let (client, mut node) = setup(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let header = client + .get_block_header(&genesis_hash) + .expect("failed to get block header"); + + assert_eq!(header.block_hash(), genesis_hash); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_header_has_valid_fields() { + let (client, mut node) = setup(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let header = client + .get_block_header(&genesis_hash) + .expect("failed to get block header"); + + assert!(header.time > 0); + assert!(header.nonce >= 1); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_raw_mempool_empty() { + let (client, mut node) = setup(); + + mine_blocks(&client, 1).expect("failed to mine block"); + + std::thread::sleep(std::time::Duration::from_millis(100)); + + let mempool = client.get_raw_mempool().expect("failed to get mempool"); + + assert!(mempool.is_empty()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_raw_mempool_with_transaction() { + let (client, mut node) = setup(); + + mine_blocks(&client, 101).expect("failed to mine blocks"); + + let address: String = client + .call("getnewaddress", &[]) + .expect("failed to get address"); + let txid: String = client + .call("sendtoaddress", &[json!(address), json!(0.001)]) + .expect("failed to send transaction"); + + let mempool = client.get_raw_mempool().expect("failed to get mempool"); + + let txid_parsed = Txid::from_str(&txid).unwrap(); + assert!(mempool.contains(&txid_parsed)); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_raw_transaction() { + let (client, mut node) = setup(); + + mine_blocks(&client, 1).expect("failed to mine block"); + + let best_hash = client + .get_best_block_hash() + .expect("failed to get best block hash"); + + let block = client.get_block(&best_hash).expect("failed to get block"); + + let txid = &block.txdata[0].compute_txid(); + + let tx = client + .get_raw_transaction(txid) + .expect("failed to get raw transaction"); + + assert_eq!(tx.compute_txid(), *txid); + assert!(!tx.input.is_empty()); + assert!(!tx.output.is_empty()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_raw_transaction_invalid_txid() { + let (client, mut node) = setup(); + + let fake_txid = + Txid::from_str("0000000000000000000000000000000000000000000000000000000000000000").unwrap(); + + let result = client.get_raw_transaction(&fake_txid); + + assert!(result.is_err()); + node.stop().expect("failed to stop node"); +} + +#[test] +fn test_get_block_filter() { + let (client, mut node) = setup(); + + let genesis_hash = client + .get_block_hash(0) + .expect("failed to get genesis hash"); + + let result = client.get_block_filter(&genesis_hash); + + match result { + Ok(filter) => { + assert!(!filter.filter.is_empty()); + } + Err(_) => { + println!("Block filters not enabled (requires -blockfilterindex=1)"); + } + } + node.stop().expect("failed to stop node"); +}