From 0196c054e2ace83c87322fb0c0297362bf0750b7 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 12 Feb 2026 09:44:35 +0000 Subject: [PATCH 1/6] Move minimal jsonrpc into corepc-client Move the required parts of jsonrpc into corepc-client so that it does not depend on jsonrpc. The intention is to extend it to work with an async client later. --- client/Cargo.toml | 7 +- client/src/client_sync/error.rs | 8 +- client/src/client_sync/jsonrpc_sync/client.rs | 55 +++++ client/src/client_sync/jsonrpc_sync/error.rs | 68 +++++++ .../jsonrpc_sync/http/bitreq_http.rs | 192 ++++++++++++++++++ .../src/client_sync/jsonrpc_sync/http/mod.rs | 5 + client/src/client_sync/jsonrpc_sync/mod.rs | 54 +++++ client/src/client_sync/mod.rs | 18 +- integration_test/Cargo.toml | 3 - 9 files changed, 393 insertions(+), 17 deletions(-) create mode 100644 client/src/client_sync/jsonrpc_sync/client.rs create mode 100644 client/src/client_sync/jsonrpc_sync/error.rs create mode 100644 client/src/client_sync/jsonrpc_sync/http/bitreq_http.rs create mode 100644 client/src/client_sync/jsonrpc_sync/http/mod.rs create mode 100644 client/src/client_sync/jsonrpc_sync/mod.rs diff --git a/client/Cargo.toml b/client/Cargo.toml index b7c13e367..839460c86 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -18,15 +18,16 @@ rustdoc-args = ["--cfg", "docsrs"] [features] # Enable this feature to get a blocking JSON-RPC client. -client-sync = ["jsonrpc"] +client-sync = ["base64", "bitreq"] [dependencies] bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] } log = "0.4" serde = { version = "1.0.103", default-features = false, features = [ "derive", "alloc" ] } -serde_json = { version = "1.0.117" } +serde_json = { version = "1.0.117", features = ["raw_value"] } 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 } +base64 = { version = "0.22.1", optional = true } +bitreq = { version = "0.3.0", path = "../bitreq", features = ["json-using-serde"], optional = true } [dev-dependencies] diff --git a/client/src/client_sync/error.rs b/client/src/client_sync/error.rs index 331a81eeb..6efb2f137 100644 --- a/client/src/client_sync/error.rs +++ b/client/src/client_sync/error.rs @@ -4,10 +4,12 @@ use std::{error, fmt, io}; use bitcoin::hex; +use crate::client_sync::jsonrpc_sync::error as jsonrpc_error; + /// The error type for errors produced in this library. #[derive(Debug)] pub enum Error { - JsonRpc(jsonrpc::error::Error), + JsonRpc(jsonrpc_error::Error), HexToArray(hex::HexToArrayError), HexToBytes(hex::HexToBytesError), Json(serde_json::error::Error), @@ -24,8 +26,8 @@ pub enum Error { MissingUserPassword, } -impl From for Error { - fn from(e: jsonrpc::error::Error) -> Error { Error::JsonRpc(e) } +impl From for Error { + fn from(e: jsonrpc_error::Error) -> Error { Error::JsonRpc(e) } } impl From for Error { diff --git a/client/src/client_sync/jsonrpc_sync/client.rs b/client/src/client_sync/jsonrpc_sync/client.rs new file mode 100644 index 000000000..043092075 --- /dev/null +++ b/client/src/client_sync/jsonrpc_sync/client.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! JSON-RPC client support. + +use std::fmt; +use std::sync::atomic; + +use serde_json::value::RawValue; + +use crate::client_sync::jsonrpc_sync::error::Error; +use crate::client_sync::jsonrpc_sync::{Request, Response}; + +/// An interface for a transport over which to use the JSON-RPC protocol. +pub trait Transport: Send + Sync + 'static { + /// Sends an RPC request over the transport. + fn send_request(&self, req: Request) -> Result; + /// 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. +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. + 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) + } +} + +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) } +} diff --git a/client/src/client_sync/jsonrpc_sync/error.rs b/client/src/client_sync/jsonrpc_sync/error.rs new file mode 100644 index 000000000..a6d839538 --- /dev/null +++ b/client/src/client_sync/jsonrpc_sync/error.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Error handling for JSON-RPC. + +use std::{error, fmt}; + +use serde::{Deserialize, Serialize}; + +/// A library error. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// A transport error. + Transport(Box), + /// Json error. + Json(serde_json::Error), + /// Error response. + Rpc(RpcError), + /// Response to a request did not have the expected nonce. + NonceMismatch, + /// Response to a request had a jsonrpc field other than "2.0". + VersionMismatch, +} + +impl From for Error { + fn from(e: serde_json::Error) -> Error { Error::Json(e) } +} + +impl From for Error { + fn from(e: RpcError) -> Error { Error::Rpc(e) } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Error::*; + + match *self { + Transport(ref e) => write!(f, "transport error: {}", e), + Json(ref e) => write!(f, "JSON decode error: {}", e), + Rpc(ref r) => write!(f, "RPC error response: {:?}", r), + NonceMismatch => write!(f, "nonce of response did not match nonce of request"), + VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + use self::Error::*; + + match *self { + Rpc(_) | NonceMismatch | VersionMismatch => None, + Transport(ref e) => Some(&**e), + Json(ref e) => Some(e), + } + } +} + +/// A JSON-RPC error object. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RpcError { + /// The integer identifier of the error. + pub code: i32, + /// A string describing the error. + pub message: String, + /// Additional data specific to the error. + pub data: Option>, +} diff --git a/client/src/client_sync/jsonrpc_sync/http/bitreq_http.rs b/client/src/client_sync/jsonrpc_sync/http/bitreq_http.rs new file mode 100644 index 000000000..b925f967a --- /dev/null +++ b/client/src/client_sync/jsonrpc_sync/http/bitreq_http.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! This module implements the `Transport` trait using `bitreq` as the HTTP transport. + +use std::time::Duration; +use std::{error, fmt}; + +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; + +use crate::client_sync::jsonrpc_sync::client::Transport; +use crate::client_sync::jsonrpc_sync::{Error as JsonRpcError, Request, Response}; + +const DEFAULT_URL: &str = "http://localhost"; +const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind. +const DEFAULT_TIMEOUT_SECONDS: u64 = 15; + +/// 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() } + + 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 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 + } + + /// 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. +#[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 JsonRpcError { + fn from(e: Error) -> JsonRpcError { + match e { + Error::Json(e) => JsonRpcError::Json(e), + e => JsonRpcError::Transport(Box::new(e)), + } + } +} diff --git a/client/src/client_sync/jsonrpc_sync/http/mod.rs b/client/src/client_sync/jsonrpc_sync/http/mod.rs new file mode 100644 index 000000000..d8415a971 --- /dev/null +++ b/client/src/client_sync/jsonrpc_sync/http/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! HTTP transport modules. + +pub(crate) mod bitreq_http; diff --git a/client/src/client_sync/jsonrpc_sync/mod.rs b/client/src/client_sync/jsonrpc_sync/mod.rs new file mode 100644 index 000000000..e3cdb36e0 --- /dev/null +++ b/client/src/client_sync/jsonrpc_sync/mod.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Minimal JSON-RPC support for the sync client. + +pub(crate) mod client; +pub(crate) mod error; +pub(crate) mod http; + +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; + +pub(crate) use self::client::Client; +pub(crate) use self::error::Error; + +/// A JSON-RPC request object. +#[derive(Debug, Clone, Serialize)] +pub struct Request<'a> { + /// The name of the RPC call. + pub method: &'a str, + /// Parameters to the RPC call. + pub params: Option<&'a RawValue>, + /// Identifier for this request, which should appear in the response. + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0". + pub jsonrpc: Option<&'a str>, +} + +/// A JSON-RPC response object. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Response { + /// A result if there is one, or [`None`]. + pub result: Option>, + /// An error if there is one, or [`None`]. + pub error: Option, + /// Identifier for this response, which should match that of the request. + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0". + pub jsonrpc: Option, +} + +impl Response { + /// Extracts the result from a response. + pub fn result serde::de::Deserialize<'a>>(&self) -> Result { + if let Some(ref e) = self.error { + return Err(Error::Rpc(e.clone())); + } + + if let Some(ref res) = self.result { + serde_json::from_str(res.get()).map_err(Error::Json) + } else { + serde_json::from_value(serde_json::Value::Null).map_err(Error::Json) + } + } +} diff --git a/client/src/client_sync/mod.rs b/client/src/client_sync/mod.rs index baaf3f8ba..453c81d3d 100644 --- a/client/src/client_sync/mod.rs +++ b/client/src/client_sync/mod.rs @@ -3,6 +3,7 @@ //! JSON-RPC clients for testing against specific versions of Bitcoin Core. mod error; +mod jsonrpc_sync; pub mod v17; pub mod v18; pub mod v19; @@ -38,7 +39,7 @@ pub enum Auth { } impl Auth { - /// Convert into the arguments that jsonrpc::Client needs. + /// Convert into the arguments that the JSON-RPC client needs. pub fn get_user_pass(self) -> Result<(Option, Option)> { match self { Auth::None => Ok((None, None)), @@ -55,7 +56,7 @@ impl Auth { } } -/// Defines a `jsonrpc::Client` using `bitreq`. +/// Defines a JSON-RPC client using `bitreq`. #[macro_export] macro_rules! define_jsonrpc_bitreq_client { ($version:literal) => { @@ -63,10 +64,11 @@ macro_rules! define_jsonrpc_bitreq_client { use $crate::client_sync::{log_response, Auth, Result}; use $crate::client_sync::error::Error; + use $crate::client_sync::jsonrpc_sync; /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. pub struct Client { - inner: jsonrpc::client::Client, + inner: jsonrpc_sync::Client, } impl fmt::Debug for Client { @@ -81,12 +83,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_sync::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::Client::with_transport(transport); + let inner = jsonrpc_sync::Client::with_transport(transport); Self { inner } } @@ -98,13 +100,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_sync::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::Client::with_transport(transport); + let inner = jsonrpc_sync::Client::with_transport(transport); Ok(Self { inner }) } @@ -165,7 +167,7 @@ where } /// Helper to log an RPC response. -fn log_response(method: &str, resp: &Result) { +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) { diff --git a/integration_test/Cargo.toml b/integration_test/Cargo.toml index 05421c435..9fec63464 100644 --- a/integration_test/Cargo.toml +++ b/integration_test/Cargo.toml @@ -74,9 +74,6 @@ types = { package = "corepc-types", version = "0.11.0", path = "../types", featu [patch.crates-io.corepc-client] path = "../client" -[patch.crates-io.jsonrpc] -path = "../jsonrpc" - [patch.crates-io.corepc-node] path = "../node" From da5205e41f264d678c9e5b554e69b14ee22c3a77 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 12 Feb 2026 13:13:10 +0000 Subject: [PATCH 2/6] 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 except for the renaming to async to make the next patch easier to review. --- client/src/client_async/error.rs | 114 +++++++++++ .../jsonrpc_async/client_async.rs | 56 +++++ .../src/client_async/jsonrpc_async/error.rs | 68 +++++++ .../jsonrpc_async/http/bitreq_http_async.rs | 192 ++++++++++++++++++ .../client_async/jsonrpc_async/http/mod.rs | 5 + client/src/client_async/jsonrpc_async/mod.rs | 55 +++++ client/src/client_async/mod.rs | 192 ++++++++++++++++++ 7 files changed, 682 insertions(+) create mode 100644 client/src/client_async/error.rs create mode 100644 client/src/client_async/jsonrpc_async/client_async.rs create mode 100644 client/src/client_async/jsonrpc_async/error.rs create mode 100644 client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs create mode 100644 client/src/client_async/jsonrpc_async/http/mod.rs create mode 100644 client/src/client_async/jsonrpc_async/mod.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..4eddb35d8 --- /dev/null +++ b/client/src/client_async/error.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: CC0-1.0 + +use std::{error, fmt, io}; + +use bitcoin::hex; + +use crate::client_async::jsonrpc_async::error as jsonrpc_error; + +/// 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/jsonrpc_async/client_async.rs b/client/src/client_async/jsonrpc_async/client_async.rs new file mode 100644 index 000000000..f8fe006f4 --- /dev/null +++ b/client/src/client_async/jsonrpc_async/client_async.rs @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! JSON-RPC client support. + +use std::fmt; +use std::sync::atomic; + +use serde_json::value::RawValue; + +use crate::client_async::jsonrpc_async::error::Error; +use crate::client_async::jsonrpc_async::{Request, Response}; + +/// An interface for a transport over which to use the JSON-RPC protocol. +pub trait Transport: Send + Sync + 'static { + /// Sends an RPC request over the transport. + fn send_request(&self, req: Request) -> Result; + /// 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. +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. + 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) + } + +} + +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) } +} diff --git a/client/src/client_async/jsonrpc_async/error.rs b/client/src/client_async/jsonrpc_async/error.rs new file mode 100644 index 000000000..a6d839538 --- /dev/null +++ b/client/src/client_async/jsonrpc_async/error.rs @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Error handling for JSON-RPC. + +use std::{error, fmt}; + +use serde::{Deserialize, Serialize}; + +/// A library error. +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// A transport error. + Transport(Box), + /// Json error. + Json(serde_json::Error), + /// Error response. + Rpc(RpcError), + /// Response to a request did not have the expected nonce. + NonceMismatch, + /// Response to a request had a jsonrpc field other than "2.0". + VersionMismatch, +} + +impl From for Error { + fn from(e: serde_json::Error) -> Error { Error::Json(e) } +} + +impl From for Error { + fn from(e: RpcError) -> Error { Error::Rpc(e) } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Error::*; + + match *self { + Transport(ref e) => write!(f, "transport error: {}", e), + Json(ref e) => write!(f, "JSON decode error: {}", e), + Rpc(ref r) => write!(f, "RPC error response: {:?}", r), + NonceMismatch => write!(f, "nonce of response did not match nonce of request"), + VersionMismatch => write!(f, "`jsonrpc` field set to non-\"2.0\""), + } + } +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + use self::Error::*; + + match *self { + Rpc(_) | NonceMismatch | VersionMismatch => None, + Transport(ref e) => Some(&**e), + Json(ref e) => Some(e), + } + } +} + +/// A JSON-RPC error object. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RpcError { + /// The integer identifier of the error. + pub code: i32, + /// A string describing the error. + pub message: String, + /// Additional data specific to the error. + pub data: Option>, +} diff --git a/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs b/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs new file mode 100644 index 000000000..b925f967a --- /dev/null +++ b/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! This module implements the `Transport` trait using `bitreq` as the HTTP transport. + +use std::time::Duration; +use std::{error, fmt}; + +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::Engine; + +use crate::client_sync::jsonrpc_sync::client::Transport; +use crate::client_sync::jsonrpc_sync::{Error as JsonRpcError, Request, Response}; + +const DEFAULT_URL: &str = "http://localhost"; +const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind. +const DEFAULT_TIMEOUT_SECONDS: u64 = 15; + +/// 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() } + + 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 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 + } + + /// 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. +#[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 JsonRpcError { + fn from(e: Error) -> JsonRpcError { + match e { + Error::Json(e) => JsonRpcError::Json(e), + e => JsonRpcError::Transport(Box::new(e)), + } + } +} diff --git a/client/src/client_async/jsonrpc_async/http/mod.rs b/client/src/client_async/jsonrpc_async/http/mod.rs new file mode 100644 index 000000000..b56a7aad0 --- /dev/null +++ b/client/src/client_async/jsonrpc_async/http/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! HTTP transport modules. + +pub(crate) mod bitreq_http_async; diff --git a/client/src/client_async/jsonrpc_async/mod.rs b/client/src/client_async/jsonrpc_async/mod.rs new file mode 100644 index 000000000..76f3b1fc3 --- /dev/null +++ b/client/src/client_async/jsonrpc_async/mod.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Minimal JSON-RPC support for the sync client. + +pub(crate) mod client; +pub(crate) mod error; +pub(crate) mod http; + +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; + +pub(crate) use self::client_async::Client; +pub(crate) use self::error::Error; + +/// A JSON-RPC request object. +#[derive(Debug, Clone, Serialize)] +pub struct Request<'a> { + /// The name of the RPC call. + pub method: &'a str, + /// Parameters to the RPC call. + pub params: Option<&'a RawValue>, + /// Identifier for this request, which should appear in the response. + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0". + pub jsonrpc: Option<&'a str>, +} + +/// A JSON-RPC response object. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Response { + /// A result if there is one, or [`None`]. + pub result: Option>, + /// An error if there is one, or [`None`]. + pub error: Option, + /// Identifier for this response, which should match that of the request. + pub id: serde_json::Value, + /// jsonrpc field, MUST be "2.0". + pub jsonrpc: Option, +} + +impl Response { + /// Extracts the result from a response. + pub fn result serde::de::Deserialize<'a>>(&self) -> Result { + if let Some(ref e) = self.error { + return Err(Error::Rpc(e.clone())); + } + + if let Some(ref res) = self.result { + serde_json::from_str(res.get()).map_err(Error::Json) + } else { + serde_json::from_value(serde_json::Value::Null).map_err(Error::Json) + } + } + +} diff --git a/client/src/client_async/mod.rs b/client/src/client_async/mod.rs new file mode 100644 index 000000000..74b382859 --- /dev/null +++ b/client/src/client_async/mod.rs @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! JSON-RPC clients for testing against specific versions of Bitcoin Core. + +mod error; +mod jsonrpc_async; +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 the JSON-RPC 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 JSON-RPC 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; + use $crate::client_sync::jsonrpc_sync; + + /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs. + pub struct Client { + inner: jsonrpc_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_sync::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_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_sync::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_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 b83b0f68c4a69e2f48607c74320539fdb379ffa2 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 12 Feb 2026 13:36:10 +0000 Subject: [PATCH 3/6] Add async support to jsonrpc_async --- .../jsonrpc_async/client_async.rs | 41 +++++++++-------- .../jsonrpc_async/http/bitreq_http_async.rs | 44 ++++++++++--------- client/src/client_async/jsonrpc_async/mod.rs | 7 ++- 3 files changed, 49 insertions(+), 43 deletions(-) diff --git a/client/src/client_async/jsonrpc_async/client_async.rs b/client/src/client_async/jsonrpc_async/client_async.rs index f8fe006f4..accb3c6fc 100644 --- a/client/src/client_async/jsonrpc_async/client_async.rs +++ b/client/src/client_async/jsonrpc_async/client_async.rs @@ -1,33 +1,37 @@ // SPDX-License-Identifier: CC0-1.0 -//! JSON-RPC client support. +//! JSON-RPC async client support. use std::fmt; +use std::future::Future; +use std::pin::Pin; use std::sync::atomic; use serde_json::value::RawValue; -use crate::client_async::jsonrpc_async::error::Error; -use crate::client_async::jsonrpc_async::{Request, Response}; +use super::{Error, Request, Response}; -/// An interface for a transport over which to use the JSON-RPC protocol. -pub trait Transport: Send + Sync + 'static { +/// 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 AsyncTransport: Send + Sync + 'static { /// Sends an RPC request over the transport. - fn send_request(&self, req: Request) -> Result; + fn send_request<'a>(&'a self, req: Request<'a>) -> BoxFuture<'a, Result>; /// 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. -pub struct Client { - pub(crate) transport: Box, +/// An async JSON-RPC client. +pub struct AsyncClient { + pub(crate) transport: Box, nonce: atomic::AtomicUsize, } -impl Client { +impl AsyncClient { /// 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) } + pub fn with_transport(transport: T) -> AsyncClient { + AsyncClient { transport: Box::new(transport), nonce: atomic::AtomicUsize::new(1) } } /// Builds a request. @@ -37,20 +41,19 @@ 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 } - } -impl fmt::Debug for Client { +impl fmt::Debug for AsyncClient { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "jsonrpc::Client(")?; + write!(f, "jsonrpc::AsyncClient(")?; self.transport.fmt_target(f)?; write!(f, ")") } } -impl From for Client { - fn from(t: T) -> Client { Client::with_transport(t) } +impl From for AsyncClient { + fn from(t: T) -> AsyncClient { AsyncClient::with_transport(t) } } diff --git a/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs b/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs index b925f967a..c33a96340 100644 --- a/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs +++ b/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs @@ -8,16 +8,17 @@ use std::{error, fmt}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; -use crate::client_sync::jsonrpc_sync::client::Transport; -use crate::client_sync::jsonrpc_sync::{Error as JsonRpcError, Request, Response}; +use crate::client_async::jsonrpc_async::{ + AsyncTransport, BoxFuture, Error as JsonRpcError, Request, Response, +}; const DEFAULT_URL: &str = "http://localhost"; const DEFAULT_PORT: u16 = 8332; // the default RPC port for bitcoind. const DEFAULT_TIMEOUT_SECONDS: u64 = 15; -/// An HTTP transport that uses `bitreq` and is useful for running a bitcoind RPC client. +/// An async HTTP transport that uses [`bitreq`]. #[derive(Clone, Debug)] -pub struct BitreqHttpTransport { +pub struct BitreqHttpAsyncTransport { /// URL of the RPC server. url: String, /// Timeout only supports second granularity. @@ -26,9 +27,9 @@ pub struct BitreqHttpTransport { basic_auth: Option, } -impl Default for BitreqHttpTransport { +impl Default for BitreqHttpAsyncTransport { fn default() -> Self { - BitreqHttpTransport { + BitreqHttpAsyncTransport { url: format!("{}:{}", DEFAULT_URL, DEFAULT_PORT), timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECONDS), basic_auth: None, @@ -36,11 +37,11 @@ impl Default for BitreqHttpTransport { } } -impl BitreqHttpTransport { - /// Constructs a new `BitreqHttpTransport` with default parameters. - pub fn new() -> Self { BitreqHttpTransport::default() } +impl BitreqHttpAsyncTransport { + /// Constructs a new [`BitreqHttpAsyncTransport`] with default parameters. + pub fn new() -> Self { BitreqHttpAsyncTransport::default() } - 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>, { @@ -57,7 +58,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) => @@ -73,23 +74,26 @@ impl BitreqHttpTransport { } } -impl Transport for BitreqHttpTransport { - fn send_request(&self, req: Request) -> Result { - Ok(self.request(req)?) +impl AsyncTransport for BitreqHttpAsyncTransport { + fn send_request<'a>( + &'a self, + req: Request<'a>, + ) -> BoxFuture<'a, Result> { + Box::pin(async move { Ok(self.request(req).await?) }) } fn fmt_target(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.url) } } -/// Builder for simple bitcoind `BitreqHttpTransport`. +/// Builder for async bitcoind [`BitreqHttpAsyncTransport`]. #[derive(Clone, Debug)] pub struct Builder { - tp: BitreqHttpTransport, + tp: BitreqHttpAsyncTransport, } impl Builder { - /// Constructs a new `Builder` with default configuration and the URL to use. - pub fn new() -> Builder { Builder { tp: BitreqHttpTransport::new() } } + /// Constructs a new [`Builder`] with default configuration and the URL to use. + pub fn new() -> Builder { Builder { tp: BitreqHttpAsyncTransport::new() } } /// Sets the timeout after which requests will abort if they aren't finished. pub fn timeout(mut self, timeout: Duration) -> Self { @@ -115,8 +119,8 @@ impl Builder { self } - /// Builds the final `BitreqHttpTransport`. - pub fn build(self) -> BitreqHttpTransport { self.tp } + /// Builds the final [`BitreqHttpAsyncTransport`]. + pub fn build(self) -> BitreqHttpAsyncTransport { self.tp } } impl Default for Builder { diff --git a/client/src/client_async/jsonrpc_async/mod.rs b/client/src/client_async/jsonrpc_async/mod.rs index 76f3b1fc3..95fab7429 100644 --- a/client/src/client_async/jsonrpc_async/mod.rs +++ b/client/src/client_async/jsonrpc_async/mod.rs @@ -1,15 +1,15 @@ // SPDX-License-Identifier: CC0-1.0 -//! Minimal JSON-RPC support for the sync client. +//! Minimal JSON-RPC support for the async client. -pub(crate) mod client; +pub(crate) mod client_async; pub(crate) mod error; pub(crate) mod http; use serde::{Deserialize, Serialize}; use serde_json::value::RawValue; -pub(crate) use self::client_async::Client; +pub use self::client_async::{AsyncClient, AsyncTransport, BoxFuture}; pub(crate) use self::error::Error; /// A JSON-RPC request object. @@ -51,5 +51,4 @@ impl Response { serde_json::from_value(serde_json::Value::Null).map_err(Error::Json) } } - } From c7bde27b3dd5b1827113978592177d42205d452f Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 5 Feb 2026 09:19:26 +0000 Subject: [PATCH 4/6] Make corepc-clint 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 | 39 +++--- 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, 618 insertions(+), 22 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 839460c86..177b3da5f 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 = ["base64", "bitreq"] +# Enable this feature to get an async JSON-RPC client. +client-async = ["base64", "bitreq", "bitreq/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 74b382859..642efd912 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; mod jsonrpc_async; @@ -23,7 +23,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. /// @@ -58,17 +58,16 @@ impl Auth { /// Defines a JSON-RPC 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_async::jsonrpc_async; - use $crate::client_sync::{log_response, Auth, Result}; - use $crate::client_sync::error::Error; - use $crate::client_sync::jsonrpc_sync; - - /// 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_sync::Client, + inner: jsonrpc_async::AsyncClient, } impl fmt::Debug for Client { @@ -83,12 +82,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_sync::http::bitreq_http::Builder::new() + let transport = jsonrpc_async::http::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_sync::Client::with_transport(transport); + let inner = jsonrpc_async::AsyncClient::with_transport(transport); Self { inner } } @@ -100,19 +99,19 @@ macro_rules! define_jsonrpc_bitreq_client { } let (user, pass) = auth.get_user_pass()?; - let transport = jsonrpc_sync::http::bitreq_http::Builder::new() + let transport = jsonrpc_async::http::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_sync::Client::with_transport(transport); + let inner = jsonrpc_async::AsyncClient::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], @@ -123,7 +122,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()?) } @@ -140,14 +139,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(), })?; @@ -167,7 +166,7 @@ where } /// Helper to log an RPC response. -fn log_response(method: &str, resp: &Result) { +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) { 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 bddd95fd6b625d7e3c168f3e2eed8b9607262b5f Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Mon, 9 Feb 2026 16:25:32 +0000 Subject: [PATCH 5/6] 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 9fec63464..8bb033e76 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 b816dbc76f0d2adca6e34a30590ae1a501e56242 Mon Sep 17 00:00:00 2001 From: "Jamil Lambert, PhD" Date: Thu, 12 Feb 2026 18:45:01 +0000 Subject: [PATCH 6/6] Update lockfiles --- Cargo-minimal.lock | 3 ++- Cargo-recent.lock | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index 852d39b38..ead5d01f8 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -240,9 +240,10 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" name = "corepc-client" version = "0.11.0" dependencies = [ + "base64 0.22.1", "bitcoin", + "bitreq", "corepc-types", - "jsonrpc", "log", "serde", "serde_json", diff --git a/Cargo-recent.lock b/Cargo-recent.lock index 852d39b38..ead5d01f8 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -240,9 +240,10 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" name = "corepc-client" version = "0.11.0" dependencies = [ + "base64 0.22.1", "bitcoin", + "bitreq", "corepc-types", - "jsonrpc", "log", "serde", "serde_json",