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", diff --git a/client/Cargo.toml b/client/Cargo.toml index b7c13e367..177b3da5f 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -18,15 +18,18 @@ rustdoc-args = ["--cfg", "docsrs"] [features] # Enable this feature to get a blocking JSON-RPC client. -client-sync = ["jsonrpc"] +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"] } 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/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/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..accb3c6fc --- /dev/null +++ b/client/src/client_async/jsonrpc_async/client_async.rs @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! 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 super::{Error, Request, Response}; + +/// 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<'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; +} + +/// An async JSON-RPC client. +pub struct AsyncClient { + pub(crate) transport: Box, + nonce: atomic::AtomicUsize, +} + +impl AsyncClient { + /// Creates a new client with the given transport. + pub fn with_transport(transport: T) -> AsyncClient { + AsyncClient { 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 async fn send_request(&self, request: Request<'_>) -> Result { + self.transport.send_request(request).await + } +} + +impl fmt::Debug for AsyncClient { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "jsonrpc::AsyncClient(")?; + self.transport.fmt_target(f)?; + write!(f, ")") + } +} + +impl From for AsyncClient { + fn from(t: T) -> AsyncClient { AsyncClient::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..c33a96340 --- /dev/null +++ b/client/src/client_async/jsonrpc_async/http/bitreq_http_async.rs @@ -0,0 +1,196 @@ +// 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_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 async HTTP transport that uses [`bitreq`]. +#[derive(Clone, Debug)] +pub struct BitreqHttpAsyncTransport { + /// 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 BitreqHttpAsyncTransport { + fn default() -> Self { + BitreqHttpAsyncTransport { + url: format!("{}:{}", DEFAULT_URL, DEFAULT_PORT), + timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECONDS), + basic_auth: None, + } + } +} + +impl BitreqHttpAsyncTransport { + /// Constructs a new [`BitreqHttpAsyncTransport`] with default parameters. + pub fn new() -> Self { BitreqHttpAsyncTransport::default() } + + async 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_async().await?; + 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 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 async bitcoind [`BitreqHttpAsyncTransport`]. +#[derive(Clone, Debug)] +pub struct Builder { + tp: BitreqHttpAsyncTransport, +} + +impl Builder { + /// 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 { + 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 [`BitreqHttpAsyncTransport`]. + pub fn build(self) -> BitreqHttpAsyncTransport { 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..95fab7429 --- /dev/null +++ b/client/src/client_async/jsonrpc_async/mod.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Minimal JSON-RPC support for the async client. + +pub(crate) mod client_async; +pub(crate) mod error; +pub(crate) mod http; + +use serde::{Deserialize, Serialize}; +use serde_json::value::RawValue; + +pub use self::client_async::{AsyncClient, AsyncTransport, BoxFuture}; +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..642efd912 --- /dev/null +++ b/client/src/client_async/mod.rs @@ -0,0 +1,191 @@ +// SPDX-License-Identifier: CC0-1.0 + +//! Async JSON-RPC clients for 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_async::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_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; + + /// Client implements an async JSON-RPC client for the Bitcoin Core daemon or compatible APIs. + pub struct Client { + inner: jsonrpc_async::AsyncClient, + } + + 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_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_async::AsyncClient::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_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_async::AsyncClient::with_transport(transport); + + Ok(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)); + } + + let resp = self.inner.send_request(req).await.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_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 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 { + 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); + }, + } + } +} 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/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/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; diff --git a/integration_test/Cargo.toml b/integration_test/Cargo.toml index 05421c435..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,14 +70,12 @@ 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] path = "../client" -[patch.crates-io.jsonrpc] -path = "../jsonrpc" - [patch.crates-io.corepc-node] path = "../node" 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()) }