Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

Cargo.lock
**/target
.vscode/
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"rust-analyzer.cargo.features": [
"client-async"
],
"rust-analyzer.check.features": [
"client-async"
]
Comment on lines +2 to +7
}
6 changes: 4 additions & 2 deletions client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ rustdoc-args = ["--cfg", "docsrs"]

[features]
# Enable this feature to get a blocking JSON-RPC client.
client-sync = ["jsonrpc"]
client-sync = ["jsonrpc", "jsonrpc/bitreq_http"]
# Enable this feature to get an async JSON-RPC client.
client-async = ["jsonrpc", "jsonrpc/bitreq_http_async", "jsonrpc/client_async"]

[dependencies]
bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] }
Expand All @@ -27,6 +29,6 @@ serde = { version = "1.0.103", default-features = false, features = [ "derive",
serde_json = { version = "1.0.117" }
types = { package = "corepc-types", version = "0.11.0", path = "../types", default-features = false, features = ["std"] }

jsonrpc = { version = "0.19.0", path = "../jsonrpc", features = ["bitreq_http"], optional = true }
jsonrpc = { version = "0.19.0", path = "../jsonrpc", 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
149 changes: 149 additions & 0 deletions client/src/bdk_client/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: CC0-1.0

use std::{error, fmt, io};

use bitcoin::hex;
use types::v17::{
GetBlockHeaderError, GetBlockHeaderVerboseError, GetBlockVerboseOneError,
GetRawTransactionVerboseError,
};
use types::v19::GetBlockFilterError;
use types::v29::{
GetBlockHeaderVerboseError as GetBlockHeaderVerboseErrorV29,
GetBlockVerboseOneError as GetBlockVerboseOneErrorV29,
};

/// The error type for errors produced in this library.
#[derive(Debug)]
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 From<GetBlockHeaderError> for Error {
fn from(e: GetBlockHeaderError) -> Self { Self::Returned(e.to_string()) }
}

impl From<GetBlockHeaderVerboseError> for Error {
fn from(e: GetBlockHeaderVerboseError) -> Self { Self::Returned(e.to_string()) }
}

impl From<GetBlockVerboseOneError> for Error {
fn from(e: GetBlockVerboseOneError) -> Self { Self::Returned(e.to_string()) }
}

impl From<GetRawTransactionVerboseError> for Error {
fn from(e: GetRawTransactionVerboseError) -> Self { Self::Returned(e.to_string()) }
}

impl From<GetBlockHeaderVerboseErrorV29> for Error {
fn from(e: GetBlockHeaderVerboseErrorV29) -> Self { Self::Returned(e.to_string()) }
}

impl From<GetBlockVerboseOneErrorV29> for Error {
fn from(e: GetBlockVerboseOneErrorV29) -> Self { Self::Returned(e.to_string()) }
}

impl From<GetBlockFilterError> for Error {
fn from(e: GetBlockFilterError) -> Self { Self::Returned(e.to_string()) }
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use Error::*;

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) }
}
166 changes: 166 additions & 0 deletions client/src/bdk_client/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: CC0-1.0

//! Async JSON-RPC clients for Bitcoin Core v25 to v30.

mod error;
mod rpcs;

use std::fmt;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;

pub use crate::bdk_client::error::Error;

/// Crate-specific Result type.
///
/// Shorthand for `std::result::Result` with our crate-specific [`Error`] type.
pub type Result<T> = std::result::Result<T, Error>;

/// The different authentication methods for the client.
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub enum Auth {
None,
UserPass(String, String),
CookieFile(PathBuf),
}

impl Auth {
/// Convert into the arguments that jsonrpc::Client needs.
pub fn get_user_pass(self) -> Result<(Option<String>, Option<String>)> {
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())))
}
}
}
}

/// Client implements an async JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
pub struct Client {
pub(crate) inner: jsonrpc::client_async::Client,
}

impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result {
write!(f, "corepc_client::client_async::Client({:?})", self.inner)
}
}

impl Client {
/// Creates a client to a bitcoind JSON-RPC server without authentication.
pub fn new(url: &str) -> Self {
let transport = jsonrpc::bitreq_http_async::Builder::new()
.url(url)
.expect("jsonrpc v0.19, this function does not error")
.timeout(std::time::Duration::from_secs(60))
.build();
let inner = jsonrpc::client_async::Client::with_transport(transport);

Self { inner }
}

/// Creates a client to a bitcoind JSON-RPC server with authentication.
pub fn new_with_auth(url: &str, auth: Auth) -> Result<Self> {
if matches!(auth, Auth::None) {
return Err(Error::MissingUserPassword);
}
let (user, pass) = auth.get_user_pass()?;
let user = user.ok_or(Error::MissingUserPassword)?;
let transport = jsonrpc::bitreq_http_async::Builder::new()
.url(url)
.expect("jsonrpc v0.19, this function does not error")
.timeout(std::time::Duration::from_secs(60))
.basic_auth(user, pass)
.build();
let inner = jsonrpc::client_async::Client::with_transport(transport);

Ok(Self { inner })
}

/// Call an RPC `method` with given `args` list.
pub async fn call<T: for<'a> serde::de::Deserialize<'a>>(
&self,
method: &str,
args: &[serde_json::Value],
) -> Result<T> {
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::bdk_client::error::UnexpectedServerVersionError {
got: server_version,
expected: $expected_versions.to_vec(),
})?;
}
Ok(())
}
}
};
}

/// Shorthand for converting a variable into a `serde_json::Value`.
pub(crate) fn into_json<T>(val: T) -> Result<serde_json::Value>
where
T: serde::ser::Serialize,
{
Ok(serde_json::to_value(val)?)
}

/// Helper to log an RPC response.
fn log_response(method: &str, resp: &Result<jsonrpc::Response>) {
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) {
if let Ok(def) =
serde_json::value::to_raw_value(&serde_json::value::Value::Null)
{
let result = resp.result.as_ref().unwrap_or(&def);
log::trace!(target: "corepc", "response for {}: {}", method, result);
}
},
}
}
}
Loading
Loading