From a0d725742517f904883ea54da07b19f1771a7fbd Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Wed, 18 Feb 2026 15:29:33 +0000 Subject: [PATCH 01/14] Rename jsonrpc sync client and features _sync In preparation for adding the new async client rename the existing sync client, associated feature and fuzz target to have the suffic _sync. --- .github/workflows/cron-daily-fuzz.yml | 2 +- client/Cargo.toml | 2 +- client/src/client_sync/mod.rs | 10 +++++----- fuzz/Cargo.toml | 6 +++--- .../{bitreq_http.rs => bitreq_http_sync.rs} | 2 +- fuzz/generate-files.sh | 2 +- jsonrpc/Cargo.toml | 2 +- jsonrpc/contrib/test_vars.sh | 2 +- jsonrpc/src/{client.rs => client_sync.rs} | 0 .../src/http/{bitreq_http.rs => bitreq_http_sync.rs} | 6 +++--- jsonrpc/src/http/mod.rs | 4 ++-- jsonrpc/src/http/simple_http.rs | 2 +- jsonrpc/src/lib.rs | 8 ++++---- jsonrpc/src/simple_tcp.rs | 2 +- jsonrpc/src/simple_uds.rs | 2 +- 15 files changed, 26 insertions(+), 26 deletions(-) rename fuzz/fuzz_targets/{bitreq_http.rs => bitreq_http_sync.rs} (95%) rename jsonrpc/src/{client.rs => client_sync.rs} (100%) rename jsonrpc/src/http/{bitreq_http.rs => bitreq_http_sync.rs} (97%) diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml index 9f87fe6d1..4fdb768c3 100644 --- a/.github/workflows/cron-daily-fuzz.yml +++ b/.github/workflows/cron-daily-fuzz.yml @@ -18,7 +18,7 @@ jobs: # We only get 20 jobs at a time, we probably don't want to go # over that limit with fuzzing because of the hour run time. fuzz_target: [ - bitreq_http, + bitreq_http_sync, simple_http, ] steps: diff --git a/client/Cargo.toml b/client/Cargo.toml index b7c13e367..922e158a1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -27,6 +27,6 @@ serde = { version = "1.0.103", default-features = false, features = [ "derive", serde_json = { version = "1.0.117" } types = { package = "corepc-types", version = "0.11.0", path = "../types", default-features = false, features = ["std"] } -jsonrpc = { version = "0.19.0", path = "../jsonrpc", features = ["bitreq_http"], optional = true } +jsonrpc = { version = "0.19.0", path = "../jsonrpc", features = ["bitreq_http_sync"], optional = true } [dev-dependencies] diff --git a/client/src/client_sync/mod.rs b/client/src/client_sync/mod.rs index baaf3f8ba..3d9a505ad 100644 --- a/client/src/client_sync/mod.rs +++ b/client/src/client_sync/mod.rs @@ -66,7 +66,7 @@ macro_rules! define_jsonrpc_bitreq_client { /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. pub struct Client { - inner: jsonrpc::client::Client, + inner: jsonrpc::client_sync::Client, } impl fmt::Debug for Client { @@ -81,12 +81,12 @@ macro_rules! define_jsonrpc_bitreq_client { impl Client { /// Creates a client to a bitcoind JSON-RPC server without authentication. pub fn new(url: &str) -> Self { - let transport = jsonrpc::http::bitreq_http::Builder::new() + let transport = jsonrpc::http::bitreq_http_sync::Builder::new() .url(url) .expect("jsonrpc v0.19, this function does not error") .timeout(std::time::Duration::from_secs(60)) .build(); - let inner = jsonrpc::client::Client::with_transport(transport); + let inner = jsonrpc::client_sync::Client::with_transport(transport); Self { inner } } @@ -98,13 +98,13 @@ macro_rules! define_jsonrpc_bitreq_client { } let (user, pass) = auth.get_user_pass()?; - let transport = jsonrpc::http::bitreq_http::Builder::new() + let transport = jsonrpc::http::bitreq_http_sync::Builder::new() .url(url) .expect("jsonrpc v0.19, this function does not error") .timeout(std::time::Duration::from_secs(60)) .basic_auth(user.unwrap(), pass) .build(); - let inner = jsonrpc::client::Client::with_transport(transport); + let inner = jsonrpc::client_sync::Client::with_transport(transport); Ok(Self { inner }) } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 10293ba55..11deb311b 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,7 +11,7 @@ cargo-fuzz = true [dependencies] honggfuzz = { version = "0.5.55", default-features = false } -jsonrpc = { path = "../jsonrpc", features = ["bitreq_http"] } +jsonrpc = { path = "../jsonrpc", features = ["bitreq_http_sync"] } serde = { version = "1.0.103", features = [ "derive" ] } serde_json = "1.0" @@ -20,8 +20,8 @@ serde_json = "1.0" unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)', 'cfg(jsonrpc_fuzz)'] } [[bin]] -name = "bitreq_http" -path = "fuzz_targets/bitreq_http.rs" +name = "bitreq_http_sync" +path = "fuzz_targets/bitreq_http_sync.rs" [[bin]] name = "simple_http" diff --git a/fuzz/fuzz_targets/bitreq_http.rs b/fuzz/fuzz_targets/bitreq_http_sync.rs similarity index 95% rename from fuzz/fuzz_targets/bitreq_http.rs rename to fuzz/fuzz_targets/bitreq_http_sync.rs index 90416ee0b..98fafc0da 100644 --- a/fuzz/fuzz_targets/bitreq_http.rs +++ b/fuzz/fuzz_targets/bitreq_http_sync.rs @@ -8,7 +8,7 @@ fn do_test(data: &[u8]) { { use std::io; - use jsonrpc::bitreq_http::{BitreqHttpTransport, FUZZ_TCP_SOCK}; + use jsonrpc::bitreq_http_sync::{BitreqHttpTransport, FUZZ_TCP_SOCK}; use jsonrpc::Client; *FUZZ_TCP_SOCK.lock().unwrap() = Some(io::Cursor::new(data.to_vec())); diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh index 9afe14bd3..77b4966b6 100755 --- a/fuzz/generate-files.sh +++ b/fuzz/generate-files.sh @@ -23,7 +23,7 @@ cargo-fuzz = true [dependencies] honggfuzz = { version = "0.5.55", default-features = false } -jsonrpc = { path = "..", features = ["bitreq_http"] } +jsonrpc = { path = "..", features = ["bitreq_http_sync"] } serde = { version = "1.0.103", features = [ "derive" ] } serde_json = "1.0" diff --git a/jsonrpc/Cargo.toml b/jsonrpc/Cargo.toml index 7c06709e7..fc7a61da2 100644 --- a/jsonrpc/Cargo.toml +++ b/jsonrpc/Cargo.toml @@ -21,7 +21,7 @@ default = [ "simple_http", "simple_tcp" ] # A bare-minimum HTTP transport. simple_http = [ "base64" ] # A transport that uses `bitreq` as the HTTP client. -bitreq_http = [ "base64", "bitreq" ] +bitreq_http_sync = [ "base64", "bitreq" ] # Basic transport over a raw TcpListener simple_tcp = [] # Basic transport over a raw UnixStream diff --git a/jsonrpc/contrib/test_vars.sh b/jsonrpc/contrib/test_vars.sh index 56bcb8eb6..05f57bdb7 100644 --- a/jsonrpc/contrib/test_vars.sh +++ b/jsonrpc/contrib/test_vars.sh @@ -4,7 +4,7 @@ FEATURES_WITH_STD="" # So this is the var to use for all tests. -FEATURES_WITHOUT_STD="simple_http bitreq_http simple_tcp simple_uds proxy" +FEATURES_WITHOUT_STD="simple_http bitreq_http_sync simple_tcp simple_uds proxy" # Run these examples. EXAMPLES="" diff --git a/jsonrpc/src/client.rs b/jsonrpc/src/client_sync.rs similarity index 100% rename from jsonrpc/src/client.rs rename to jsonrpc/src/client_sync.rs diff --git a/jsonrpc/src/http/bitreq_http.rs b/jsonrpc/src/http/bitreq_http_sync.rs similarity index 97% rename from jsonrpc/src/http/bitreq_http.rs rename to jsonrpc/src/http/bitreq_http_sync.rs index 20903bfc5..dbbcff100 100644 --- a/jsonrpc/src/http/bitreq_http.rs +++ b/jsonrpc/src/http/bitreq_http_sync.rs @@ -1,4 +1,4 @@ -//! This module implements the [`crate::client::Transport`] trait using [`bitreq`] +//! This module implements the [`crate::client_sync::Transport`] trait using [`bitreq`] //! as the underlying HTTP transport. //! //! [bitreq]: @@ -13,7 +13,7 @@ use std::{error, fmt}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use crate::client::Transport; +use crate::client_sync::Transport; use crate::{Request, Response}; const DEFAULT_URL: &str = "http://localhost"; @@ -137,7 +137,7 @@ impl Builder { /// # Examples /// /// ```no_run - /// # use jsonrpc::bitreq_http::BitreqHttpTransport; + /// # use jsonrpc::bitreq_http_sync::BitreqHttpTransport; /// # use std::fs::{self, File}; /// # use std::path::Path; /// # let cookie_file = Path::new("~/.bitcoind/.cookie"); diff --git a/jsonrpc/src/http/mod.rs b/jsonrpc/src/http/mod.rs index f6221f388..efdaf890b 100644 --- a/jsonrpc/src/http/mod.rs +++ b/jsonrpc/src/http/mod.rs @@ -3,8 +3,8 @@ #[cfg(feature = "simple_http")] pub mod simple_http; -#[cfg(feature = "bitreq_http")] -pub mod bitreq_http; +#[cfg(feature = "bitreq_http_sync")] +pub mod bitreq_http_sync; /// The default TCP port to use for connections. /// Set to 8332, the default RPC port for bitcoind. diff --git a/jsonrpc/src/http/simple_http.rs b/jsonrpc/src/http/simple_http.rs index 795b67092..1edd02b40 100644 --- a/jsonrpc/src/http/simple_http.rs +++ b/jsonrpc/src/http/simple_http.rs @@ -19,7 +19,7 @@ use base64::Engine; #[cfg(feature = "proxy")] use socks::Socks5Stream; -use crate::client::Transport; +use crate::client_sync::Transport; use crate::http::DEFAULT_PORT; #[cfg(feature = "proxy")] use crate::http::DEFAULT_PROXY_PORT; diff --git a/jsonrpc/src/lib.rs b/jsonrpc/src/lib.rs index ee2953ee7..b4da49cf0 100644 --- a/jsonrpc/src/lib.rs +++ b/jsonrpc/src/lib.rs @@ -20,12 +20,12 @@ pub extern crate base64; #[cfg(feature = "bitreq")] pub extern crate bitreq; -pub mod client; +pub mod client_sync; pub mod error; pub mod http; -#[cfg(feature = "bitreq_http")] -pub use http::bitreq_http; +#[cfg(feature = "bitreq_http_sync")] +pub use http::bitreq_http_sync; #[cfg(feature = "simple_http")] pub use http::simple_http; @@ -38,7 +38,7 @@ pub mod simple_uds; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; -pub use crate::client::{Client, Transport}; +pub use crate::client_sync::{Client, Transport}; pub use crate::error::Error; /// Shorthand method to convert an argument into a boxed [`serde_json::value::RawValue`]. diff --git a/jsonrpc/src/simple_tcp.rs b/jsonrpc/src/simple_tcp.rs index 9b1ed24db..846f1027f 100644 --- a/jsonrpc/src/simple_tcp.rs +++ b/jsonrpc/src/simple_tcp.rs @@ -5,7 +5,7 @@ use std::{error, fmt, io, net, time}; -use crate::client::Transport; +use crate::client_sync::Transport; use crate::{Request, Response}; #[derive(Debug, Clone)] diff --git a/jsonrpc/src/simple_uds.rs b/jsonrpc/src/simple_uds.rs index 0f324d738..40fb01e84 100644 --- a/jsonrpc/src/simple_uds.rs +++ b/jsonrpc/src/simple_uds.rs @@ -5,7 +5,7 @@ use std::os::unix::net::UnixStream; use std::{error, fmt, io, path, time}; -use crate::client::Transport; +use crate::client_sync::Transport; use crate::{Request, Response}; /// Simple synchronous UDS transport. From 64986c4ae608742ca18adb02d21d603de88a9d5e Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Wed, 18 Feb 2026 16:48:30 +0000 Subject: [PATCH 02/14] Remove jsonrpc reexports of Client and Transport In preparation for adding an async client remove the reexports of the sync Client and Transport. Fix the affected imports and Debug impl. --- jsonrpc/src/client_sync.rs | 2 +- jsonrpc/src/http/bitreq_http_sync.rs | 2 +- jsonrpc/src/http/simple_http.rs | 13 ++++++------- jsonrpc/src/lib.rs | 1 - jsonrpc/src/simple_tcp.rs | 2 +- jsonrpc/src/simple_uds.rs | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/jsonrpc/src/client_sync.rs b/jsonrpc/src/client_sync.rs index e9cb0ca67..6ffdd5843 100644 --- a/jsonrpc/src/client_sync.rs +++ b/jsonrpc/src/client_sync.rs @@ -124,7 +124,7 @@ impl Client { } } -impl fmt::Debug for crate::Client { +impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "jsonrpc::Client(")?; self.transport.fmt_target(f)?; diff --git a/jsonrpc/src/http/bitreq_http_sync.rs b/jsonrpc/src/http/bitreq_http_sync.rs index dbbcff100..7fbf05573 100644 --- a/jsonrpc/src/http/bitreq_http_sync.rs +++ b/jsonrpc/src/http/bitreq_http_sync.rs @@ -259,7 +259,7 @@ mod impls { #[cfg(test)] mod tests { use super::*; - use crate::Client; + use crate::client_sync::Client; #[test] fn construct() { diff --git a/jsonrpc/src/http/simple_http.rs b/jsonrpc/src/http/simple_http.rs index 1edd02b40..c77085e4f 100644 --- a/jsonrpc/src/http/simple_http.rs +++ b/jsonrpc/src/http/simple_http.rs @@ -19,7 +19,7 @@ use base64::Engine; #[cfg(feature = "proxy")] use socks::Socks5Stream; -use crate::client_sync::Transport; +use crate::client_sync::{Client, Transport}; use crate::http::DEFAULT_PORT; #[cfg(feature = "proxy")] use crate::http::DEFAULT_PROXY_PORT; @@ -413,18 +413,18 @@ impl Default for Builder { fn default() -> Self { Builder::new() } } -impl crate::Client { +impl Client { /// Creates a new JSON-RPC client using a bare-minimum HTTP transport. pub fn simple_http( url: &str, user: Option, pass: Option, - ) -> Result { + ) -> Result { let mut builder = Builder::new().url(url)?; if let Some(user) = user { builder = builder.auth(user, pass); } - Ok(crate::Client::with_transport(builder.build())) + Ok(Client::with_transport(builder.build())) } /// Creates a new JSON_RPC client using a HTTP-Socks5 proxy transport. @@ -435,7 +435,7 @@ impl crate::Client { pass: Option, proxy_addr: &str, proxy_auth: Option<(&str, &str)>, - ) -> Result { + ) -> Result { let mut builder = Builder::new().url(url)?; if let Some(user) = user { builder = builder.auth(user, pass); @@ -445,7 +445,7 @@ impl crate::Client { builder = builder.proxy_auth(user, pass); } let tp = builder.build(); - Ok(crate::Client::with_transport(tp)) + Ok(Client::with_transport(tp)) } } @@ -627,7 +627,6 @@ mod tests { use std::str::FromStr; use super::*; - use crate::Client; #[test] fn test_urls() { diff --git a/jsonrpc/src/lib.rs b/jsonrpc/src/lib.rs index b4da49cf0..7642ab018 100644 --- a/jsonrpc/src/lib.rs +++ b/jsonrpc/src/lib.rs @@ -38,7 +38,6 @@ pub mod simple_uds; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; -pub use crate::client_sync::{Client, Transport}; pub use crate::error::Error; /// Shorthand method to convert an argument into a boxed [`serde_json::value::RawValue`]. diff --git a/jsonrpc/src/simple_tcp.rs b/jsonrpc/src/simple_tcp.rs index 846f1027f..fa5a52fea 100644 --- a/jsonrpc/src/simple_tcp.rs +++ b/jsonrpc/src/simple_tcp.rs @@ -110,7 +110,7 @@ mod tests { use std::thread; use super::*; - use crate::Client; + use crate::client_sync::Client; // Test a dummy request / response over a raw TCP transport #[test] diff --git a/jsonrpc/src/simple_uds.rs b/jsonrpc/src/simple_uds.rs index 40fb01e84..69942d247 100644 --- a/jsonrpc/src/simple_uds.rs +++ b/jsonrpc/src/simple_uds.rs @@ -115,7 +115,7 @@ mod tests { use std::{fs, process, thread}; use super::*; - use crate::Client; + use crate::client_sync::Client; // Test a dummy request / response over an UDS #[test] From 6efe8b939d6b8a50379757c8b3e74f1090a101b3 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Fri, 6 Feb 2026 16:20:46 +0000 Subject: [PATCH 03/14] Copy jsonrpc client and bitrteq_http for async. Code copy only to make it easier in the next patch to see what the changes are for async. --- jsonrpc/src/client_async.rs | 251 +++++++++++++++++++++++ jsonrpc/src/http/bitreq_http_async.rs | 274 ++++++++++++++++++++++++++ 2 files changed, 525 insertions(+) create mode 100644 jsonrpc/src/client_async.rs create mode 100644 jsonrpc/src/http/bitreq_http_async.rs diff --git a/jsonrpc/src/client_async.rs b/jsonrpc/src/client_async.rs new file mode 100644 index 000000000..6ffdd5843 --- /dev/null +++ b/jsonrpc/src/client_async.rs @@ -0,0 +1,251 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! # Client support +//! +//! Support for connecting to JSONRPC servers over HTTP, sending requests, +//! and parsing responses. + +use std::borrow::Cow; +use std::collections::HashMap; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::sync::atomic; + +use serde_json::value::RawValue; +use serde_json::Value; + +use crate::error::Error; +use crate::{Request, Response}; + +/// An interface for a transport over which to use the JSONRPC protocol. +pub trait Transport: Send + Sync + 'static { + /// Sends an RPC request over the transport. + fn send_request(&self, _: Request) -> Result; + /// Sends a batch of RPC requests over the transport. + fn send_batch(&self, _: &[Request]) -> Result, Error>; + /// Formats the target of this transport. I.e. the URL/socket/... + fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result; +} + +/// A JSON-RPC client. +/// +/// Creates a new Client using one of the transport-specific constructors e.g., +/// [`Client::simple_http`] for a bare-minimum HTTP transport. +pub struct Client { + pub(crate) transport: Box, + nonce: atomic::AtomicUsize, +} + +impl Client { + /// Creates a new client with the given transport. + pub fn with_transport(transport: T) -> Client { + Client { transport: Box::new(transport), nonce: atomic::AtomicUsize::new(1) } + } + + /// Builds a request. + /// + /// To construct the arguments, one can use one of the shorthand methods. + /// [`crate::arg`] or [`crate::try_arg`]. + pub fn build_request<'a>(&self, method: &'a str, params: Option<&'a RawValue>) -> Request<'a> { + let nonce = self.nonce.fetch_add(1, atomic::Ordering::Relaxed); + Request { method, params, id: serde_json::Value::from(nonce), jsonrpc: Some("2.0") } + } + + /// Sends a request to a client. + pub fn send_request(&self, request: Request) -> Result { + self.transport.send_request(request) + } + + /// Sends a batch of requests to the client. + /// + /// Note that the requests need to have valid IDs, so it is advised to create the requests + /// with [`Client::build_request`]. + /// + /// # Returns + /// + /// The return vector holds the response for the request at the corresponding index. If no + /// response was provided, it's [`None`]. + pub fn send_batch(&self, requests: &[Request]) -> Result>, Error> { + if requests.is_empty() { + return Err(Error::EmptyBatch); + } + + // If the request body is invalid JSON, the response is a single response object. + // We ignore this case since we are confident we are producing valid JSON. + let responses = self.transport.send_batch(requests)?; + if responses.len() > requests.len() { + return Err(Error::WrongBatchResponseSize); + } + + //TODO(stevenroose) check if the server preserved order to avoid doing the mapping + + // First index responses by ID and catch duplicate IDs. + let mut by_id = HashMap::with_capacity(requests.len()); + for resp in responses.into_iter() { + let id = HashableValue(Cow::Owned(resp.id.clone())); + if let Some(dup) = by_id.insert(id, resp) { + return Err(Error::BatchDuplicateResponseId(dup.id)); + } + } + // Match responses to the requests. + let results = + requests.iter().map(|r| by_id.remove(&HashableValue(Cow::Borrowed(&r.id)))).collect(); + + // Since we're also just producing the first duplicate ID, we can also just produce the + // first incorrect ID in case there are multiple. + if let Some(id) = by_id.keys().next() { + return Err(Error::WrongBatchResponseId((*id.0).clone())); + } + + Ok(results) + } + + /// Makes a request and deserializes the response. + /// + /// To construct the arguments, one can use one of the shorthand methods + /// [`crate::arg`] or [`crate::try_arg`]. + pub fn call serde::de::Deserialize<'a>>( + &self, + method: &str, + args: Option<&RawValue>, + ) -> Result { + let request = self.build_request(method, args); + let id = request.id.clone(); + + let response = self.send_request(request)?; + if response.jsonrpc.is_some() && response.jsonrpc != Some(From::from("2.0")) { + return Err(Error::VersionMismatch); + } + if response.id != id { + return Err(Error::NonceMismatch); + } + + response.result() + } +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "jsonrpc::Client(")?; + self.transport.fmt_target(f)?; + write!(f, ")") + } +} + +impl From for Client { + fn from(t: T) -> Client { Client::with_transport(t) } +} + +/// Newtype around `Value` which allows hashing for use as hashmap keys, +/// this is needed for batch requests. +/// +/// The reason `Value` does not support `Hash` or `Eq` by itself +/// is that it supports `f64` values; but for batch requests we +/// will only be hashing the "id" field of the request/response +/// pair, which should never need decimal precision and therefore +/// never use `f64`. +#[derive(Clone, PartialEq, Debug)] +struct HashableValue<'a>(pub Cow<'a, Value>); + +impl Eq for HashableValue<'_> {} + +impl Hash for HashableValue<'_> { + fn hash(&self, state: &mut H) { + match *self.0.as_ref() { + Value::Null => "null".hash(state), + Value::Bool(false) => "false".hash(state), + Value::Bool(true) => "true".hash(state), + Value::Number(ref n) => { + "number".hash(state); + if let Some(n) = n.as_i64() { + n.hash(state); + } else if let Some(n) = n.as_u64() { + n.hash(state); + } else { + n.to_string().hash(state); + } + } + Value::String(ref s) => { + "string".hash(state); + s.hash(state); + } + Value::Array(ref v) => { + "array".hash(state); + v.len().hash(state); + for obj in v { + HashableValue(Cow::Borrowed(obj)).hash(state); + } + } + Value::Object(ref m) => { + "object".hash(state); + m.len().hash(state); + for (key, val) in m { + key.hash(state); + HashableValue(Cow::Borrowed(val)).hash(state); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + use std::collections::HashSet; + use std::str::FromStr; + use std::sync; + + use super::*; + + struct DummyTransport; + impl Transport for DummyTransport { + fn send_request(&self, _: Request) -> Result { Err(Error::NonceMismatch) } + fn send_batch(&self, _: &[Request]) -> Result, Error> { Ok(vec![]) } + fn fmt_target(&self, _: &mut fmt::Formatter) -> fmt::Result { Ok(()) } + } + + #[test] + fn sanity() { + let client = Client::with_transport(DummyTransport); + assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 1); + let req1 = client.build_request("test", None); + assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 2); + let req2 = client.build_request("test", None); + assert_eq!(client.nonce.load(sync::atomic::Ordering::Relaxed), 3); + assert!(req1.id != req2.id); + } + + #[test] + fn hash_value() { + let val = HashableValue(Cow::Owned(Value::from_str("null").unwrap())); + let t = HashableValue(Cow::Owned(Value::from_str("true").unwrap())); + let f = HashableValue(Cow::Owned(Value::from_str("false").unwrap())); + let ns = + HashableValue(Cow::Owned(Value::from_str("[0, -0, 123.4567, -100000000]").unwrap())); + let m = + HashableValue(Cow::Owned(Value::from_str("{ \"field\": 0, \"field\": -0 }").unwrap())); + + let mut coll = HashSet::new(); + + assert!(!coll.contains(&val)); + coll.insert(val.clone()); + assert!(coll.contains(&val)); + + assert!(!coll.contains(&t)); + assert!(!coll.contains(&f)); + coll.insert(t.clone()); + assert!(coll.contains(&t)); + assert!(!coll.contains(&f)); + coll.insert(f.clone()); + assert!(coll.contains(&t)); + assert!(coll.contains(&f)); + + assert!(!coll.contains(&ns)); + coll.insert(ns.clone()); + assert!(coll.contains(&ns)); + + assert!(!coll.contains(&m)); + coll.insert(m.clone()); + assert!(coll.contains(&m)); + } +} diff --git a/jsonrpc/src/http/bitreq_http_async.rs b/jsonrpc/src/http/bitreq_http_async.rs new file mode 100644 index 000000000..7fbf05573 --- /dev/null +++ b/jsonrpc/src/http/bitreq_http_async.rs @@ -0,0 +1,274 @@ +//! This module implements the [`crate::client_sync::Transport`] trait using [`bitreq`] +//! as the underlying HTTP transport. +//! +//! [bitreq]: + +#[cfg(jsonrpc_fuzz)] +use std::io::{self, Read, Write}; +#[cfg(jsonrpc_fuzz)] +use std::sync::Mutex; +use std::time::Duration; +use std::{error, fmt}; + +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; + +use crate::client_sync::Transport; +use crate::{Request, Response}; + +const DEFAULT_URL: &str = "http://localhost"; +const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind. +#[cfg(not(jsonrpc_fuzz))] +const DEFAULT_TIMEOUT_SECONDS: u64 = 15; +#[cfg(jsonrpc_fuzz)] +const DEFAULT_TIMEOUT_SECONDS: u64 = 1; + +/// An HTTP transport that uses [`bitreq`] and is useful for running a bitcoind RPC client. +#[derive(Clone, Debug)] +pub struct BitreqHttpTransport { + /// URL of the RPC server. + url: String, + /// Timeout only supports second granularity. + timeout: Duration, + /// The value of the `Authorization` HTTP header, i.e., a base64 encoding of 'user:password'. + basic_auth: Option, +} + +impl Default for BitreqHttpTransport { + fn default() -> Self { + BitreqHttpTransport { + url: format!("{}:{}", DEFAULT_URL, DEFAULT_PORT), + timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECONDS), + basic_auth: None, + } + } +} + +impl BitreqHttpTransport { + /// Constructs a new [`BitreqHttpTransport`] with default parameters. + pub fn new() -> Self { BitreqHttpTransport::default() } + + /// Returns a builder for [`BitreqHttpTransport`]. + pub fn builder() -> Builder { Builder::new() } + + fn request(&self, req: impl serde::Serialize) -> Result + where + R: for<'a> serde::de::Deserialize<'a>, + { + let req = match &self.basic_auth { + Some(auth) => bitreq::Request::new(bitreq::Method::Post, &self.url) + .with_timeout(self.timeout.as_secs()) + .with_header("Authorization", auth) + .with_json(&req)?, + None => bitreq::Request::new(bitreq::Method::Post, &self.url) + .with_timeout(self.timeout.as_secs()) + .with_json(&req)?, + }; + + // Send the request and parse the response. If the response is an error that does not + // contain valid JSON in its body (for instance if the bitcoind HTTP server work queue + // depth is exceeded), return the raw HTTP error so users can match against it. + let resp = req.send()?; + match resp.json() { + Ok(json) => Ok(json), + Err(bitreq_err) => + if resp.status_code != 200 { + Err(Error::Http(HttpError { + status_code: resp.status_code, + body: resp.as_str().unwrap_or("").to_string(), + })) + } else { + Err(Error::Bitreq(bitreq_err)) + }, + } + } +} + +impl Transport for BitreqHttpTransport { + fn send_request(&self, req: Request) -> Result { + Ok(self.request(req)?) + } + + fn send_batch(&self, reqs: &[Request]) -> Result, crate::Error> { + Ok(self.request(reqs)?) + } + + fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.url) } +} + +/// Builder for simple bitcoind [`BitreqHttpTransport`]. +#[derive(Clone, Debug)] +pub struct Builder { + tp: BitreqHttpTransport, +} + +impl Builder { + /// Constructs a new [`Builder`] with default configuration and the URL to use. + pub fn new() -> Builder { Builder { tp: BitreqHttpTransport::new() } } + + /// Sets the timeout after which requests will abort if they aren't finished. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.tp.timeout = timeout; + self + } + + /// Sets the URL of the server to the transport. + #[allow(clippy::assigning_clones)] // clone_into is only available in Rust 1.63 + pub fn url(mut self, url: &str) -> Result { + self.tp.url = url.to_owned(); + Ok(self) + } + + /// Adds authentication information to the transport. + pub fn basic_auth(mut self, user: String, pass: Option) -> Self { + let mut s = user; + s.push(':'); + if let Some(ref pass) = pass { + s.push_str(pass.as_ref()); + } + self.tp.basic_auth = Some(format!("Basic {}", &BASE64.encode(s.as_bytes()))); + self + } + + /// Adds authentication information to the transport using a cookie string ('user:pass'). + /// + /// Does no checking on the format of the cookie string, just base64 encodes whatever is passed in. + /// + /// # Examples + /// + /// ```no_run + /// # use jsonrpc::bitreq_http_sync::BitreqHttpTransport; + /// # use std::fs::{self, File}; + /// # use std::path::Path; + /// # let cookie_file = Path::new("~/.bitcoind/.cookie"); + /// let mut file = File::open(cookie_file).expect("couldn't open cookie file"); + /// let mut cookie = String::new(); + /// fs::read_to_string(&mut cookie).expect("couldn't read cookie file"); + /// let client = BitreqHttpTransport::builder().cookie_auth(cookie); + /// ``` + pub fn cookie_auth>(mut self, cookie: S) -> Self { + self.tp.basic_auth = Some(format!("Basic {}", &BASE64.encode(cookie.as_ref().as_bytes()))); + self + } + + /// Builds the final [`BitreqHttpTransport`]. + pub fn build(self) -> BitreqHttpTransport { self.tp } +} + +impl Default for Builder { + fn default() -> Self { Builder::new() } +} + +/// An HTTP error. +#[derive(Debug)] +pub struct HttpError { + /// Status code of the error response. + pub status_code: i32, + /// Raw body of the error response. + pub body: String, +} + +impl fmt::Display for HttpError { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + write!(f, "status: {}, body: {}", self.status_code, self.body) + } +} + +impl error::Error for HttpError {} + +/// Error that can happen when sending requests. +/// +/// In case of error, a JSON error is returned if the body of the response could be parsed as such. +/// Otherwise, an HTTP error is returned containing the status code and the raw body. +#[non_exhaustive] +#[derive(Debug)] +pub enum Error { + /// JSON parsing error. + Json(serde_json::Error), + /// Bitreq error. + Bitreq(bitreq::Error), + /// HTTP error that does not contain valid JSON as body. + Http(HttpError), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + match *self { + Error::Json(ref e) => write!(f, "parsing JSON failed: {}", e), + Error::Bitreq(ref e) => write!(f, "bitreq: {}", e), + Error::Http(ref e) => write!(f, "http ({})", e), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + use self::Error::*; + + match *self { + Json(ref e) => Some(e), + Bitreq(ref e) => Some(e), + Http(ref e) => Some(e), + } + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { Error::Json(e) } +} + +impl From for Error { + fn from(e: bitreq::Error) -> Self { Error::Bitreq(e) } +} + +impl From for crate::Error { + fn from(e: Error) -> crate::Error { + match e { + Error::Json(e) => crate::Error::Json(e), + e => crate::Error::Transport(Box::new(e)), + } + } +} + +/// Global mutex used by the fuzzing harness to inject data into the read end of the TCP stream. +#[cfg(jsonrpc_fuzz)] +pub static FUZZ_TCP_SOCK: Mutex>>> = Mutex::new(None); + +#[cfg(jsonrpc_fuzz)] +#[derive(Clone, Debug)] +struct TcpStream; + +#[cfg(jsonrpc_fuzz)] +mod impls { + use super::*; + + impl Read for TcpStream { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match *FUZZ_TCP_SOCK.lock().unwrap() { + Some(ref mut cursor) => io::Read::read(cursor, buf), + None => Ok(0), + } + } + } + impl Write for TcpStream { + fn write(&mut self, buf: &[u8]) -> io::Result { io::sink().write(buf) } + fn flush(&mut self) -> io::Result<()> { Ok(()) } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::client_sync::Client; + + #[test] + fn construct() { + let tp = Builder::new() + .timeout(Duration::from_millis(100)) + .url("http://localhost:22") + .unwrap() + .basic_auth("user".to_string(), None) + .build(); + let _ = Client::with_transport(tp); + } +} From a652f85dc025a004b01c3697c0b1c9bc22a6e420 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Fri, 6 Feb 2026 17:38:47 +0000 Subject: [PATCH 04/14] Remove fuzz related sections Remove code related to fuzzing from the new async files. These can be added back later once the async version is working. --- jsonrpc/src/http/bitreq_http_async.rs | 33 --------------------------- 1 file changed, 33 deletions(-) diff --git a/jsonrpc/src/http/bitreq_http_async.rs b/jsonrpc/src/http/bitreq_http_async.rs index 7fbf05573..f99c4b86d 100644 --- a/jsonrpc/src/http/bitreq_http_async.rs +++ b/jsonrpc/src/http/bitreq_http_async.rs @@ -3,10 +3,6 @@ //! //! [bitreq]: -#[cfg(jsonrpc_fuzz)] -use std::io::{self, Read, Write}; -#[cfg(jsonrpc_fuzz)] -use std::sync::Mutex; use std::time::Duration; use std::{error, fmt}; @@ -18,10 +14,7 @@ use crate::{Request, Response}; const DEFAULT_URL: &str = "http://localhost"; const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind. -#[cfg(not(jsonrpc_fuzz))] const DEFAULT_TIMEOUT_SECONDS: u64 = 15; -#[cfg(jsonrpc_fuzz)] -const DEFAULT_TIMEOUT_SECONDS: u64 = 1; /// An HTTP transport that uses [`bitreq`] and is useful for running a bitcoind RPC client. #[derive(Clone, Debug)] @@ -230,32 +223,6 @@ impl From for crate::Error { } } -/// Global mutex used by the fuzzing harness to inject data into the read end of the TCP stream. -#[cfg(jsonrpc_fuzz)] -pub static FUZZ_TCP_SOCK: Mutex>>> = Mutex::new(None); - -#[cfg(jsonrpc_fuzz)] -#[derive(Clone, Debug)] -struct TcpStream; - -#[cfg(jsonrpc_fuzz)] -mod impls { - use super::*; - - impl Read for TcpStream { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - match *FUZZ_TCP_SOCK.lock().unwrap() { - Some(ref mut cursor) => io::Read::read(cursor, buf), - None => Ok(0), - } - } - } - impl Write for TcpStream { - fn write(&mut self, buf: &[u8]) -> io::Result { io::sink().write(buf) } - fn flush(&mut self) -> io::Result<()> { Ok(()) } - } -} - #[cfg(test)] mod tests { use super::*; From 2b0664da4cd639aa0f77d099c88b6e4697d3075d Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Fri, 6 Feb 2026 15:46:02 +0000 Subject: [PATCH 05/14] Add async support to jsonrpc --- jsonrpc/Cargo.toml | 4 +++ jsonrpc/src/client_async.rs | 49 +++++++++++++++++++-------- jsonrpc/src/http/bitreq_http_async.rs | 28 +++++++++------ jsonrpc/src/http/mod.rs | 3 ++ jsonrpc/src/lib.rs | 4 +++ 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/jsonrpc/Cargo.toml b/jsonrpc/Cargo.toml index fc7a61da2..624009f2a 100644 --- a/jsonrpc/Cargo.toml +++ b/jsonrpc/Cargo.toml @@ -22,6 +22,10 @@ default = [ "simple_http", "simple_tcp" ] simple_http = [ "base64" ] # A transport that uses `bitreq` as the HTTP client. bitreq_http_sync = [ "base64", "bitreq" ] +# A transport that uses `bitreq` as the async HTTP client. +bitreq_http_async = [ "base64", "bitreq", "bitreq/async", "client_async" ] +# An async JSON-RPC client implementation. +client_async = [] # Basic transport over a raw TcpListener simple_tcp = [] # Basic transport over a raw UnixStream diff --git a/jsonrpc/src/client_async.rs b/jsonrpc/src/client_async.rs index 6ffdd5843..834a0c78e 100644 --- a/jsonrpc/src/client_async.rs +++ b/jsonrpc/src/client_async.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -//! # Client support +//! # Async client support //! //! Support for connecting to JSONRPC servers over HTTP, sending requests, //! and parsing responses. @@ -8,7 +8,9 @@ use std::borrow::Cow; use std::collections::HashMap; use std::fmt; +use std::future::Future; use std::hash::{Hash, Hasher}; +use std::pin::Pin; use std::sync::atomic; use serde_json::value::RawValue; @@ -17,20 +19,25 @@ use serde_json::Value; use crate::error::Error; use crate::{Request, Response}; -/// An interface for a transport over which to use the JSONRPC protocol. +/// Boxed future type used by async transports. +pub type BoxFuture<'a, T> = Pin + Send + 'a>>; + +/// An interface for an async transport over which to use the JSONRPC protocol. pub trait Transport: Send + Sync + 'static { /// Sends an RPC request over the transport. - fn send_request(&self, _: Request) -> Result; + fn send_request<'a>(&'a self, req: Request<'a>) -> BoxFuture<'a, Result>; /// Sends a batch of RPC requests over the transport. - fn send_batch(&self, _: &[Request]) -> Result, Error>; + fn send_batch<'a>( + &'a self, + reqs: &'a [Request<'a>], + ) -> BoxFuture<'a, Result, Error>>; /// Formats the target of this transport. I.e. the URL/socket/... fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result; } -/// A JSON-RPC client. +/// An async JSON-RPC client. /// -/// Creates a new Client using one of the transport-specific constructors e.g., -/// [`Client::simple_http`] for a bare-minimum HTTP transport. +/// Creates a new Client using one of the transport-specific constructors. pub struct Client { pub(crate) transport: Box, nonce: atomic::AtomicUsize, @@ -52,8 +59,8 @@ impl Client { } /// Sends a request to a client. - pub fn send_request(&self, request: Request) -> Result { - self.transport.send_request(request) + pub async fn send_request(&self, request: Request<'_>) -> Result { + self.transport.send_request(request).await } /// Sends a batch of requests to the client. @@ -65,14 +72,17 @@ impl Client { /// /// The return vector holds the response for the request at the corresponding index. If no /// response was provided, it's [`None`]. - pub fn send_batch(&self, requests: &[Request]) -> Result>, Error> { + pub async fn send_batch( + &self, + requests: &[Request<'_>], + ) -> Result>, Error> { if requests.is_empty() { return Err(Error::EmptyBatch); } // If the request body is invalid JSON, the response is a single response object. // We ignore this case since we are confident we are producing valid JSON. - let responses = self.transport.send_batch(requests)?; + let responses = self.transport.send_batch(requests).await?; if responses.len() > requests.len() { return Err(Error::WrongBatchResponseSize); } @@ -104,7 +114,7 @@ impl Client { /// /// To construct the arguments, one can use one of the shorthand methods /// [`crate::arg`] or [`crate::try_arg`]. - pub fn call serde::de::Deserialize<'a>>( + pub async fn call serde::de::Deserialize<'a>>( &self, method: &str, args: Option<&RawValue>, @@ -112,7 +122,7 @@ impl Client { let request = self.build_request(method, args); let id = request.id.clone(); - let response = self.send_request(request)?; + let response = self.send_request(request).await?; if response.jsonrpc.is_some() && response.jsonrpc != Some(From::from("2.0")) { return Err(Error::VersionMismatch); } @@ -199,8 +209,17 @@ mod tests { struct DummyTransport; impl Transport for DummyTransport { - fn send_request(&self, _: Request) -> Result { Err(Error::NonceMismatch) } - fn send_batch(&self, _: &[Request]) -> Result, Error> { Ok(vec![]) } + fn send_request<'a>(&'a self, _: Request<'a>) -> BoxFuture<'a, Result> { + Box::pin(async { Err(Error::NonceMismatch) }) + } + + fn send_batch<'a>( + &'a self, + _: &'a [Request<'a>], + ) -> BoxFuture<'a, Result, Error>> { + Box::pin(async { Ok(vec![]) }) + } + fn fmt_target(&self, _: &mut fmt::Formatter) -> fmt::Result { Ok(()) } } diff --git a/jsonrpc/src/http/bitreq_http_async.rs b/jsonrpc/src/http/bitreq_http_async.rs index f99c4b86d..046c47a3d 100644 --- a/jsonrpc/src/http/bitreq_http_async.rs +++ b/jsonrpc/src/http/bitreq_http_async.rs @@ -1,4 +1,4 @@ -//! This module implements the [`crate::client_sync::Transport`] trait using [`bitreq`] +//! This module implements the [`crate::client_async::Transport`] trait using [`bitreq`] //! as the underlying HTTP transport. //! //! [bitreq]: @@ -9,7 +9,7 @@ use std::{error, fmt}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use crate::client_sync::Transport; +use crate::client_async::{BoxFuture, Transport}; use crate::{Request, Response}; const DEFAULT_URL: &str = "http://localhost"; @@ -44,7 +44,7 @@ impl BitreqHttpTransport { /// Returns a builder for [`BitreqHttpTransport`]. pub fn builder() -> Builder { Builder::new() } - fn request(&self, req: impl serde::Serialize) -> Result + async fn request(&self, req: impl serde::Serialize) -> Result where R: for<'a> serde::de::Deserialize<'a>, { @@ -61,7 +61,7 @@ impl BitreqHttpTransport { // Send the request and parse the response. If the response is an error that does not // contain valid JSON in its body (for instance if the bitcoind HTTP server work queue // depth is exceeded), return the raw HTTP error so users can match against it. - let resp = req.send()?; + let resp = req.send_async().await?; match resp.json() { Ok(json) => Ok(json), Err(bitreq_err) => @@ -78,18 +78,24 @@ impl BitreqHttpTransport { } impl Transport for BitreqHttpTransport { - fn send_request(&self, req: Request) -> Result { - Ok(self.request(req)?) + fn send_request<'a>( + &'a self, + req: Request<'a>, + ) -> BoxFuture<'a, Result> { + Box::pin(async move { Ok(self.request(req).await?) }) } - fn send_batch(&self, reqs: &[Request]) -> Result, crate::Error> { - Ok(self.request(reqs)?) + fn send_batch<'a>( + &'a self, + reqs: &'a [Request<'a>], + ) -> BoxFuture<'a, Result, crate::Error>> { + Box::pin(async move { Ok(self.request(reqs).await?) }) } fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.url) } } -/// Builder for simple bitcoind [`BitreqHttpTransport`]. +/// Builder for async bitcoind [`BitreqHttpTransport`]. #[derive(Clone, Debug)] pub struct Builder { tp: BitreqHttpTransport, @@ -130,7 +136,7 @@ impl Builder { /// # Examples /// /// ```no_run - /// # use jsonrpc::bitreq_http_sync::BitreqHttpTransport; + /// # use jsonrpc::bitreq_http_async::BitreqHttpTransport; /// # use std::fs::{self, File}; /// # use std::path::Path; /// # let cookie_file = Path::new("~/.bitcoind/.cookie"); @@ -226,7 +232,7 @@ impl From for crate::Error { #[cfg(test)] mod tests { use super::*; - use crate::client_sync::Client; + use crate::client_async::Client; #[test] fn construct() { diff --git a/jsonrpc/src/http/mod.rs b/jsonrpc/src/http/mod.rs index efdaf890b..dc2df4b5c 100644 --- a/jsonrpc/src/http/mod.rs +++ b/jsonrpc/src/http/mod.rs @@ -6,6 +6,9 @@ pub mod simple_http; #[cfg(feature = "bitreq_http_sync")] pub mod bitreq_http_sync; +#[cfg(feature = "bitreq_http_async")] +pub mod bitreq_http_async; + /// The default TCP port to use for connections. /// Set to 8332, the default RPC port for bitcoind. pub const DEFAULT_PORT: u16 = 8332; diff --git a/jsonrpc/src/lib.rs b/jsonrpc/src/lib.rs index 7642ab018..b38b899c8 100644 --- a/jsonrpc/src/lib.rs +++ b/jsonrpc/src/lib.rs @@ -20,10 +20,14 @@ pub extern crate base64; #[cfg(feature = "bitreq")] pub extern crate bitreq; +#[cfg(feature = "client_async")] +pub mod client_async; pub mod client_sync; pub mod error; pub mod http; +#[cfg(feature = "bitreq_http_async")] +pub use http::bitreq_http_async; #[cfg(feature = "bitreq_http_sync")] pub use http::bitreq_http_sync; #[cfg(feature = "simple_http")] From 681989fc3228555fa986cfe21857ce748b44258e Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Fri, 6 Feb 2026 17:00:02 +0000 Subject: [PATCH 06/14] Set bitreq_http feature for sync only In preparation for adding bitreq_http_async feature to jsonrpc move the sync version to the client-sync feature so it is not always on with jsonrpc. --- client/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/Cargo.toml b/client/Cargo.toml index 922e158a1..0d0e5653a 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] # Enable this feature to get a blocking JSON-RPC client. -client-sync = ["jsonrpc"] +client-sync = ["jsonrpc", "jsonrpc/bitreq_http_sync"] [dependencies] bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] } @@ -27,6 +27,6 @@ serde = { version = "1.0.103", default-features = false, features = [ "derive", serde_json = { version = "1.0.117" } types = { package = "corepc-types", version = "0.11.0", path = "../types", default-features = false, features = ["std"] } -jsonrpc = { version = "0.19.0", path = "../jsonrpc", features = ["bitreq_http_sync"], optional = true } +jsonrpc = { version = "0.19.0", path = "../jsonrpc", optional = true } [dev-dependencies] From 4ff8dad08f2cf9d82fa3a95118a877b7d78146b9 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Fri, 6 Feb 2026 17:03:10 +0000 Subject: [PATCH 07/14] Copy client_sync to client_async Create a new folder for the upcoming async client and copy in the existing client_sync code. Code copy only to make the next patch easier to review. --- client/src/client_async/error.rs | 112 ++++++++++++++++++ client/src/client_async/mod.rs | 190 +++++++++++++++++++++++++++++++ 2 files changed, 302 insertions(+) create mode 100644 client/src/client_async/error.rs create mode 100644 client/src/client_async/mod.rs diff --git a/client/src/client_async/error.rs b/client/src/client_async/error.rs new file mode 100644 index 000000000..331a81eeb --- /dev/null +++ b/client/src/client_async/error.rs @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: CC0-1.0 + +use std::{error, fmt, io}; + +use bitcoin::hex; + +/// The error type for errors produced in this library. +#[derive(Debug)] +pub enum Error { + JsonRpc(jsonrpc::error::Error), + HexToArray(hex::HexToArrayError), + HexToBytes(hex::HexToBytesError), + Json(serde_json::error::Error), + BitcoinSerialization(bitcoin::consensus::encode::FromHexError), + Io(io::Error), + InvalidCookieFile, + /// The JSON result had an unexpected structure. + UnexpectedStructure, + /// The daemon returned an error string. + Returned(String), + /// The server version did not match what was expected. + ServerVersion(UnexpectedServerVersionError), + /// Missing user/password. + MissingUserPassword, +} + +impl From for Error { + fn from(e: jsonrpc::error::Error) -> Error { Error::JsonRpc(e) } +} + +impl From for Error { + fn from(e: hex::HexToArrayError) -> Self { Self::HexToArray(e) } +} + +impl From for Error { + fn from(e: hex::HexToBytesError) -> Self { Self::HexToBytes(e) } +} + +impl From for Error { + fn from(e: serde_json::error::Error) -> Error { Error::Json(e) } +} + +impl From for Error { + fn from(e: bitcoin::consensus::encode::FromHexError) -> Error { Error::BitcoinSerialization(e) } +} + +impl From for Error { + fn from(e: io::Error) -> Error { Error::Io(e) } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Error::*; + + match *self { + JsonRpc(ref e) => write!(f, "JSON-RPC error: {}", e), + HexToArray(ref e) => write!(f, "hex to array decode error: {}", e), + HexToBytes(ref e) => write!(f, "hex to bytes decode error: {}", e), + Json(ref e) => write!(f, "JSON error: {}", e), + BitcoinSerialization(ref e) => write!(f, "Bitcoin serialization error: {}", e), + Io(ref e) => write!(f, "I/O error: {}", e), + InvalidCookieFile => write!(f, "invalid cookie file"), + UnexpectedStructure => write!(f, "the JSON result had an unexpected structure"), + Returned(ref s) => write!(f, "the daemon returned an error string: {}", s), + ServerVersion(ref e) => write!(f, "server version: {}", e), + MissingUserPassword => write!(f, "missing user and/or password"), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use Error::*; + + match *self { + JsonRpc(ref e) => Some(e), + HexToArray(ref e) => Some(e), + HexToBytes(ref e) => Some(e), + Json(ref e) => Some(e), + BitcoinSerialization(ref e) => Some(e), + Io(ref e) => Some(e), + ServerVersion(ref e) => Some(e), + InvalidCookieFile | UnexpectedStructure | Returned(_) | MissingUserPassword => None, + } + } +} + +/// Error returned when RPC client expects a different version than bitcoind reports. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnexpectedServerVersionError { + /// Version from server. + pub got: usize, + /// Expected server version. + pub expected: Vec, +} + +impl fmt::Display for UnexpectedServerVersionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut expected = String::new(); + for version in &self.expected { + let v = format!(" {} ", version); + expected.push_str(&v); + } + write!(f, "unexpected bitcoind version, got: {} expected one of: {}", self.got, expected) + } +} + +impl error::Error for UnexpectedServerVersionError {} + +impl From for Error { + fn from(e: UnexpectedServerVersionError) -> Self { Self::ServerVersion(e) } +} diff --git a/client/src/client_async/mod.rs b/client/src/client_async/mod.rs new file mode 100644 index 000000000..3d9a505ad --- /dev/null +++ b/client/src/client_async/mod.rs @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! JSON-RPC clients for testing against specific versions of Bitcoin Core. + +mod error; +pub mod v17; +pub mod v18; +pub mod v19; +pub mod v20; +pub mod v21; +pub mod v22; +pub mod v23; +pub mod v24; +pub mod v25; +pub mod v26; +pub mod v27; +pub mod v28; +pub mod v29; +pub mod v30; + +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::PathBuf; + +pub use crate::client_sync::error::Error; + +/// Crate-specific Result type. +/// +/// Shorthand for `std::result::Result` with our crate-specific [`Error`] type. +pub type Result = std::result::Result; + +/// The different authentication methods for the client. +#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +pub enum Auth { + None, + UserPass(String, String), + CookieFile(PathBuf), +} + +impl Auth { + /// Convert into the arguments that jsonrpc::Client needs. + pub fn get_user_pass(self) -> Result<(Option, Option)> { + match self { + Auth::None => Ok((None, None)), + Auth::UserPass(u, p) => Ok((Some(u), Some(p))), + Auth::CookieFile(path) => { + let line = BufReader::new(File::open(path)?) + .lines() + .next() + .ok_or(Error::InvalidCookieFile)??; + let colon = line.find(':').ok_or(Error::InvalidCookieFile)?; + Ok((Some(line[..colon].into()), Some(line[colon + 1..].into()))) + } + } + } +} + +/// Defines a `jsonrpc::Client` using `bitreq`. +#[macro_export] +macro_rules! define_jsonrpc_bitreq_client { + ($version:literal) => { + use std::fmt; + + use $crate::client_sync::{log_response, Auth, Result}; + use $crate::client_sync::error::Error; + + /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. + pub struct Client { + inner: jsonrpc::client_sync::Client, + } + + impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result { + write!( + f, + "corepc_client::client_sync::{}::Client({:?})", $version, self.inner + ) + } + } + + impl Client { + /// Creates a client to a bitcoind JSON-RPC server without authentication. + pub fn new(url: &str) -> Self { + let transport = jsonrpc::http::bitreq_http_sync::Builder::new() + .url(url) + .expect("jsonrpc v0.19, this function does not error") + .timeout(std::time::Duration::from_secs(60)) + .build(); + let inner = jsonrpc::client_sync::Client::with_transport(transport); + + Self { inner } + } + + /// Creates a client to a bitcoind JSON-RPC server with authentication. + pub fn new_with_auth(url: &str, auth: Auth) -> Result { + if matches!(auth, Auth::None) { + return Err(Error::MissingUserPassword); + } + let (user, pass) = auth.get_user_pass()?; + + let transport = jsonrpc::http::bitreq_http_sync::Builder::new() + .url(url) + .expect("jsonrpc v0.19, this function does not error") + .timeout(std::time::Duration::from_secs(60)) + .basic_auth(user.unwrap(), pass) + .build(); + let inner = jsonrpc::client_sync::Client::with_transport(transport); + + Ok(Self { inner }) + } + + /// Call an RPC `method` with given `args` list. + pub fn call serde::de::Deserialize<'a>>( + &self, + method: &str, + args: &[serde_json::Value], + ) -> Result { + let raw = serde_json::value::to_raw_value(args)?; + let req = self.inner.build_request(&method, Some(&*raw)); + if log::log_enabled!(log::Level::Debug) { + log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args)); + } + + let resp = self.inner.send_request(req).map_err(Error::from); + log_response(method, &resp); + Ok(resp?.result()?) + } + } + } +} + +/// Implements the `check_expected_server_version()` on `Client`. +/// +/// Requires `Client` to be in scope and implement `server_version()`. +/// See and/or use `impl_client_v17__getnetworkinfo`. +/// +/// # Parameters +/// +/// - `$expected_versions`: An vector of expected server versions e.g., `[230100, 230200]`. +#[macro_export] +macro_rules! impl_client_check_expected_server_version { + ($expected_versions:expr) => { + impl Client { + /// Checks that the JSON-RPC endpoint is for a `bitcoind` instance with the expected version. + pub fn check_expected_server_version(&self) -> Result<()> { + let server_version = self.server_version()?; + if !$expected_versions.contains(&server_version) { + return Err($crate::client_sync::error::UnexpectedServerVersionError { + got: server_version, + expected: $expected_versions.to_vec(), + })?; + } + Ok(()) + } + } + }; +} + +/// Shorthand for converting a variable into a `serde_json::Value`. +fn into_json(val: T) -> Result +where + T: serde::ser::Serialize, +{ + Ok(serde_json::to_value(val)?) +} + +/// Helper to log an RPC response. +fn log_response(method: &str, resp: &Result) { + use log::Level::{Debug, Trace, Warn}; + + if log::log_enabled!(Warn) || log::log_enabled!(Debug) || log::log_enabled!(Trace) { + match resp { + Err(ref e) => + if log::log_enabled!(Debug) { + log::debug!(target: "corepc", "error: {}: {:?}", method, e); + }, + Ok(ref resp) => + if let Some(ref e) = resp.error { + if log::log_enabled!(Debug) { + log::debug!(target: "corepc", "response error for {}: {:?}", method, e); + } + } else if log::log_enabled!(Trace) { + let def = + serde_json::value::to_raw_value(&serde_json::value::Value::Null).unwrap(); + let result = resp.result.as_ref().unwrap_or(&def); + log::trace!(target: "corepc", "response for {}: {}", method, result); + }, + } + } +} From 0d5184d6b163e749621f10a34e012071c2c4db92 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 5 Feb 2026 09:19:26 +0000 Subject: [PATCH 08/14] Make corepc-client async Edit the copy of the sync client created in the previous commit to be async. Update the readme and cargo.toml files. Add only small set of RPCs. --- client/Cargo.toml | 2 + client/README.md | 13 +- client/src/client_async/mod.rs | 41 +++---- client/src/client_async/v17/blockchain.rs | 114 ++++++++++++++++++ client/src/client_async/v17/mod.rs | 28 +++++ client/src/client_async/v17/network.rs | 28 +++++ .../src/client_async/v17/raw_transactions.rs | 32 +++++ client/src/client_async/v18/mod.rs | 24 ++++ client/src/client_async/v19/blockchain.rs | 22 ++++ client/src/client_async/v19/mod.rs | 27 +++++ client/src/client_async/v20/mod.rs | 25 ++++ client/src/client_async/v21/blockchain.rs | 31 +++++ client/src/client_async/v21/mod.rs | 27 +++++ client/src/client_async/v22/mod.rs | 25 ++++ client/src/client_async/v23/mod.rs | 25 ++++ client/src/client_async/v24/mod.rs | 25 ++++ client/src/client_async/v25/mod.rs | 25 ++++ client/src/client_async/v26/mod.rs | 25 ++++ client/src/client_async/v27/mod.rs | 25 ++++ client/src/client_async/v28/mod.rs | 25 ++++ client/src/client_async/v29/mod.rs | 25 ++++ client/src/client_async/v30/mod.rs | 25 ++++ client/src/lib.rs | 3 + 23 files changed, 619 insertions(+), 23 deletions(-) create mode 100644 client/src/client_async/v17/blockchain.rs create mode 100644 client/src/client_async/v17/mod.rs create mode 100644 client/src/client_async/v17/network.rs create mode 100644 client/src/client_async/v17/raw_transactions.rs create mode 100644 client/src/client_async/v18/mod.rs create mode 100644 client/src/client_async/v19/blockchain.rs create mode 100644 client/src/client_async/v19/mod.rs create mode 100644 client/src/client_async/v20/mod.rs create mode 100644 client/src/client_async/v21/blockchain.rs create mode 100644 client/src/client_async/v21/mod.rs create mode 100644 client/src/client_async/v22/mod.rs create mode 100644 client/src/client_async/v23/mod.rs create mode 100644 client/src/client_async/v24/mod.rs create mode 100644 client/src/client_async/v25/mod.rs create mode 100644 client/src/client_async/v26/mod.rs create mode 100644 client/src/client_async/v27/mod.rs create mode 100644 client/src/client_async/v28/mod.rs create mode 100644 client/src/client_async/v29/mod.rs create mode 100644 client/src/client_async/v30/mod.rs diff --git a/client/Cargo.toml b/client/Cargo.toml index 0d0e5653a..16b6519aa 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -19,6 +19,8 @@ rustdoc-args = ["--cfg", "docsrs"] [features] # Enable this feature to get a blocking JSON-RPC client. client-sync = ["jsonrpc", "jsonrpc/bitreq_http_sync"] +# Enable this feature to get an async JSON-RPC client. +client-async = ["jsonrpc", "jsonrpc/bitreq_http_async", "jsonrpc/client_async"] [dependencies] bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] } diff --git a/client/README.md b/client/README.md index 0c09dedd8..a842b1555 100644 --- a/client/README.md +++ b/client/README.md @@ -1,7 +1,16 @@ # corepc-client -Rust client for the Bitcoin Core daemon's JSON-RPC API. Currently this -is only a blocking client and is intended to be used in integration testing. +Rust client for the Bitcoin Core daemon's JSON-RPC API. + +This crate provides: + +- A blocking client intended for integration testing (`client-sync`). +- An async client intended for production (`client-async`). + +## Features + +- `client-sync`: Blocking JSON-RPC client. +- `client-async`: Async JSON-RPC client. ## Minimum Supported Rust Version (MSRV) diff --git a/client/src/client_async/mod.rs b/client/src/client_async/mod.rs index 3d9a505ad..5e87cd043 100644 --- a/client/src/client_async/mod.rs +++ b/client/src/client_async/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: CC0-1.0 -//! JSON-RPC clients for testing against specific versions of Bitcoin Core. +//! Async JSON-RPC clients for specific versions of Bitcoin Core. mod error; pub mod v17; @@ -22,7 +22,7 @@ use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -pub use crate::client_sync::error::Error; +pub use crate::client_async::error::Error; /// Crate-specific Result type. /// @@ -55,25 +55,25 @@ impl Auth { } } -/// Defines a `jsonrpc::Client` using `bitreq`. +/// Defines a async `jsonrpc::Client` using `bitreq`. #[macro_export] -macro_rules! define_jsonrpc_bitreq_client { +macro_rules! define_jsonrpc_bitreq_async_client { ($version:literal) => { use std::fmt; + use $crate::client_async::{log_response, Auth, Result}; + use $crate::client_async::error::Error; - use $crate::client_sync::{log_response, Auth, Result}; - use $crate::client_sync::error::Error; - - /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. + /// Client implements an async JSON-RPC client for the Bitcoin Core daemon or compatible APIs. pub struct Client { - inner: jsonrpc::client_sync::Client, + inner: jsonrpc::client_async::Client, } impl fmt::Debug for Client { fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result { write!( f, - "corepc_client::client_sync::{}::Client({:?})", $version, self.inner + "corepc_client::client_async::{}::Client({:?})", + $version, self.inner ) } } @@ -81,12 +81,12 @@ macro_rules! define_jsonrpc_bitreq_client { impl Client { /// Creates a client to a bitcoind JSON-RPC server without authentication. pub fn new(url: &str) -> Self { - let transport = jsonrpc::http::bitreq_http_sync::Builder::new() + let transport = jsonrpc::bitreq_http_async::Builder::new() .url(url) .expect("jsonrpc v0.19, this function does not error") .timeout(std::time::Duration::from_secs(60)) .build(); - let inner = jsonrpc::client_sync::Client::with_transport(transport); + let inner = jsonrpc::client_async::Client::with_transport(transport); Self { inner } } @@ -97,20 +97,19 @@ macro_rules! define_jsonrpc_bitreq_client { return Err(Error::MissingUserPassword); } let (user, pass) = auth.get_user_pass()?; - - let transport = jsonrpc::http::bitreq_http_sync::Builder::new() + let transport = jsonrpc::bitreq_http_async::Builder::new() .url(url) .expect("jsonrpc v0.19, this function does not error") .timeout(std::time::Duration::from_secs(60)) .basic_auth(user.unwrap(), pass) .build(); - let inner = jsonrpc::client_sync::Client::with_transport(transport); + let inner = jsonrpc::client_async::Client::with_transport(transport); Ok(Self { inner }) } /// Call an RPC `method` with given `args` list. - pub fn call serde::de::Deserialize<'a>>( + pub async fn call serde::de::Deserialize<'a>>( &self, method: &str, args: &[serde_json::Value], @@ -121,7 +120,7 @@ macro_rules! define_jsonrpc_bitreq_client { log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args)); } - let resp = self.inner.send_request(req).map_err(Error::from); + let resp = self.inner.send_request(req).await.map_err(Error::from); log_response(method, &resp); Ok(resp?.result()?) } @@ -138,14 +137,14 @@ macro_rules! define_jsonrpc_bitreq_client { /// /// - `$expected_versions`: An vector of expected server versions e.g., `[230100, 230200]`. #[macro_export] -macro_rules! impl_client_check_expected_server_version { +macro_rules! impl_async_client_check_expected_server_version { ($expected_versions:expr) => { impl Client { /// Checks that the JSON-RPC endpoint is for a `bitcoind` instance with the expected version. - pub fn check_expected_server_version(&self) -> Result<()> { - let server_version = self.server_version()?; + pub async fn check_expected_server_version(&self) -> Result<()> { + let server_version = self.server_version().await?; if !$expected_versions.contains(&server_version) { - return Err($crate::client_sync::error::UnexpectedServerVersionError { + return Err($crate::client_async::error::UnexpectedServerVersionError { got: server_version, expected: $expected_versions.to_vec(), })?; diff --git a/client/src/client_async/v17/blockchain.rs b/client/src/client_async/v17/blockchain.rs new file mode 100644 index 000000000..ac96ff320 --- /dev/null +++ b/client/src/client_async/v17/blockchain.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing async JSON-RPC methods on a client. +//! +//! Specifically this is methods found under the `== Blockchain ==` section of the +//! API docs of Bitcoin Core `v0.17`. +//! +//! All macros require `Client` to be in scope. +//! +//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. + +/// Implements Bitcoin Core JSON-RPC API method `getblock`. +#[macro_export] +macro_rules! impl_async_client_v17__get_block { + () => { + impl Client { + /// Gets a block by blockhash. + pub async fn get_block(&self, hash: BlockHash) -> Result { + let json = self.get_block_verbose_zero(hash).await?; + Ok(json.block()?) + } + + /// Gets a block by blockhash with verbose set to 0. + pub async fn get_block_verbose_zero( + &self, + hash: BlockHash, + ) -> Result { + self.call("getblock", &[into_json(hash)?, 0.into()]).await + } + + /// Gets a block by blockhash with verbose set to 1. + pub async fn get_block_verbose_one( + &self, + hash: BlockHash, + ) -> Result { + self.call("getblock", &[into_json(hash)?, 1.into()]).await + } + + /// Alias for getblock verbosity 1, matching bitcoincore-rpc naming. + pub async fn get_block_info(&self, hash: BlockHash) -> Result { + self.get_block_verbose_one(hash).await + } + } + }; +} + +/// Implements Bitcoin Core JSON-RPC API method `getblockcount`. +#[macro_export] +macro_rules! impl_async_client_v17__get_block_count { + () => { + impl Client { + pub async fn get_block_count(&self) -> Result { + self.call("getblockcount", &[]).await + } + } + }; +} + +/// Implements Bitcoin Core JSON-RPC API method `getblockhash`. +#[macro_export] +macro_rules! impl_async_client_v17__get_block_hash { + () => { + impl Client { + pub async fn get_block_hash(&self, height: u64) -> Result { + self.call("getblockhash", &[into_json(height)?]).await + } + } + }; +} + +/// Implements Bitcoin Core JSON-RPC API method `getblockheader`. +#[macro_export] +macro_rules! impl_async_client_v17__get_block_header { + () => { + impl Client { + pub async fn get_block_header(&self, hash: &BlockHash) -> Result { + self.call("getblockheader", &[into_json(hash)?, into_json(false)?]).await + } + + // This is the same as calling getblockheader with verbose==true. + pub async fn get_block_header_verbose( + &self, + hash: &BlockHash, + ) -> Result { + self.call("getblockheader", &[into_json(hash)?]).await + } + + /// Alias for getblockheader with verbose true. + pub async fn get_block_header_info( + &self, + hash: &BlockHash, + ) -> Result { + self.get_block_header_verbose(hash).await + } + } + }; +} + +/// Implements Bitcoin Core JSON-RPC API method `getrawmempool`. +#[macro_export] +macro_rules! impl_async_client_v17__get_raw_mempool { + () => { + impl Client { + pub async fn get_raw_mempool(&self) -> Result { + // Equivalent to self.call("getrawmempool", &[into_json(false)?]) + self.call("getrawmempool", &[]).await + } + + pub async fn get_raw_mempool_verbose(&self) -> Result { + self.call("getrawmempool", &[into_json(true)?]).await + } + } + }; +} diff --git a/client/src/client_async/v17/mod.rs b/client/src/client_async/v17/mod.rs new file mode 100644 index 000000000..c81bb3f11 --- /dev/null +++ b/client/src/client_async/v17/mod.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v0.17`. + +pub mod blockchain; +pub mod network; +pub mod raw_transactions; + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v17::*; + +crate::define_jsonrpc_bitreq_async_client!("v17"); +crate::impl_async_client_check_expected_server_version!({ [170200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v17__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v17/network.rs b/client/src/client_async/v17/network.rs new file mode 100644 index 000000000..3c6e78b14 --- /dev/null +++ b/client/src/client_async/v17/network.rs @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing async JSON-RPC methods on a client. +//! +//! Specifically this is methods found under the `== Network ==` section of the +//! API docs of Bitcoin Core `v0.17`. +//! +//! All macros require `Client` to be in scope. +//! +//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. + +/// Implements Bitcoin Core JSON-RPC API method `getnetworkinfo`. +#[macro_export] +macro_rules! impl_async_client_v17__get_network_info { + () => { + impl Client { + /// Returns the server version field of `GetNetworkInfo`. + pub async fn server_version(&self) -> Result { + let info = self.get_network_info().await?; + Ok(info.version) + } + + pub async fn get_network_info(&self) -> Result { + self.call("getnetworkinfo", &[]).await + } + } + }; +} diff --git a/client/src/client_async/v17/raw_transactions.rs b/client/src/client_async/v17/raw_transactions.rs new file mode 100644 index 000000000..dea957ec7 --- /dev/null +++ b/client/src/client_async/v17/raw_transactions.rs @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing async JSON-RPC methods on a client. +//! +//! Specifically this is methods found under the `== Rawtransactions ==` section of the +//! API docs of Bitcoin Core `v0.17`. +//! +//! All macros require `Client` to be in scope. +//! +//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. + +/// Implements Bitcoin Core JSON-RPC API method `getrawtransaction`. +#[macro_export] +macro_rules! impl_async_client_v17__get_raw_transaction { + () => { + impl Client { + pub async fn get_raw_transaction( + &self, + txid: bitcoin::Txid, + ) -> Result { + self.call("getrawtransaction", &[into_json(&txid)?, false.into()]).await + } + + pub async fn get_raw_transaction_verbose( + &self, + txid: Txid, + ) -> Result { + self.call("getrawtransaction", &[into_json(&txid)?, true.into()]).await + } + } + }; +} diff --git a/client/src/client_async/v18/mod.rs b/client/src/client_async/v18/mod.rs new file mode 100644 index 000000000..896410fa6 --- /dev/null +++ b/client/src/client_async/v18/mod.rs @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v0.18`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v18::*; + +crate::define_jsonrpc_bitreq_async_client!("v18"); +crate::impl_async_client_check_expected_server_version!({ [180100] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v17__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v19/blockchain.rs b/client/src/client_async/v19/blockchain.rs new file mode 100644 index 000000000..d3226b928 --- /dev/null +++ b/client/src/client_async/v19/blockchain.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing async JSON-RPC methods on a client. +//! +//! Specifically this is methods found under the `== Blockchain ==` section of the +//! API docs of Bitcoin Core `v0.19`. +//! +//! All macros require `Client` to be in scope. +//! +//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. + +/// Implements Bitcoin Core JSON-RPC API method `getblockfilter`. +#[macro_export] +macro_rules! impl_async_client_v19__get_block_filter { + () => { + impl Client { + pub async fn get_block_filter(&self, block: BlockHash) -> Result { + self.call("getblockfilter", &[into_json(block)?]).await + } + } + }; +} diff --git a/client/src/client_async/v19/mod.rs b/client/src/client_async/v19/mod.rs new file mode 100644 index 000000000..ace91663d --- /dev/null +++ b/client/src/client_async/v19/mod.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v0.19`. + +pub mod blockchain; + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v19::*; + +crate::define_jsonrpc_bitreq_async_client!("v19"); +crate::impl_async_client_check_expected_server_version!({ [190100] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v17__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v20/mod.rs b/client/src/client_async/v20/mod.rs new file mode 100644 index 000000000..f89e8510f --- /dev/null +++ b/client/src/client_async/v20/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v0.20`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v20::*; + +crate::define_jsonrpc_bitreq_async_client!("v20"); +crate::impl_async_client_check_expected_server_version!({ [200200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v17__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v21/blockchain.rs b/client/src/client_async/v21/blockchain.rs new file mode 100644 index 000000000..902fcdc9b --- /dev/null +++ b/client/src/client_async/v21/blockchain.rs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Macros for implementing async JSON-RPC methods on a client. +//! +//! Specifically this is methods found under the `== Blockchain ==` section of the +//! API docs of Bitcoin Core `v0.21`. +//! +//! All macros require `Client` to be in scope. +//! +//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. + +/// Implements Bitcoin Core JSON-RPC API method `getrawmempool`. +#[macro_export] +macro_rules! impl_async_client_v21__get_raw_mempool { + () => { + impl Client { + pub async fn get_raw_mempool(&self) -> Result { + // Equivalent to self.call("getrawmempool", &[into_json(false)?]) + self.call("getrawmempool", &[]).await + } + + pub async fn get_raw_mempool_verbose(&self) -> Result { + self.call("getrawmempool", &[into_json(true)?]).await + } + + pub async fn get_raw_mempool_sequence(&self) -> Result { + self.call("getrawmempool", &[into_json(false)?, into_json(true)?]).await + } + } + }; +} diff --git a/client/src/client_async/v21/mod.rs b/client/src/client_async/v21/mod.rs new file mode 100644 index 000000000..8837233e6 --- /dev/null +++ b/client/src/client_async/v21/mod.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v0.21`. + +pub mod blockchain; + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v21::*; + +crate::define_jsonrpc_bitreq_async_client!("v21"); +crate::impl_async_client_check_expected_server_version!({ [210200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v22/mod.rs b/client/src/client_async/v22/mod.rs new file mode 100644 index 000000000..dcf0e4e56 --- /dev/null +++ b/client/src/client_async/v22/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v22`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v22::*; + +crate::define_jsonrpc_bitreq_async_client!("v22"); +crate::impl_async_client_check_expected_server_version!({ [220100] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v23/mod.rs b/client/src/client_async/v23/mod.rs new file mode 100644 index 000000000..1d56ef9d0 --- /dev/null +++ b/client/src/client_async/v23/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v23`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v23::*; + +crate::define_jsonrpc_bitreq_async_client!("v23"); +crate::impl_async_client_check_expected_server_version!({ [230200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v24/mod.rs b/client/src/client_async/v24/mod.rs new file mode 100644 index 000000000..38e4dc807 --- /dev/null +++ b/client/src/client_async/v24/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v24`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v24::*; + +crate::define_jsonrpc_bitreq_async_client!("v24"); +crate::impl_async_client_check_expected_server_version!({ [240200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v25/mod.rs b/client/src/client_async/v25/mod.rs new file mode 100644 index 000000000..fc5abd24b --- /dev/null +++ b/client/src/client_async/v25/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v25`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v25::*; + +crate::define_jsonrpc_bitreq_async_client!("v25"); +crate::impl_async_client_check_expected_server_version!({ [250200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v26/mod.rs b/client/src/client_async/v26/mod.rs new file mode 100644 index 000000000..5b2e9d413 --- /dev/null +++ b/client/src/client_async/v26/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v26`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v26::*; + +crate::define_jsonrpc_bitreq_async_client!("v26"); +crate::impl_async_client_check_expected_server_version!({ [260000, 260100, 260200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v27/mod.rs b/client/src/client_async/v27/mod.rs new file mode 100644 index 000000000..61bc2bd9b --- /dev/null +++ b/client/src/client_async/v27/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v27`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v27::*; + +crate::define_jsonrpc_bitreq_async_client!("v27"); +crate::impl_async_client_check_expected_server_version!({ [270000, 270100, 270200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v28/mod.rs b/client/src/client_async/v28/mod.rs new file mode 100644 index 000000000..7f5f891b2 --- /dev/null +++ b/client/src/client_async/v28/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v28`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v28::*; + +crate::define_jsonrpc_bitreq_async_client!("v28"); +crate::impl_async_client_check_expected_server_version!({ [280000, 280100, 280200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v29/mod.rs b/client/src/client_async/v29/mod.rs new file mode 100644 index 000000000..3dad3d0f9 --- /dev/null +++ b/client/src/client_async/v29/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v29`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v29::*; + +crate::define_jsonrpc_bitreq_async_client!("v29"); +crate::impl_async_client_check_expected_server_version!({ [290000] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v30/mod.rs b/client/src/client_async/v30/mod.rs new file mode 100644 index 000000000..7138173b0 --- /dev/null +++ b/client/src/client_async/v30/mod.rs @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! An async JSON-RPC client for Bitcoin Core `v30`. + +use bitcoin::{Block, BlockHash, Txid}; + +use crate::client_async::into_json; +use crate::types::v30::*; + +crate::define_jsonrpc_bitreq_async_client!("v30"); +crate::impl_async_client_check_expected_server_version!({ [300000, 300100, 300200] }); + +// == Blockchain == +crate::impl_async_client_v17__get_block!(); +crate::impl_async_client_v17__get_block_count!(); +crate::impl_async_client_v19__get_block_filter!(); +crate::impl_async_client_v17__get_block_hash!(); +crate::impl_async_client_v17__get_block_header!(); +crate::impl_async_client_v21__get_raw_mempool!(); + +// == Network == +crate::impl_async_client_v17__get_network_info!(); + +// == Rawtransactions == +crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/lib.rs b/client/src/lib.rs index 4b34271e3..01e7c7429 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -11,3 +11,6 @@ pub extern crate types; #[cfg(feature = "client-sync")] #[macro_use] pub mod client_sync; + +#[cfg(feature = "client-async")] +pub mod client_async; From d1f01817db6b4d0b39d8c7c9424522b478577532 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Mon, 9 Feb 2026 16:25:32 +0000 Subject: [PATCH 09/14] Vibe code some async tests Add some vibe coded tests for the async client to check that everything is functional. Tests are for development purposes only to catch simple errors like feature gates etc. and have not been reviewed. --- integration_test/Cargo.toml | 2 + integration_test/tests/async_client.rs | 223 +++++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 integration_test/tests/async_client.rs diff --git a/integration_test/Cargo.toml b/integration_test/Cargo.toml index 05421c435..0a3ec8c9d 100644 --- a/integration_test/Cargo.toml +++ b/integration_test/Cargo.toml @@ -62,6 +62,7 @@ TODO = [] # This is a dirty hack while writing the tests. [dependencies] bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] } +corepc-client = { version = "0.11.0", path = "../client", default-features = false, features = ["client-async"] } env_logger = "0.9.0" node = { package = "corepc-node", version = "0.11.0", path = "../node", default-features = false } rand = "0.8.5" @@ -69,6 +70,7 @@ rand = "0.8.5" types = { package = "corepc-types", version = "0.11.0", path = "../types", features = ["serde-deny-unknown-fields"] } [dev-dependencies] +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } [patch.crates-io.corepc-client] diff --git a/integration_test/tests/async_client.rs b/integration_test/tests/async_client.rs new file mode 100644 index 000000000..d289e6c79 --- /dev/null +++ b/integration_test/tests/async_client.rs @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Tests for the async client. + +#![allow(non_snake_case)] // Test names intentionally use double underscore. + +#[cfg(all(feature = "v18_and_below", not(feature = "v17")))] +use corepc_client::client_async::v18 as async_client_v18; +#[cfg(not(feature = "v18_and_below"))] +use corepc_client::client_async::v19 as async_client_v19; +#[cfg(not(feature = "v20_and_below"))] +use corepc_client::client_async::v21 as async_client_v21; +#[cfg(all(not(feature = "v22_and_below"), feature = "v23_and_below"))] +use corepc_client::client_async::v23 as async_client_v23; +#[cfg(not(feature = "v23_and_below"))] +use corepc_client::client_async::v24 as async_client_v24; +#[cfg(not(feature = "v28_and_below"))] +use corepc_client::client_async::v29 as async_client_v29; +use corepc_client::client_async::{v17 as async_client, Auth}; +use integration_test::{Node, NodeExt as _, Wallet}; + +fn async_client_for(node: &Node) -> async_client::Client { + async_client::Client::new_with_auth(&node.rpc_url(), auth_for(node)).expect("async client") +} + +#[tokio::test] +async fn async_client__get_block_count_and_hash() { + let node = Node::with_wallet(Wallet::None, &[]); + let client = async_client_for(&node); + + let count = client.get_block_count().await.expect("getblockcount"); + assert_eq!(count.0, 0); + + let json = client.get_block_hash(0).await.expect("getblockhash"); + let model = json.into_model().expect("getblockhash model"); + let expected = node.client.best_block_hash().expect("best_block_hash"); + assert_eq!(model.0, expected); +} + +#[tokio::test] +async fn async_client__get_block_variants() { + let node = Node::with_wallet(Wallet::None, &[]); + let client = async_client_for(&node); + + let best_hash = node.client.best_block_hash().expect("best_block_hash"); + + let block = client.get_block(best_hash).await.expect("getblock"); + assert_eq!(block.block_hash(), best_hash); + + let block_v0 = client.get_block_verbose_zero(best_hash).await.expect("getblock verbose=0"); + let block_from_hex = block_v0.block().expect("getblock verbose=0 decode"); + assert_eq!(block_from_hex.block_hash(), best_hash); + + #[cfg(feature = "v28_and_below")] + { + let block_v1 = client.get_block_verbose_one(best_hash).await.expect("getblock verbose=1"); + assert_eq!(block_v1.hash, best_hash.to_string()); + + let block_info = client.get_block_info(best_hash).await.expect("getblock info"); + assert_eq!(block_info.hash, best_hash.to_string()); + } + + #[cfg(not(feature = "v28_and_below"))] + { + let client = async_client_v29::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v29"); + let block_v1 = + client.get_block_verbose_one(best_hash).await.expect("getblock verbose=1 v29"); + assert_eq!(block_v1.hash, best_hash.to_string()); + + let block_info = client.get_block_info(best_hash).await.expect("getblock info v29"); + assert_eq!(block_info.hash, best_hash.to_string()); + } +} + +#[tokio::test] +async fn async_client__get_block_header_variants() { + let node = Node::with_wallet(Wallet::None, &[]); + let client = async_client_for(&node); + + let best_hash = node.client.best_block_hash().expect("best_block_hash"); + let header = client.get_block_header(&best_hash).await.expect("getblockheader"); + assert!(!header.0.is_empty()); + + #[cfg(feature = "v28_and_below")] + { + let header_verbose = + client.get_block_header_verbose(&best_hash).await.expect("getblockheader verbose"); + + assert_eq!(header_verbose.hash, best_hash.to_string()); + assert_eq!(header_verbose.height, 0); + + let header_info = + client.get_block_header_info(&best_hash).await.expect("getblockheader info"); + + assert_eq!(header_info.hash, best_hash.to_string()); + assert_eq!(header_info.height, 0); + } + + #[cfg(not(feature = "v28_and_below"))] + { + let client = async_client_v29::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v29"); + let header_verbose = + client.get_block_header_verbose(&best_hash).await.expect("getblockheader verbose v29"); + + assert_eq!(header_verbose.hash, best_hash.to_string()); + assert_eq!(header_verbose.height, 0); + + let header_info = + client.get_block_header_info(&best_hash).await.expect("getblockheader info v29"); + + assert_eq!(header_info.hash, best_hash.to_string()); + assert_eq!(header_info.height, 0); + } +} + +#[tokio::test] +async fn async_client__get_raw_mempool_variants() { + let node = Node::with_wallet(Wallet::Default, &[]); + node.fund_wallet(); + + let (_address, txid) = node.create_mempool_transaction(); + let txid_str = txid.to_string(); + let client = async_client_for(&node); + + let mempool = client.get_raw_mempool().await.expect("getrawmempool"); + assert!(mempool.0.iter().any(|id| id == &txid_str)); + + #[cfg(feature = "v17")] + { + let mempool_verbose = + client.get_raw_mempool_verbose().await.expect("getrawmempool verbose"); + assert!(mempool_verbose.0.contains_key(&txid_str)); + } + + #[cfg(all(feature = "v18_and_below", not(feature = "v17")))] + { + let client = async_client_v18::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v18"); + let mempool_verbose = + client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v18"); + assert!(mempool_verbose.0.contains_key(&txid_str)); + } + + #[cfg(all(not(feature = "v18_and_below"), feature = "v20_and_below"))] + { + let client = async_client_v19::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v19"); + let mempool_verbose = + client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v19"); + assert!(mempool_verbose.0.contains_key(&txid_str)); + } + + #[cfg(all(not(feature = "v20_and_below"), feature = "v22_and_below"))] + { + let client = async_client_v21::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v21"); + let mempool_verbose = + client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v21"); + assert!(mempool_verbose.0.contains_key(&txid_str)); + } + + #[cfg(all(not(feature = "v22_and_below"), feature = "v23_and_below"))] + { + let client = async_client_v23::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v23"); + let mempool_verbose = + client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v23"); + assert!(mempool_verbose.0.contains_key(&txid_str)); + } + + #[cfg(not(feature = "v23_and_below"))] + { + let client = async_client_v24::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v24"); + let mempool_verbose = + client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v24"); + assert!(mempool_verbose.0.contains_key(&txid_str)); + } + + #[cfg(not(feature = "v20_and_below"))] + { + let client = async_client_v21::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v21"); + let mempool_sequence = + client.get_raw_mempool_sequence().await.expect("getrawmempool sequence"); + assert!(mempool_sequence.txids.iter().any(|id| id == &txid_str)); + assert!(mempool_sequence.mempool_sequence > 0); + } +} + +#[tokio::test] +async fn async_client__get_raw_transaction_variants() { + let node = Node::with_wallet(Wallet::Default, &[]); + node.fund_wallet(); + + let (_address, txid) = node.create_mempool_transaction(); + let client = async_client_for(&node); + + let raw = client.get_raw_transaction(txid).await.expect("getrawtransaction"); + assert!(!raw.0.is_empty()); + + let verbose = + client.get_raw_transaction_verbose(txid).await.expect("getrawtransaction verbose"); + assert_eq!(verbose.txid, txid.to_string()); + assert!(!verbose.hex.is_empty()); +} + +#[tokio::test] +#[cfg(not(feature = "v18_and_below"))] +async fn async_client__get_block_filter() { + let node = Node::with_wallet(Wallet::Default, &["-blockfilterindex"]); + node.mine_a_block(); + let client = async_client_v19::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) + .expect("async client v19"); + + let best_hash = node.client.best_block_hash().expect("best_block_hash"); + let filter = client.get_block_filter(best_hash).await.expect("getblockfilter"); + assert!(!filter.filter.is_empty()); +} + +fn auth_for(node: &Node) -> Auth { Auth::CookieFile(node.params.cookie_file.clone()) } From 8a9d9f7e667ab49303112317c6ea29f8ba9e970a Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Fri, 20 Feb 2026 12:33:33 +0000 Subject: [PATCH 10/14] Improve jsonrpc async client Add a JSONRPC_VERSION constant to remove magic number in code. Add the same version check to batch responses. --- jsonrpc/src/client_async.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/jsonrpc/src/client_async.rs b/jsonrpc/src/client_async.rs index 834a0c78e..e3b6d4d4a 100644 --- a/jsonrpc/src/client_async.rs +++ b/jsonrpc/src/client_async.rs @@ -19,6 +19,8 @@ use serde_json::Value; use crate::error::Error; use crate::{Request, Response}; +const JSONRPC_VERSION: &str = "2.0"; + /// Boxed future type used by async transports. pub type BoxFuture<'a, T> = Pin + Send + 'a>>; @@ -55,7 +57,12 @@ impl Client { /// [`crate::arg`] or [`crate::try_arg`]. pub fn build_request<'a>(&self, method: &'a str, params: Option<&'a RawValue>) -> Request<'a> { let nonce = self.nonce.fetch_add(1, atomic::Ordering::Relaxed); - Request { method, params, id: serde_json::Value::from(nonce), jsonrpc: Some("2.0") } + Request { + method, + params, + id: serde_json::Value::from(nonce), + jsonrpc: Some(JSONRPC_VERSION), + } } /// Sends a request to a client. @@ -92,6 +99,9 @@ impl Client { // First index responses by ID and catch duplicate IDs. let mut by_id = HashMap::with_capacity(requests.len()); for resp in responses.into_iter() { + if resp.jsonrpc.is_some() && resp.jsonrpc.as_deref() != Some(JSONRPC_VERSION) { + return Err(Error::VersionMismatch); + } let id = HashableValue(Cow::Owned(resp.id.clone())); if let Some(dup) = by_id.insert(id, resp) { return Err(Error::BatchDuplicateResponseId(dup.id)); @@ -123,7 +133,7 @@ impl Client { let id = request.id.clone(); let response = self.send_request(request).await?; - if response.jsonrpc.is_some() && response.jsonrpc != Some(From::from("2.0")) { + if response.jsonrpc.is_some() && response.jsonrpc.as_deref() != Some(JSONRPC_VERSION) { return Err(Error::VersionMismatch); } if response.id != id { From fe506f4786ca8ba6a5fa08fc96b0d8be4eb518e9 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 19 Feb 2026 17:01:51 +0000 Subject: [PATCH 11/14] Rewrite the async client Remove the version folders with RPC macros. Rewrite the async client to have the base methods in the main module removing all the macroization. Create a new module for the bdk client that has the required RPCs in it that all return the non-version specific model types. Rewrite the tests to use the bdk client and use a similar structure to the sync tests. --- .gitignore | 1 + .vscode/settings.json | 8 + client/src/client_async/bdk_client.rs | 96 +++++++ client/src/client_async/error.rs | 37 +++ client/src/client_async/mod.rs | 145 +++++----- client/src/client_async/v17/blockchain.rs | 114 -------- client/src/client_async/v17/mod.rs | 28 -- client/src/client_async/v17/network.rs | 28 -- .../src/client_async/v17/raw_transactions.rs | 32 --- client/src/client_async/v18/mod.rs | 24 -- client/src/client_async/v19/blockchain.rs | 22 -- client/src/client_async/v19/mod.rs | 27 -- client/src/client_async/v20/mod.rs | 25 -- client/src/client_async/v21/blockchain.rs | 31 --- client/src/client_async/v21/mod.rs | 27 -- client/src/client_async/v22/mod.rs | 25 -- client/src/client_async/v23/mod.rs | 25 -- client/src/client_async/v24/mod.rs | 25 -- client/src/client_async/v25/mod.rs | 25 -- client/src/client_async/v26/mod.rs | 25 -- client/src/client_async/v27/mod.rs | 25 -- client/src/client_async/v28/mod.rs | 25 -- client/src/client_async/v29/mod.rs | 25 -- client/src/client_async/v30/mod.rs | 25 -- integration_test/tests/async_client.rs | 250 ++++++------------ 25 files changed, 282 insertions(+), 838 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 client/src/client_async/bdk_client.rs delete mode 100644 client/src/client_async/v17/blockchain.rs delete mode 100644 client/src/client_async/v17/mod.rs delete mode 100644 client/src/client_async/v17/network.rs delete mode 100644 client/src/client_async/v17/raw_transactions.rs delete mode 100644 client/src/client_async/v18/mod.rs delete mode 100644 client/src/client_async/v19/blockchain.rs delete mode 100644 client/src/client_async/v19/mod.rs delete mode 100644 client/src/client_async/v20/mod.rs delete mode 100644 client/src/client_async/v21/blockchain.rs delete mode 100644 client/src/client_async/v21/mod.rs delete mode 100644 client/src/client_async/v22/mod.rs delete mode 100644 client/src/client_async/v23/mod.rs delete mode 100644 client/src/client_async/v24/mod.rs delete mode 100644 client/src/client_async/v25/mod.rs delete mode 100644 client/src/client_async/v26/mod.rs delete mode 100644 client/src/client_async/v27/mod.rs delete mode 100644 client/src/client_async/v28/mod.rs delete mode 100644 client/src/client_async/v29/mod.rs delete mode 100644 client/src/client_async/v30/mod.rs diff --git a/.gitignore b/.gitignore index 01626769f..969341414 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Cargo.lock **/target +.vscode/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..14e16cf43 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "rust-analyzer.cargo.features": [ + "client-async" + ], + "rust-analyzer.check.features": [ + "client-async" + ] +} diff --git a/client/src/client_async/bdk_client.rs b/client/src/client_async/bdk_client.rs new file mode 100644 index 000000000..469c89e38 --- /dev/null +++ b/client/src/client_async/bdk_client.rs @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Async JSON-RPC client with the RPC set used by BDK for Core versions 25 to 30. + +use bitcoin::{block, Block, BlockHash, Transaction, Txid}; + +use crate::client_async::{into_json, Client, Result}; +use crate::types::model::{GetBlockFilter, GetBlockHeaderVerbose, GetBlockVerboseOne}; + +impl Client { + /// Gets a block by blockhash. + pub async fn get_block(&self, hash: &BlockHash) -> Result { + let json: crate::types::v25::GetBlockVerboseZero = + self.call("getblock", &[into_json(hash)?, into_json(0)?]).await?; + Ok(json.into_model()?.0) + } + + /// Gets block count. + pub async fn get_block_count(&self) -> Result { + let json: crate::types::v25::GetBlockCount = self.call("getblockcount", &[]).await?; + Ok(json.into_model().0) + } + + /// Gets block hash for a height. + pub async fn get_block_hash(&self, height: u32) -> Result { + let json: crate::types::v25::GetBlockHash = + self.call("getblockhash", &[into_json(height)?]).await?; + Ok(json.into_model()?.0) + } + + /// Gets the hash of the chain tip. + pub async fn get_best_block_hash(&self) -> Result { + let json: crate::types::v25::GetBestBlockHash = self.call("getbestblockhash", &[]).await?; + Ok(json.into_model()?.0) + } + + /// Gets block header by blockhash. + pub async fn get_block_header(&self, hash: &BlockHash) -> Result { + let json: crate::types::v25::GetBlockHeader = + self.call("getblockheader", &[into_json(hash)?, into_json(false)?]).await?; + Ok(json.into_model()?.0) + } + + /// Gets block header with verbose output. + pub async fn get_block_header_verbose( + &self, + hash: &BlockHash, + ) -> Result { + let response: serde_json::Value = + self.call("getblockheader", &[into_json(hash)?, into_json(true)?]).await?; + + if let Ok(json) = + serde_json::from_value::(response.clone()) + { + Ok(json.into_model()?) + } else { + let json: crate::types::v25::GetBlockHeaderVerbose = serde_json::from_value(response)?; + Ok(json.into_model()?) + } + } + + /// Gets a block by blockhash with verbose set to 1. + pub async fn get_block_verbose(&self, hash: &BlockHash) -> Result { + let response: serde_json::Value = + self.call("getblock", &[into_json(hash)?, into_json(1)?]).await?; + + if let Ok(json) = + serde_json::from_value::(response.clone()) + { + Ok(json.into_model()?) + } else { + let json: crate::types::v25::GetBlockVerboseOne = serde_json::from_value(response)?; + Ok(json.into_model()?) + } + } + + /// Gets block filter for a blockhash. + pub async fn get_block_filter(&self, hash: &BlockHash) -> Result { + let json: crate::types::v25::GetBlockFilter = + self.call("getblockfilter", &[into_json(hash)?]).await?; + Ok(json.into_model()?) + } + + /// Gets transaction IDs currently in the mempool. + pub async fn get_raw_mempool(&self) -> Result> { + let json: crate::types::v25::GetRawMempool = self.call("getrawmempool", &[]).await?; + Ok(json.into_model()?.0) + } + + /// Gets raw transaction by txid. + pub async fn get_raw_transaction(&self, txid: &Txid) -> Result { + let json: crate::types::v25::GetRawTransaction = + self.call("getrawtransaction", &[into_json(txid)?]).await?; + Ok(json.into_model()?.0) + } +} diff --git a/client/src/client_async/error.rs b/client/src/client_async/error.rs index 331a81eeb..27ba7c93d 100644 --- a/client/src/client_async/error.rs +++ b/client/src/client_async/error.rs @@ -3,6 +3,15 @@ use std::{error, fmt, io}; use bitcoin::hex; +use types::v17::{ + GetBlockHeaderError, GetBlockHeaderVerboseError, GetBlockVerboseOneError, + GetRawTransactionVerboseError, +}; +use types::v19::GetBlockFilterError; +use types::v29::{ + GetBlockHeaderVerboseError as GetBlockHeaderVerboseErrorV29, + GetBlockVerboseOneError as GetBlockVerboseOneErrorV29, +}; /// The error type for errors produced in this library. #[derive(Debug)] @@ -48,6 +57,34 @@ impl From for Error { fn from(e: io::Error) -> Error { Error::Io(e) } } +impl From for Error { + fn from(e: GetBlockHeaderError) -> Self { Self::Returned(e.to_string()) } +} + +impl From for Error { + fn from(e: GetBlockHeaderVerboseError) -> Self { Self::Returned(e.to_string()) } +} + +impl From for Error { + fn from(e: GetBlockVerboseOneError) -> Self { Self::Returned(e.to_string()) } +} + +impl From for Error { + fn from(e: GetRawTransactionVerboseError) -> Self { Self::Returned(e.to_string()) } +} + +impl From for Error { + fn from(e: GetBlockHeaderVerboseErrorV29) -> Self { Self::Returned(e.to_string()) } +} + +impl From for Error { + fn from(e: GetBlockVerboseOneErrorV29) -> Self { Self::Returned(e.to_string()) } +} + +impl From for Error { + fn from(e: GetBlockFilterError) -> Self { Self::Returned(e.to_string()) } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { use Error::*; diff --git a/client/src/client_async/mod.rs b/client/src/client_async/mod.rs index 5e87cd043..91b93b6d2 100644 --- a/client/src/client_async/mod.rs +++ b/client/src/client_async/mod.rs @@ -2,22 +2,10 @@ //! Async JSON-RPC clients for specific versions of Bitcoin Core. +pub mod bdk_client; mod error; -pub mod v17; -pub mod v18; -pub mod v19; -pub mod v20; -pub mod v21; -pub mod v22; -pub mod v23; -pub mod v24; -pub mod v25; -pub mod v26; -pub mod v27; -pub mod v28; -pub mod v29; -pub mod v30; +use std::fmt; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -55,76 +43,63 @@ impl Auth { } } -/// Defines a async `jsonrpc::Client` using `bitreq`. -#[macro_export] -macro_rules! define_jsonrpc_bitreq_async_client { - ($version:literal) => { - use std::fmt; - use $crate::client_async::{log_response, Auth, Result}; - use $crate::client_async::error::Error; - - /// Client implements an async JSON-RPC client for the Bitcoin Core daemon or compatible APIs. - pub struct Client { - inner: jsonrpc::client_async::Client, - } - - impl fmt::Debug for Client { - fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result { - write!( - f, - "corepc_client::client_async::{}::Client({:?})", - $version, self.inner - ) - } - } +/// Client implements an async JSON-RPC client for the Bitcoin Core daemon or compatible APIs. +pub struct Client { + pub(crate) inner: jsonrpc::client_async::Client, +} - impl Client { - /// Creates a client to a bitcoind JSON-RPC server without authentication. - pub fn new(url: &str) -> Self { - let transport = jsonrpc::bitreq_http_async::Builder::new() - .url(url) - .expect("jsonrpc v0.19, this function does not error") - .timeout(std::time::Duration::from_secs(60)) - .build(); - let inner = jsonrpc::client_async::Client::with_transport(transport); - - Self { inner } - } +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result { + write!(f, "corepc_client::client_async::Client({:?})", self.inner) + } +} - /// Creates a client to a bitcoind JSON-RPC server with authentication. - pub fn new_with_auth(url: &str, auth: Auth) -> Result { - if matches!(auth, Auth::None) { - return Err(Error::MissingUserPassword); - } - let (user, pass) = auth.get_user_pass()?; - let transport = jsonrpc::bitreq_http_async::Builder::new() - .url(url) - .expect("jsonrpc v0.19, this function does not error") - .timeout(std::time::Duration::from_secs(60)) - .basic_auth(user.unwrap(), pass) - .build(); - let inner = jsonrpc::client_async::Client::with_transport(transport); - - Ok(Self { inner }) - } +impl Client { + /// Creates a client to a bitcoind JSON-RPC server without authentication. + pub fn new(url: &str) -> Self { + let transport = jsonrpc::bitreq_http_async::Builder::new() + .url(url) + .expect("jsonrpc v0.19, this function does not error") + .timeout(std::time::Duration::from_secs(60)) + .build(); + let inner = jsonrpc::client_async::Client::with_transport(transport); + + Self { inner } + } - /// Call an RPC `method` with given `args` list. - pub async fn call serde::de::Deserialize<'a>>( - &self, - method: &str, - args: &[serde_json::Value], - ) -> Result { - let raw = serde_json::value::to_raw_value(args)?; - let req = self.inner.build_request(&method, Some(&*raw)); - if log::log_enabled!(log::Level::Debug) { - log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args)); - } + /// Creates a client to a bitcoind JSON-RPC server with authentication. + pub fn new_with_auth(url: &str, auth: Auth) -> Result { + if matches!(auth, Auth::None) { + return Err(Error::MissingUserPassword); + } + let (user, pass) = auth.get_user_pass()?; + let user = user.ok_or(Error::MissingUserPassword)?; + let transport = jsonrpc::bitreq_http_async::Builder::new() + .url(url) + .expect("jsonrpc v0.19, this function does not error") + .timeout(std::time::Duration::from_secs(60)) + .basic_auth(user, pass) + .build(); + let inner = jsonrpc::client_async::Client::with_transport(transport); + + Ok(Self { inner }) + } - let resp = self.inner.send_request(req).await.map_err(Error::from); - log_response(method, &resp); - Ok(resp?.result()?) - } + /// Call an RPC `method` with given `args` list. + pub async fn call serde::de::Deserialize<'a>>( + &self, + method: &str, + args: &[serde_json::Value], + ) -> Result { + let raw = serde_json::value::to_raw_value(args)?; + let req = self.inner.build_request(method, Some(&*raw)); + if log::log_enabled!(log::Level::Debug) { + log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args)); } + + let resp = self.inner.send_request(req).await.map_err(Error::from); + log_response(method, &resp); + Ok(resp?.result()?) } } @@ -156,7 +131,7 @@ macro_rules! impl_async_client_check_expected_server_version { } /// Shorthand for converting a variable into a `serde_json::Value`. -fn into_json(val: T) -> Result +pub(crate) fn into_json(val: T) -> Result where T: serde::ser::Serialize, { @@ -179,10 +154,12 @@ fn log_response(method: &str, resp: &Result) { log::debug!(target: "corepc", "response error for {}: {:?}", method, e); } } else if log::log_enabled!(Trace) { - let def = - serde_json::value::to_raw_value(&serde_json::value::Value::Null).unwrap(); - let result = resp.result.as_ref().unwrap_or(&def); - log::trace!(target: "corepc", "response for {}: {}", method, result); + if let Ok(def) = + serde_json::value::to_raw_value(&serde_json::value::Value::Null) + { + let result = resp.result.as_ref().unwrap_or(&def); + log::trace!(target: "corepc", "response for {}: {}", method, result); + } }, } } diff --git a/client/src/client_async/v17/blockchain.rs b/client/src/client_async/v17/blockchain.rs deleted file mode 100644 index ac96ff320..000000000 --- a/client/src/client_async/v17/blockchain.rs +++ /dev/null @@ -1,114 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Macros for implementing async JSON-RPC methods on a client. -//! -//! Specifically this is methods found under the `== Blockchain ==` section of the -//! API docs of Bitcoin Core `v0.17`. -//! -//! All macros require `Client` to be in scope. -//! -//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. - -/// Implements Bitcoin Core JSON-RPC API method `getblock`. -#[macro_export] -macro_rules! impl_async_client_v17__get_block { - () => { - impl Client { - /// Gets a block by blockhash. - pub async fn get_block(&self, hash: BlockHash) -> Result { - let json = self.get_block_verbose_zero(hash).await?; - Ok(json.block()?) - } - - /// Gets a block by blockhash with verbose set to 0. - pub async fn get_block_verbose_zero( - &self, - hash: BlockHash, - ) -> Result { - self.call("getblock", &[into_json(hash)?, 0.into()]).await - } - - /// Gets a block by blockhash with verbose set to 1. - pub async fn get_block_verbose_one( - &self, - hash: BlockHash, - ) -> Result { - self.call("getblock", &[into_json(hash)?, 1.into()]).await - } - - /// Alias for getblock verbosity 1, matching bitcoincore-rpc naming. - pub async fn get_block_info(&self, hash: BlockHash) -> Result { - self.get_block_verbose_one(hash).await - } - } - }; -} - -/// Implements Bitcoin Core JSON-RPC API method `getblockcount`. -#[macro_export] -macro_rules! impl_async_client_v17__get_block_count { - () => { - impl Client { - pub async fn get_block_count(&self) -> Result { - self.call("getblockcount", &[]).await - } - } - }; -} - -/// Implements Bitcoin Core JSON-RPC API method `getblockhash`. -#[macro_export] -macro_rules! impl_async_client_v17__get_block_hash { - () => { - impl Client { - pub async fn get_block_hash(&self, height: u64) -> Result { - self.call("getblockhash", &[into_json(height)?]).await - } - } - }; -} - -/// Implements Bitcoin Core JSON-RPC API method `getblockheader`. -#[macro_export] -macro_rules! impl_async_client_v17__get_block_header { - () => { - impl Client { - pub async fn get_block_header(&self, hash: &BlockHash) -> Result { - self.call("getblockheader", &[into_json(hash)?, into_json(false)?]).await - } - - // This is the same as calling getblockheader with verbose==true. - pub async fn get_block_header_verbose( - &self, - hash: &BlockHash, - ) -> Result { - self.call("getblockheader", &[into_json(hash)?]).await - } - - /// Alias for getblockheader with verbose true. - pub async fn get_block_header_info( - &self, - hash: &BlockHash, - ) -> Result { - self.get_block_header_verbose(hash).await - } - } - }; -} - -/// Implements Bitcoin Core JSON-RPC API method `getrawmempool`. -#[macro_export] -macro_rules! impl_async_client_v17__get_raw_mempool { - () => { - impl Client { - pub async fn get_raw_mempool(&self) -> Result { - // Equivalent to self.call("getrawmempool", &[into_json(false)?]) - self.call("getrawmempool", &[]).await - } - - pub async fn get_raw_mempool_verbose(&self) -> Result { - self.call("getrawmempool", &[into_json(true)?]).await - } - } - }; -} diff --git a/client/src/client_async/v17/mod.rs b/client/src/client_async/v17/mod.rs deleted file mode 100644 index c81bb3f11..000000000 --- a/client/src/client_async/v17/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v0.17`. - -pub mod blockchain; -pub mod network; -pub mod raw_transactions; - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v17::*; - -crate::define_jsonrpc_bitreq_async_client!("v17"); -crate::impl_async_client_check_expected_server_version!({ [170200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v17__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v17/network.rs b/client/src/client_async/v17/network.rs deleted file mode 100644 index 3c6e78b14..000000000 --- a/client/src/client_async/v17/network.rs +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Macros for implementing async JSON-RPC methods on a client. -//! -//! Specifically this is methods found under the `== Network ==` section of the -//! API docs of Bitcoin Core `v0.17`. -//! -//! All macros require `Client` to be in scope. -//! -//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. - -/// Implements Bitcoin Core JSON-RPC API method `getnetworkinfo`. -#[macro_export] -macro_rules! impl_async_client_v17__get_network_info { - () => { - impl Client { - /// Returns the server version field of `GetNetworkInfo`. - pub async fn server_version(&self) -> Result { - let info = self.get_network_info().await?; - Ok(info.version) - } - - pub async fn get_network_info(&self) -> Result { - self.call("getnetworkinfo", &[]).await - } - } - }; -} diff --git a/client/src/client_async/v17/raw_transactions.rs b/client/src/client_async/v17/raw_transactions.rs deleted file mode 100644 index dea957ec7..000000000 --- a/client/src/client_async/v17/raw_transactions.rs +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Macros for implementing async JSON-RPC methods on a client. -//! -//! Specifically this is methods found under the `== Rawtransactions ==` section of the -//! API docs of Bitcoin Core `v0.17`. -//! -//! All macros require `Client` to be in scope. -//! -//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. - -/// Implements Bitcoin Core JSON-RPC API method `getrawtransaction`. -#[macro_export] -macro_rules! impl_async_client_v17__get_raw_transaction { - () => { - impl Client { - pub async fn get_raw_transaction( - &self, - txid: bitcoin::Txid, - ) -> Result { - self.call("getrawtransaction", &[into_json(&txid)?, false.into()]).await - } - - pub async fn get_raw_transaction_verbose( - &self, - txid: Txid, - ) -> Result { - self.call("getrawtransaction", &[into_json(&txid)?, true.into()]).await - } - } - }; -} diff --git a/client/src/client_async/v18/mod.rs b/client/src/client_async/v18/mod.rs deleted file mode 100644 index 896410fa6..000000000 --- a/client/src/client_async/v18/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v0.18`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v18::*; - -crate::define_jsonrpc_bitreq_async_client!("v18"); -crate::impl_async_client_check_expected_server_version!({ [180100] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v17__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v19/blockchain.rs b/client/src/client_async/v19/blockchain.rs deleted file mode 100644 index d3226b928..000000000 --- a/client/src/client_async/v19/blockchain.rs +++ /dev/null @@ -1,22 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Macros for implementing async JSON-RPC methods on a client. -//! -//! Specifically this is methods found under the `== Blockchain ==` section of the -//! API docs of Bitcoin Core `v0.19`. -//! -//! All macros require `Client` to be in scope. -//! -//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. - -/// Implements Bitcoin Core JSON-RPC API method `getblockfilter`. -#[macro_export] -macro_rules! impl_async_client_v19__get_block_filter { - () => { - impl Client { - pub async fn get_block_filter(&self, block: BlockHash) -> Result { - self.call("getblockfilter", &[into_json(block)?]).await - } - } - }; -} diff --git a/client/src/client_async/v19/mod.rs b/client/src/client_async/v19/mod.rs deleted file mode 100644 index ace91663d..000000000 --- a/client/src/client_async/v19/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v0.19`. - -pub mod blockchain; - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v19::*; - -crate::define_jsonrpc_bitreq_async_client!("v19"); -crate::impl_async_client_check_expected_server_version!({ [190100] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v17__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v20/mod.rs b/client/src/client_async/v20/mod.rs deleted file mode 100644 index f89e8510f..000000000 --- a/client/src/client_async/v20/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v0.20`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v20::*; - -crate::define_jsonrpc_bitreq_async_client!("v20"); -crate::impl_async_client_check_expected_server_version!({ [200200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v17__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v21/blockchain.rs b/client/src/client_async/v21/blockchain.rs deleted file mode 100644 index 902fcdc9b..000000000 --- a/client/src/client_async/v21/blockchain.rs +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Macros for implementing async JSON-RPC methods on a client. -//! -//! Specifically this is methods found under the `== Blockchain ==` section of the -//! API docs of Bitcoin Core `v0.21`. -//! -//! All macros require `Client` to be in scope. -//! -//! See or use the `define_jsonrpc_bitreq_async_client!` macro to define a `Client`. - -/// Implements Bitcoin Core JSON-RPC API method `getrawmempool`. -#[macro_export] -macro_rules! impl_async_client_v21__get_raw_mempool { - () => { - impl Client { - pub async fn get_raw_mempool(&self) -> Result { - // Equivalent to self.call("getrawmempool", &[into_json(false)?]) - self.call("getrawmempool", &[]).await - } - - pub async fn get_raw_mempool_verbose(&self) -> Result { - self.call("getrawmempool", &[into_json(true)?]).await - } - - pub async fn get_raw_mempool_sequence(&self) -> Result { - self.call("getrawmempool", &[into_json(false)?, into_json(true)?]).await - } - } - }; -} diff --git a/client/src/client_async/v21/mod.rs b/client/src/client_async/v21/mod.rs deleted file mode 100644 index 8837233e6..000000000 --- a/client/src/client_async/v21/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v0.21`. - -pub mod blockchain; - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v21::*; - -crate::define_jsonrpc_bitreq_async_client!("v21"); -crate::impl_async_client_check_expected_server_version!({ [210200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v22/mod.rs b/client/src/client_async/v22/mod.rs deleted file mode 100644 index dcf0e4e56..000000000 --- a/client/src/client_async/v22/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v22`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v22::*; - -crate::define_jsonrpc_bitreq_async_client!("v22"); -crate::impl_async_client_check_expected_server_version!({ [220100] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v23/mod.rs b/client/src/client_async/v23/mod.rs deleted file mode 100644 index 1d56ef9d0..000000000 --- a/client/src/client_async/v23/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v23`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v23::*; - -crate::define_jsonrpc_bitreq_async_client!("v23"); -crate::impl_async_client_check_expected_server_version!({ [230200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v24/mod.rs b/client/src/client_async/v24/mod.rs deleted file mode 100644 index 38e4dc807..000000000 --- a/client/src/client_async/v24/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v24`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v24::*; - -crate::define_jsonrpc_bitreq_async_client!("v24"); -crate::impl_async_client_check_expected_server_version!({ [240200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v25/mod.rs b/client/src/client_async/v25/mod.rs deleted file mode 100644 index fc5abd24b..000000000 --- a/client/src/client_async/v25/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v25`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v25::*; - -crate::define_jsonrpc_bitreq_async_client!("v25"); -crate::impl_async_client_check_expected_server_version!({ [250200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v26/mod.rs b/client/src/client_async/v26/mod.rs deleted file mode 100644 index 5b2e9d413..000000000 --- a/client/src/client_async/v26/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v26`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v26::*; - -crate::define_jsonrpc_bitreq_async_client!("v26"); -crate::impl_async_client_check_expected_server_version!({ [260000, 260100, 260200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v27/mod.rs b/client/src/client_async/v27/mod.rs deleted file mode 100644 index 61bc2bd9b..000000000 --- a/client/src/client_async/v27/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v27`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v27::*; - -crate::define_jsonrpc_bitreq_async_client!("v27"); -crate::impl_async_client_check_expected_server_version!({ [270000, 270100, 270200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v28/mod.rs b/client/src/client_async/v28/mod.rs deleted file mode 100644 index 7f5f891b2..000000000 --- a/client/src/client_async/v28/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v28`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v28::*; - -crate::define_jsonrpc_bitreq_async_client!("v28"); -crate::impl_async_client_check_expected_server_version!({ [280000, 280100, 280200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v29/mod.rs b/client/src/client_async/v29/mod.rs deleted file mode 100644 index 3dad3d0f9..000000000 --- a/client/src/client_async/v29/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v29`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v29::*; - -crate::define_jsonrpc_bitreq_async_client!("v29"); -crate::impl_async_client_check_expected_server_version!({ [290000] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/client/src/client_async/v30/mod.rs b/client/src/client_async/v30/mod.rs deleted file mode 100644 index 7138173b0..000000000 --- a/client/src/client_async/v30/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! An async JSON-RPC client for Bitcoin Core `v30`. - -use bitcoin::{Block, BlockHash, Txid}; - -use crate::client_async::into_json; -use crate::types::v30::*; - -crate::define_jsonrpc_bitreq_async_client!("v30"); -crate::impl_async_client_check_expected_server_version!({ [300000, 300100, 300200] }); - -// == Blockchain == -crate::impl_async_client_v17__get_block!(); -crate::impl_async_client_v17__get_block_count!(); -crate::impl_async_client_v19__get_block_filter!(); -crate::impl_async_client_v17__get_block_hash!(); -crate::impl_async_client_v17__get_block_header!(); -crate::impl_async_client_v21__get_raw_mempool!(); - -// == Network == -crate::impl_async_client_v17__get_network_info!(); - -// == Rawtransactions == -crate::impl_async_client_v17__get_raw_transaction!(); diff --git a/integration_test/tests/async_client.rs b/integration_test/tests/async_client.rs index d289e6c79..9ba2d7bd4 100644 --- a/integration_test/tests/async_client.rs +++ b/integration_test/tests/async_client.rs @@ -2,222 +2,130 @@ //! Tests for the async client. +#![cfg(not(feature = "v24_and_below"))] #![allow(non_snake_case)] // Test names intentionally use double underscore. -#[cfg(all(feature = "v18_and_below", not(feature = "v17")))] -use corepc_client::client_async::v18 as async_client_v18; -#[cfg(not(feature = "v18_and_below"))] -use corepc_client::client_async::v19 as async_client_v19; -#[cfg(not(feature = "v20_and_below"))] -use corepc_client::client_async::v21 as async_client_v21; -#[cfg(all(not(feature = "v22_and_below"), feature = "v23_and_below"))] -use corepc_client::client_async::v23 as async_client_v23; -#[cfg(not(feature = "v23_and_below"))] -use corepc_client::client_async::v24 as async_client_v24; -#[cfg(not(feature = "v28_and_below"))] -use corepc_client::client_async::v29 as async_client_v29; -use corepc_client::client_async::{v17 as async_client, Auth}; +use bitcoin::address::KnownHrp; +use bitcoin::{Address, CompressedPublicKey, PrivateKey}; +use corepc_client::client_async::{Auth, Client, Error as AsyncClientError}; use integration_test::{Node, NodeExt as _, Wallet}; +use node::mtype; -fn async_client_for(node: &Node) -> async_client::Client { - async_client::Client::new_with_auth(&node.rpc_url(), auth_for(node)).expect("async client") +fn async_client_for(node: &Node) -> Client { + Client::new_with_auth(&node.rpc_url(), auth_for(node)).expect("async client") } #[tokio::test] -async fn async_client__get_block_count_and_hash() { +async fn async__get_best_block_hash__modelled() { let node = Node::with_wallet(Wallet::None, &[]); let client = async_client_for(&node); - let count = client.get_block_count().await.expect("getblockcount"); - assert_eq!(count.0, 0); - - let json = client.get_block_hash(0).await.expect("getblockhash"); - let model = json.into_model().expect("getblockhash model"); + let model: Result = client.get_best_block_hash().await; + let model = model.unwrap(); let expected = node.client.best_block_hash().expect("best_block_hash"); - assert_eq!(model.0, expected); + assert_eq!(model, expected); } #[tokio::test] -async fn async_client__get_block_variants() { +async fn async__get_block__modelled() { let node = Node::with_wallet(Wallet::None, &[]); let client = async_client_for(&node); let best_hash = node.client.best_block_hash().expect("best_block_hash"); - let block = client.get_block(best_hash).await.expect("getblock"); - assert_eq!(block.block_hash(), best_hash); - - let block_v0 = client.get_block_verbose_zero(best_hash).await.expect("getblock verbose=0"); - let block_from_hex = block_v0.block().expect("getblock verbose=0 decode"); - assert_eq!(block_from_hex.block_hash(), best_hash); - - #[cfg(feature = "v28_and_below")] - { - let block_v1 = client.get_block_verbose_one(best_hash).await.expect("getblock verbose=1"); - assert_eq!(block_v1.hash, best_hash.to_string()); - - let block_info = client.get_block_info(best_hash).await.expect("getblock info"); - assert_eq!(block_info.hash, best_hash.to_string()); - } - - #[cfg(not(feature = "v28_and_below"))] - { - let client = async_client_v29::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v29"); - let block_v1 = - client.get_block_verbose_one(best_hash).await.expect("getblock verbose=1 v29"); - assert_eq!(block_v1.hash, best_hash.to_string()); - - let block_info = client.get_block_info(best_hash).await.expect("getblock info v29"); - assert_eq!(block_info.hash, best_hash.to_string()); - } + let model: Result = client.get_block(&best_hash).await; + let model = model.unwrap(); + assert_eq!(model.block_hash(), best_hash); + + let model: Result = + client.get_block_verbose(&best_hash).await; + let model = model.unwrap(); + assert_eq!(model.hash, best_hash); } #[tokio::test] -async fn async_client__get_block_header_variants() { +async fn async__get_block_count__modelled() { let node = Node::with_wallet(Wallet::None, &[]); let client = async_client_for(&node); - let best_hash = node.client.best_block_hash().expect("best_block_hash"); - let header = client.get_block_header(&best_hash).await.expect("getblockheader"); - assert!(!header.0.is_empty()); - - #[cfg(feature = "v28_and_below")] - { - let header_verbose = - client.get_block_header_verbose(&best_hash).await.expect("getblockheader verbose"); - - assert_eq!(header_verbose.hash, best_hash.to_string()); - assert_eq!(header_verbose.height, 0); - - let header_info = - client.get_block_header_info(&best_hash).await.expect("getblockheader info"); + let model: Result = client.get_block_count().await; + let model = model.unwrap(); + assert_eq!(model, 0); +} - assert_eq!(header_info.hash, best_hash.to_string()); - assert_eq!(header_info.height, 0); - } +#[tokio::test] +#[cfg(not(feature = "v18_and_below"))] +async fn async__get_block_filter__modelled() { + let node = Node::with_wallet(Wallet::None, &["-blockfilterindex"]); + let client = async_client_for(&node); - #[cfg(not(feature = "v28_and_below"))] - { - let client = async_client_v29::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v29"); - let header_verbose = - client.get_block_header_verbose(&best_hash).await.expect("getblockheader verbose v29"); + let best_hash = node.client.best_block_hash().expect("best_block_hash"); + let model: Result = + client.get_block_filter(&best_hash).await; + let model = model.unwrap(); - assert_eq!(header_verbose.hash, best_hash.to_string()); - assert_eq!(header_verbose.height, 0); + assert!(!model.filter.is_empty()); + assert_eq!(model.header.to_string().len(), 64); +} - let header_info = - client.get_block_header_info(&best_hash).await.expect("getblockheader info v29"); +#[tokio::test] +async fn async__get_block_hash__modelled() { + let node = Node::with_wallet(Wallet::None, &[]); + let client = async_client_for(&node); - assert_eq!(header_info.hash, best_hash.to_string()); - assert_eq!(header_info.height, 0); - } + let model: Result = client.get_block_hash(0).await; + let model = model.unwrap(); + let expected = node.client.best_block_hash().expect("best_block_hash"); + assert_eq!(model, expected); } #[tokio::test] -async fn async_client__get_raw_mempool_variants() { - let node = Node::with_wallet(Wallet::Default, &[]); - node.fund_wallet(); - - let (_address, txid) = node.create_mempool_transaction(); - let txid_str = txid.to_string(); +async fn async__get_block_header__modelled() { + let node = Node::with_wallet(Wallet::None, &[]); let client = async_client_for(&node); - let mempool = client.get_raw_mempool().await.expect("getrawmempool"); - assert!(mempool.0.iter().any(|id| id == &txid_str)); - - #[cfg(feature = "v17")] - { - let mempool_verbose = - client.get_raw_mempool_verbose().await.expect("getrawmempool verbose"); - assert!(mempool_verbose.0.contains_key(&txid_str)); - } - - #[cfg(all(feature = "v18_and_below", not(feature = "v17")))] - { - let client = async_client_v18::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v18"); - let mempool_verbose = - client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v18"); - assert!(mempool_verbose.0.contains_key(&txid_str)); - } - - #[cfg(all(not(feature = "v18_and_below"), feature = "v20_and_below"))] - { - let client = async_client_v19::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v19"); - let mempool_verbose = - client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v19"); - assert!(mempool_verbose.0.contains_key(&txid_str)); - } - - #[cfg(all(not(feature = "v20_and_below"), feature = "v22_and_below"))] - { - let client = async_client_v21::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v21"); - let mempool_verbose = - client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v21"); - assert!(mempool_verbose.0.contains_key(&txid_str)); - } - - #[cfg(all(not(feature = "v22_and_below"), feature = "v23_and_below"))] - { - let client = async_client_v23::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v23"); - let mempool_verbose = - client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v23"); - assert!(mempool_verbose.0.contains_key(&txid_str)); - } - - #[cfg(not(feature = "v23_and_below"))] - { - let client = async_client_v24::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v24"); - let mempool_verbose = - client.get_raw_mempool_verbose().await.expect("getrawmempool verbose v24"); - assert!(mempool_verbose.0.contains_key(&txid_str)); - } - - #[cfg(not(feature = "v20_and_below"))] - { - let client = async_client_v21::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v21"); - let mempool_sequence = - client.get_raw_mempool_sequence().await.expect("getrawmempool sequence"); - assert!(mempool_sequence.txids.iter().any(|id| id == &txid_str)); - assert!(mempool_sequence.mempool_sequence > 0); - } + let best_hash = node.client.best_block_hash().expect("best_block_hash"); + let model: Result = + client.get_block_header(&best_hash).await; + let model = model.unwrap(); + assert_eq!(model.block_hash(), best_hash); + + let model: Result = + client.get_block_header_verbose(&best_hash).await; + let model = model.unwrap(); + assert_eq!(model.hash, best_hash); + assert_eq!(model.height, 0); } #[tokio::test] -async fn async_client__get_raw_transaction_variants() { - let node = Node::with_wallet(Wallet::Default, &[]); - node.fund_wallet(); - - let (_address, txid) = node.create_mempool_transaction(); +async fn async__get_raw_mempool__modelled() { + let node = Node::with_wallet(Wallet::None, &[]); let client = async_client_for(&node); - let raw = client.get_raw_transaction(txid).await.expect("getrawtransaction"); - assert!(!raw.0.is_empty()); - - let verbose = - client.get_raw_transaction_verbose(txid).await.expect("getrawtransaction verbose"); - assert_eq!(verbose.txid, txid.to_string()); - assert!(!verbose.hex.is_empty()); + let model: Result, AsyncClientError> = client.get_raw_mempool().await; + let model = model.unwrap(); + assert!(model.is_empty()); } #[tokio::test] -#[cfg(not(feature = "v18_and_below"))] -async fn async_client__get_block_filter() { - let node = Node::with_wallet(Wallet::Default, &["-blockfilterindex"]); - node.mine_a_block(); - let client = async_client_v19::Client::new_with_auth(&node.rpc_url(), auth_for(&node)) - .expect("async client v19"); +async fn async__get_raw_transaction__modelled() { + let node = Node::with_wallet(Wallet::None, &["-txindex"]); + let privkey = + PrivateKey::from_wif("cVt4o7BGAig1UXywgGSmARhxMdzP5qvQsxKkSsc1XEkw3tDTQFpy").expect("wif"); + let secp = bitcoin::secp256k1::Secp256k1::new(); + let pubkey = privkey.public_key(&secp); + let address = Address::p2wpkh(&CompressedPublicKey(pubkey.inner), KnownHrp::Regtest); + node.client.generate_to_address(1, &address).expect("generatetoaddress"); + let client = async_client_for(&node); let best_hash = node.client.best_block_hash().expect("best_block_hash"); - let filter = client.get_block_filter(best_hash).await.expect("getblockfilter"); - assert!(!filter.filter.is_empty()); + let block = client.get_block(&best_hash).await.expect("getblock"); + let txid = block.txdata[0].compute_txid(); + + let model: Result = + client.get_raw_transaction(&txid).await; + let model = model.unwrap(); + assert_eq!(model.compute_txid(), txid); } fn auth_for(node: &Node) -> Auth { Auth::CookieFile(node.params.cookie_file.clone()) } From 7a45f6f200fcc1170ec78e4b2d6efdb66ca597f6 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Wed, 25 Feb 2026 16:32:09 +0000 Subject: [PATCH 12/14] Rename the client_async module to bdk_client The client is only designed for BDK and is not a general purpose async client. Rename it to bdk_client to make the purpose clear. --- .../src/{client_async => bdk_client}/error.rs | 0 .../src/{client_async => bdk_client}/mod.rs | 6 +++--- .../bdk_client.rs => bdk_client/rpcs.rs} | 19 ++++++++++--------- client/src/lib.rs | 2 +- .../tests/{async_client.rs => bdk_client.rs} | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) rename client/src/{client_async => bdk_client}/error.rs (100%) rename client/src/{client_async => bdk_client}/mod.rs (97%) rename client/src/{client_async/bdk_client.rs => bdk_client/rpcs.rs} (87%) rename integration_test/tests/{async_client.rs => bdk_client.rs} (98%) diff --git a/client/src/client_async/error.rs b/client/src/bdk_client/error.rs similarity index 100% rename from client/src/client_async/error.rs rename to client/src/bdk_client/error.rs diff --git a/client/src/client_async/mod.rs b/client/src/bdk_client/mod.rs similarity index 97% rename from client/src/client_async/mod.rs rename to client/src/bdk_client/mod.rs index 91b93b6d2..f0941a135 100644 --- a/client/src/client_async/mod.rs +++ b/client/src/bdk_client/mod.rs @@ -1,16 +1,16 @@ // SPDX-License-Identifier: CC0-1.0 -//! Async JSON-RPC clients for specific versions of Bitcoin Core. +//! Async JSON-RPC clients for Bitcoin Core v25 to v30. -pub mod bdk_client; mod error; +mod rpcs; use std::fmt; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; -pub use crate::client_async::error::Error; +pub use crate::bdk_client::error::Error; /// Crate-specific Result type. /// diff --git a/client/src/client_async/bdk_client.rs b/client/src/bdk_client/rpcs.rs similarity index 87% rename from client/src/client_async/bdk_client.rs rename to client/src/bdk_client/rpcs.rs index 469c89e38..11e1d7dd7 100644 --- a/client/src/client_async/bdk_client.rs +++ b/client/src/bdk_client/rpcs.rs @@ -1,10 +1,11 @@ // SPDX-License-Identifier: CC0-1.0 -//! Async JSON-RPC client with the RPC set used by BDK for Core versions 25 to 30. +//! RPC set used by BDK. +//! All functions return the version nonspecific, strongly typed types. use bitcoin::{block, Block, BlockHash, Transaction, Txid}; -use crate::client_async::{into_json, Client, Result}; +use crate::bdk_client::{into_json, Client, Result}; use crate::types::model::{GetBlockFilter, GetBlockHeaderVerbose, GetBlockVerboseOne}; impl Client { @@ -15,13 +16,13 @@ impl Client { Ok(json.into_model()?.0) } - /// Gets block count. + /// Gets the block count. pub async fn get_block_count(&self) -> Result { let json: crate::types::v25::GetBlockCount = self.call("getblockcount", &[]).await?; Ok(json.into_model().0) } - /// Gets block hash for a height. + /// Gets the block hash for a height. pub async fn get_block_hash(&self, height: u32) -> Result { let json: crate::types::v25::GetBlockHash = self.call("getblockhash", &[into_json(height)?]).await?; @@ -34,14 +35,14 @@ impl Client { Ok(json.into_model()?.0) } - /// Gets block header by blockhash. + /// Gets the block header by blockhash. pub async fn get_block_header(&self, hash: &BlockHash) -> Result { let json: crate::types::v25::GetBlockHeader = self.call("getblockheader", &[into_json(hash)?, into_json(false)?]).await?; Ok(json.into_model()?.0) } - /// Gets block header with verbose output. + /// Gets the block header with verbose output. pub async fn get_block_header_verbose( &self, hash: &BlockHash, @@ -74,20 +75,20 @@ impl Client { } } - /// Gets block filter for a blockhash. + /// Gets the block filter for a blockhash. pub async fn get_block_filter(&self, hash: &BlockHash) -> Result { let json: crate::types::v25::GetBlockFilter = self.call("getblockfilter", &[into_json(hash)?]).await?; Ok(json.into_model()?) } - /// Gets transaction IDs currently in the mempool. + /// Gets the transaction IDs currently in the mempool. pub async fn get_raw_mempool(&self) -> Result> { let json: crate::types::v25::GetRawMempool = self.call("getrawmempool", &[]).await?; Ok(json.into_model()?.0) } - /// Gets raw transaction by txid. + /// Gets the raw transaction by txid. pub async fn get_raw_transaction(&self, txid: &Txid) -> Result { let json: crate::types::v25::GetRawTransaction = self.call("getrawtransaction", &[into_json(txid)?]).await?; diff --git a/client/src/lib.rs b/client/src/lib.rs index 01e7c7429..3efc5d00f 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -13,4 +13,4 @@ pub extern crate types; pub mod client_sync; #[cfg(feature = "client-async")] -pub mod client_async; +pub mod bdk_client; diff --git a/integration_test/tests/async_client.rs b/integration_test/tests/bdk_client.rs similarity index 98% rename from integration_test/tests/async_client.rs rename to integration_test/tests/bdk_client.rs index 9ba2d7bd4..1abe8ae73 100644 --- a/integration_test/tests/async_client.rs +++ b/integration_test/tests/bdk_client.rs @@ -7,7 +7,7 @@ use bitcoin::address::KnownHrp; use bitcoin::{Address, CompressedPublicKey, PrivateKey}; -use corepc_client::client_async::{Auth, Client, Error as AsyncClientError}; +use corepc_client::bdk_client::{Auth, Client, Error as AsyncClientError}; use integration_test::{Node, NodeExt as _, Wallet}; use node::mtype; From 84da74661d0aaed7f235e728483a9e54eb379204 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 12 Mar 2026 08:46:23 +0000 Subject: [PATCH 13/14] Revert breaking changes to jsonrpc sync client Restore the original sync client feature name, module name and reexports so that downstream is not broken. --- .github/workflows/cron-daily-fuzz.yml | 2 +- client/Cargo.toml | 2 +- client/src/client_sync/mod.rs | 10 +++++----- fuzz/Cargo.toml | 6 +++--- .../{bitreq_http_sync.rs => bitreq_http.rs} | 2 +- fuzz/generate-files.sh | 2 +- jsonrpc/Cargo.toml | 2 +- jsonrpc/contrib/test_vars.sh | 2 +- jsonrpc/src/{client_sync.rs => client.rs} | 2 +- .../http/{bitreq_http_sync.rs => bitreq_http.rs} | 8 ++++---- jsonrpc/src/http/mod.rs | 4 ++-- jsonrpc/src/http/simple_http.rs | 13 +++++++------ jsonrpc/src/lib.rs | 7 ++++--- jsonrpc/src/simple_tcp.rs | 4 ++-- jsonrpc/src/simple_uds.rs | 4 ++-- 15 files changed, 36 insertions(+), 34 deletions(-) rename fuzz/fuzz_targets/{bitreq_http_sync.rs => bitreq_http.rs} (95%) rename jsonrpc/src/{client_sync.rs => client.rs} (99%) rename jsonrpc/src/http/{bitreq_http_sync.rs => bitreq_http.rs} (97%) diff --git a/.github/workflows/cron-daily-fuzz.yml b/.github/workflows/cron-daily-fuzz.yml index 4fdb768c3..9f87fe6d1 100644 --- a/.github/workflows/cron-daily-fuzz.yml +++ b/.github/workflows/cron-daily-fuzz.yml @@ -18,7 +18,7 @@ jobs: # We only get 20 jobs at a time, we probably don't want to go # over that limit with fuzzing because of the hour run time. fuzz_target: [ - bitreq_http_sync, + bitreq_http, simple_http, ] steps: diff --git a/client/Cargo.toml b/client/Cargo.toml index 16b6519aa..6758a20d4 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -18,7 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] # Enable this feature to get a blocking JSON-RPC client. -client-sync = ["jsonrpc", "jsonrpc/bitreq_http_sync"] +client-sync = ["jsonrpc", "jsonrpc/bitreq_http"] # Enable this feature to get an async JSON-RPC client. client-async = ["jsonrpc", "jsonrpc/bitreq_http_async", "jsonrpc/client_async"] diff --git a/client/src/client_sync/mod.rs b/client/src/client_sync/mod.rs index 3d9a505ad..baaf3f8ba 100644 --- a/client/src/client_sync/mod.rs +++ b/client/src/client_sync/mod.rs @@ -66,7 +66,7 @@ macro_rules! define_jsonrpc_bitreq_client { /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. pub struct Client { - inner: jsonrpc::client_sync::Client, + inner: jsonrpc::client::Client, } impl fmt::Debug for Client { @@ -81,12 +81,12 @@ macro_rules! define_jsonrpc_bitreq_client { impl Client { /// Creates a client to a bitcoind JSON-RPC server without authentication. pub fn new(url: &str) -> Self { - let transport = jsonrpc::http::bitreq_http_sync::Builder::new() + let transport = jsonrpc::http::bitreq_http::Builder::new() .url(url) .expect("jsonrpc v0.19, this function does not error") .timeout(std::time::Duration::from_secs(60)) .build(); - let inner = jsonrpc::client_sync::Client::with_transport(transport); + let inner = jsonrpc::client::Client::with_transport(transport); Self { inner } } @@ -98,13 +98,13 @@ macro_rules! define_jsonrpc_bitreq_client { } let (user, pass) = auth.get_user_pass()?; - let transport = jsonrpc::http::bitreq_http_sync::Builder::new() + let transport = jsonrpc::http::bitreq_http::Builder::new() .url(url) .expect("jsonrpc v0.19, this function does not error") .timeout(std::time::Duration::from_secs(60)) .basic_auth(user.unwrap(), pass) .build(); - let inner = jsonrpc::client_sync::Client::with_transport(transport); + let inner = jsonrpc::client::Client::with_transport(transport); Ok(Self { inner }) } diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 11deb311b..10293ba55 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -11,7 +11,7 @@ cargo-fuzz = true [dependencies] honggfuzz = { version = "0.5.55", default-features = false } -jsonrpc = { path = "../jsonrpc", features = ["bitreq_http_sync"] } +jsonrpc = { path = "../jsonrpc", features = ["bitreq_http"] } serde = { version = "1.0.103", features = [ "derive" ] } serde_json = "1.0" @@ -20,8 +20,8 @@ serde_json = "1.0" unexpected_cfgs = { level = "deny", check-cfg = ['cfg(fuzzing)', 'cfg(jsonrpc_fuzz)'] } [[bin]] -name = "bitreq_http_sync" -path = "fuzz_targets/bitreq_http_sync.rs" +name = "bitreq_http" +path = "fuzz_targets/bitreq_http.rs" [[bin]] name = "simple_http" diff --git a/fuzz/fuzz_targets/bitreq_http_sync.rs b/fuzz/fuzz_targets/bitreq_http.rs similarity index 95% rename from fuzz/fuzz_targets/bitreq_http_sync.rs rename to fuzz/fuzz_targets/bitreq_http.rs index 98fafc0da..90416ee0b 100644 --- a/fuzz/fuzz_targets/bitreq_http_sync.rs +++ b/fuzz/fuzz_targets/bitreq_http.rs @@ -8,7 +8,7 @@ fn do_test(data: &[u8]) { { use std::io; - use jsonrpc::bitreq_http_sync::{BitreqHttpTransport, FUZZ_TCP_SOCK}; + use jsonrpc::bitreq_http::{BitreqHttpTransport, FUZZ_TCP_SOCK}; use jsonrpc::Client; *FUZZ_TCP_SOCK.lock().unwrap() = Some(io::Cursor::new(data.to_vec())); diff --git a/fuzz/generate-files.sh b/fuzz/generate-files.sh index 77b4966b6..9afe14bd3 100755 --- a/fuzz/generate-files.sh +++ b/fuzz/generate-files.sh @@ -23,7 +23,7 @@ cargo-fuzz = true [dependencies] honggfuzz = { version = "0.5.55", default-features = false } -jsonrpc = { path = "..", features = ["bitreq_http_sync"] } +jsonrpc = { path = "..", features = ["bitreq_http"] } serde = { version = "1.0.103", features = [ "derive" ] } serde_json = "1.0" diff --git a/jsonrpc/Cargo.toml b/jsonrpc/Cargo.toml index 624009f2a..ca818f1bc 100644 --- a/jsonrpc/Cargo.toml +++ b/jsonrpc/Cargo.toml @@ -21,7 +21,7 @@ default = [ "simple_http", "simple_tcp" ] # A bare-minimum HTTP transport. simple_http = [ "base64" ] # A transport that uses `bitreq` as the HTTP client. -bitreq_http_sync = [ "base64", "bitreq" ] +bitreq_http = [ "base64", "bitreq" ] # A transport that uses `bitreq` as the async HTTP client. bitreq_http_async = [ "base64", "bitreq", "bitreq/async", "client_async" ] # An async JSON-RPC client implementation. diff --git a/jsonrpc/contrib/test_vars.sh b/jsonrpc/contrib/test_vars.sh index 05f57bdb7..56bcb8eb6 100644 --- a/jsonrpc/contrib/test_vars.sh +++ b/jsonrpc/contrib/test_vars.sh @@ -4,7 +4,7 @@ FEATURES_WITH_STD="" # So this is the var to use for all tests. -FEATURES_WITHOUT_STD="simple_http bitreq_http_sync simple_tcp simple_uds proxy" +FEATURES_WITHOUT_STD="simple_http bitreq_http simple_tcp simple_uds proxy" # Run these examples. EXAMPLES="" diff --git a/jsonrpc/src/client_sync.rs b/jsonrpc/src/client.rs similarity index 99% rename from jsonrpc/src/client_sync.rs rename to jsonrpc/src/client.rs index 6ffdd5843..e9cb0ca67 100644 --- a/jsonrpc/src/client_sync.rs +++ b/jsonrpc/src/client.rs @@ -124,7 +124,7 @@ impl Client { } } -impl fmt::Debug for Client { +impl fmt::Debug for crate::Client { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "jsonrpc::Client(")?; self.transport.fmt_target(f)?; diff --git a/jsonrpc/src/http/bitreq_http_sync.rs b/jsonrpc/src/http/bitreq_http.rs similarity index 97% rename from jsonrpc/src/http/bitreq_http_sync.rs rename to jsonrpc/src/http/bitreq_http.rs index 7fbf05573..20903bfc5 100644 --- a/jsonrpc/src/http/bitreq_http_sync.rs +++ b/jsonrpc/src/http/bitreq_http.rs @@ -1,4 +1,4 @@ -//! This module implements the [`crate::client_sync::Transport`] trait using [`bitreq`] +//! This module implements the [`crate::client::Transport`] trait using [`bitreq`] //! as the underlying HTTP transport. //! //! [bitreq]: @@ -13,7 +13,7 @@ use std::{error, fmt}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use crate::client_sync::Transport; +use crate::client::Transport; use crate::{Request, Response}; const DEFAULT_URL: &str = "http://localhost"; @@ -137,7 +137,7 @@ impl Builder { /// # Examples /// /// ```no_run - /// # use jsonrpc::bitreq_http_sync::BitreqHttpTransport; + /// # use jsonrpc::bitreq_http::BitreqHttpTransport; /// # use std::fs::{self, File}; /// # use std::path::Path; /// # let cookie_file = Path::new("~/.bitcoind/.cookie"); @@ -259,7 +259,7 @@ mod impls { #[cfg(test)] mod tests { use super::*; - use crate::client_sync::Client; + use crate::Client; #[test] fn construct() { diff --git a/jsonrpc/src/http/mod.rs b/jsonrpc/src/http/mod.rs index dc2df4b5c..d6f7f5a73 100644 --- a/jsonrpc/src/http/mod.rs +++ b/jsonrpc/src/http/mod.rs @@ -3,8 +3,8 @@ #[cfg(feature = "simple_http")] pub mod simple_http; -#[cfg(feature = "bitreq_http_sync")] -pub mod bitreq_http_sync; +#[cfg(feature = "bitreq_http")] +pub mod bitreq_http; #[cfg(feature = "bitreq_http_async")] pub mod bitreq_http_async; diff --git a/jsonrpc/src/http/simple_http.rs b/jsonrpc/src/http/simple_http.rs index c77085e4f..795b67092 100644 --- a/jsonrpc/src/http/simple_http.rs +++ b/jsonrpc/src/http/simple_http.rs @@ -19,7 +19,7 @@ use base64::Engine; #[cfg(feature = "proxy")] use socks::Socks5Stream; -use crate::client_sync::{Client, Transport}; +use crate::client::Transport; use crate::http::DEFAULT_PORT; #[cfg(feature = "proxy")] use crate::http::DEFAULT_PROXY_PORT; @@ -413,18 +413,18 @@ impl Default for Builder { fn default() -> Self { Builder::new() } } -impl Client { +impl crate::Client { /// Creates a new JSON-RPC client using a bare-minimum HTTP transport. pub fn simple_http( url: &str, user: Option, pass: Option, - ) -> Result { + ) -> Result { let mut builder = Builder::new().url(url)?; if let Some(user) = user { builder = builder.auth(user, pass); } - Ok(Client::with_transport(builder.build())) + Ok(crate::Client::with_transport(builder.build())) } /// Creates a new JSON_RPC client using a HTTP-Socks5 proxy transport. @@ -435,7 +435,7 @@ impl Client { pass: Option, proxy_addr: &str, proxy_auth: Option<(&str, &str)>, - ) -> Result { + ) -> Result { let mut builder = Builder::new().url(url)?; if let Some(user) = user { builder = builder.auth(user, pass); @@ -445,7 +445,7 @@ impl Client { builder = builder.proxy_auth(user, pass); } let tp = builder.build(); - Ok(Client::with_transport(tp)) + Ok(crate::Client::with_transport(tp)) } } @@ -627,6 +627,7 @@ mod tests { use std::str::FromStr; use super::*; + use crate::Client; #[test] fn test_urls() { diff --git a/jsonrpc/src/lib.rs b/jsonrpc/src/lib.rs index b38b899c8..c06178510 100644 --- a/jsonrpc/src/lib.rs +++ b/jsonrpc/src/lib.rs @@ -20,16 +20,16 @@ pub extern crate base64; #[cfg(feature = "bitreq")] pub extern crate bitreq; +pub mod client; #[cfg(feature = "client_async")] pub mod client_async; -pub mod client_sync; pub mod error; pub mod http; +#[cfg(feature = "bitreq_http")] +pub use http::bitreq_http; #[cfg(feature = "bitreq_http_async")] pub use http::bitreq_http_async; -#[cfg(feature = "bitreq_http_sync")] -pub use http::bitreq_http_sync; #[cfg(feature = "simple_http")] pub use http::simple_http; @@ -42,6 +42,7 @@ pub mod simple_uds; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; +pub use crate::client::{Client, Transport}; pub use crate::error::Error; /// Shorthand method to convert an argument into a boxed [`serde_json::value::RawValue`]. diff --git a/jsonrpc/src/simple_tcp.rs b/jsonrpc/src/simple_tcp.rs index fa5a52fea..9b1ed24db 100644 --- a/jsonrpc/src/simple_tcp.rs +++ b/jsonrpc/src/simple_tcp.rs @@ -5,7 +5,7 @@ use std::{error, fmt, io, net, time}; -use crate::client_sync::Transport; +use crate::client::Transport; use crate::{Request, Response}; #[derive(Debug, Clone)] @@ -110,7 +110,7 @@ mod tests { use std::thread; use super::*; - use crate::client_sync::Client; + use crate::Client; // Test a dummy request / response over a raw TCP transport #[test] diff --git a/jsonrpc/src/simple_uds.rs b/jsonrpc/src/simple_uds.rs index 69942d247..0f324d738 100644 --- a/jsonrpc/src/simple_uds.rs +++ b/jsonrpc/src/simple_uds.rs @@ -5,7 +5,7 @@ use std::os::unix::net::UnixStream; use std::{error, fmt, io, path, time}; -use crate::client_sync::Transport; +use crate::client::Transport; use crate::{Request, Response}; /// Simple synchronous UDS transport. @@ -115,7 +115,7 @@ mod tests { use std::{fs, process, thread}; use super::*; - use crate::client_sync::Client; + use crate::Client; // Test a dummy request / response over an UDS #[test] From 84d501ba97a9135bab0a419a79b9f4accef73d90 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Mon, 16 Mar 2026 17:47:58 +0000 Subject: [PATCH 14/14] Copilot fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- client/src/bdk_client/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/bdk_client/mod.rs b/client/src/bdk_client/mod.rs index f0941a135..5aebc1738 100644 --- a/client/src/bdk_client/mod.rs +++ b/client/src/bdk_client/mod.rs @@ -119,7 +119,7 @@ macro_rules! impl_async_client_check_expected_server_version { pub async fn check_expected_server_version(&self) -> Result<()> { let server_version = self.server_version().await?; if !$expected_versions.contains(&server_version) { - return Err($crate::client_async::error::UnexpectedServerVersionError { + return Err($crate::bdk_client::error::UnexpectedServerVersionError { got: server_version, expected: $expected_versions.to_vec(), })?;