Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo-minimal.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion Cargo-recent.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 6 additions & 3 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
13 changes: 11 additions & 2 deletions client/README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
114 changes: 114 additions & 0 deletions client/src/client_async/error.rs
Original file line number Diff line number Diff line change
@@ -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<jsonrpc_error::Error> for Error {
fn from(e: jsonrpc_error::Error) -> Error { Error::JsonRpc(e) }
}

impl From<hex::HexToArrayError> for Error {
fn from(e: hex::HexToArrayError) -> Self { Self::HexToArray(e) }
}

impl From<hex::HexToBytesError> for Error {
fn from(e: hex::HexToBytesError) -> Self { Self::HexToBytes(e) }
}

impl From<serde_json::error::Error> for Error {
fn from(e: serde_json::error::Error) -> Error { Error::Json(e) }
}

impl From<bitcoin::consensus::encode::FromHexError> for Error {
fn from(e: bitcoin::consensus::encode::FromHexError) -> Error { Error::BitcoinSerialization(e) }
}

impl From<io::Error> 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<usize>,
}

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<UnexpectedServerVersionError> for Error {
fn from(e: UnexpectedServerVersionError) -> Self { Self::ServerVersion(e) }
}
59 changes: 59 additions & 0 deletions client/src/client_async/jsonrpc_async/client_async.rs
Original file line number Diff line number Diff line change
@@ -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<Box<dyn Future<Output = T> + 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<Response, Error>>;
/// 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<dyn AsyncTransport>,
nonce: atomic::AtomicUsize,
}

impl AsyncClient {
/// Creates a new client with the given transport.
pub fn with_transport<T: AsyncTransport>(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<Response, Error> {
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<T: AsyncTransport> From<T> for AsyncClient {
fn from(t: T) -> AsyncClient { AsyncClient::with_transport(t) }
}
68 changes: 68 additions & 0 deletions client/src/client_async/jsonrpc_async/error.rs
Original file line number Diff line number Diff line change
@@ -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<dyn error::Error + Send + Sync>),
/// 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<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Error { Error::Json(e) }
}

impl From<RpcError> 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<Box<serde_json::value::RawValue>>,
}
Loading