Skip to content

Commit ccc3f0c

Browse files
committed
Make async corepc-client
Edit the copy of the sync client created in the previous commit to be async. Update the readme and cargo.toml files. Replace macros with functions. There is only one async client so the macros are not needed anymore. Create a new module for the bdk client that has the required RPCs in it that return either the rust-bitcoin type or non-version specific model types.
1 parent f1f45db commit ccc3f0c

8 files changed

Lines changed: 360 additions & 111 deletions

File tree

client/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ allowed_duplicates = ["base64"]
2323
[features]
2424
# Enable this feature to get a blocking JSON-RPC client.
2525
client-sync = ["jsonrpc", "jsonrpc/bitreq_http"]
26+
# Enable this feature to get an async JSON-RPC client.
27+
client-async = ["jsonrpc", "jsonrpc/bitreq_http_async", "jsonrpc/client_async"]
2628

2729
[dependencies]
2830
bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] }

client/README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
# corepc-client
22

3-
Rust client for the Bitcoin Core daemon's JSON-RPC API. Currently this
4-
is only a blocking client and is intended to be used in integration testing.
3+
Rust client for the Bitcoin Core daemon's JSON-RPC API.
4+
5+
This crate provides:
6+
7+
- A blocking client intended for integration testing (`client-sync`).
8+
- An async client intended for production (`client-async`).
9+
10+
## Features
11+
12+
- `client-sync`: Blocking JSON-RPC client.
13+
- `client-async`: Async JSON-RPC client.
514

615
## Minimum Supported Rust Version (MSRV)
716

client/src/client_async/error.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
use std::{error, fmt, io};
44

55
use bitcoin::hex;
6+
use types::v17::{
7+
GetBlockHeaderError, GetBlockHeaderVerboseError, GetBlockVerboseOneError,
8+
GetRawTransactionVerboseError,
9+
};
10+
use types::v19::GetBlockFilterError;
11+
use types::v29::{
12+
GetBlockHeaderVerboseError as GetBlockHeaderVerboseErrorV29,
13+
GetBlockVerboseOneError as GetBlockVerboseOneErrorV29,
14+
};
615

716
/// The error type for errors produced in this library.
817
#[derive(Debug)]
@@ -48,6 +57,34 @@ impl From<io::Error> for Error {
4857
fn from(e: io::Error) -> Error { Error::Io(e) }
4958
}
5059

60+
impl From<GetBlockHeaderError> for Error {
61+
fn from(e: GetBlockHeaderError) -> Self { Self::Returned(e.to_string()) }
62+
}
63+
64+
impl From<GetBlockHeaderVerboseError> for Error {
65+
fn from(e: GetBlockHeaderVerboseError) -> Self { Self::Returned(e.to_string()) }
66+
}
67+
68+
impl From<GetBlockVerboseOneError> for Error {
69+
fn from(e: GetBlockVerboseOneError) -> Self { Self::Returned(e.to_string()) }
70+
}
71+
72+
impl From<GetRawTransactionVerboseError> for Error {
73+
fn from(e: GetRawTransactionVerboseError) -> Self { Self::Returned(e.to_string()) }
74+
}
75+
76+
impl From<GetBlockHeaderVerboseErrorV29> for Error {
77+
fn from(e: GetBlockHeaderVerboseErrorV29) -> Self { Self::Returned(e.to_string()) }
78+
}
79+
80+
impl From<GetBlockVerboseOneErrorV29> for Error {
81+
fn from(e: GetBlockVerboseOneErrorV29) -> Self { Self::Returned(e.to_string()) }
82+
}
83+
84+
impl From<GetBlockFilterError> for Error {
85+
fn from(e: GetBlockFilterError) -> Self { Self::Returned(e.to_string()) }
86+
}
87+
5188
impl fmt::Display for Error {
5289
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
5390
use Error::*;

client/src/client_async/mod.rs

Lines changed: 55 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,16 @@
11
// SPDX-License-Identifier: CC0-1.0
22

3-
//! JSON-RPC clients for testing against specific versions of Bitcoin Core.
3+
//! Async JSON-RPC client for Bitcoin Core v25 to v30.
44
55
mod error;
6-
pub mod v17;
7-
pub mod v18;
8-
pub mod v19;
9-
pub mod v20;
10-
pub mod v21;
11-
pub mod v22;
12-
pub mod v23;
13-
pub mod v24;
14-
pub mod v25;
15-
pub mod v26;
16-
pub mod v27;
17-
pub mod v28;
18-
pub mod v29;
19-
pub mod v30;
20-
pub mod v31;
6+
mod rpcs;
217

8+
use std::fmt;
229
use std::fs::File;
2310
use std::io::{BufRead, BufReader};
2411
use std::path::PathBuf;
2512

26-
pub use crate::client_sync::error::Error;
13+
pub use crate::client_async::error::Error;
2714
pub(crate) use crate::{into_json, log_response};
2815

2916
/// Crate-specific Result type.
@@ -57,103 +44,62 @@ impl Auth {
5744
}
5845
}
5946

60-
/// Defines a `jsonrpc::Client` using `bitreq`.
61-
#[macro_export]
62-
macro_rules! define_jsonrpc_bitreq_client {
63-
($version:literal) => {
64-
use std::fmt;
65-
66-
use $crate::client_sync::{log_response, Auth, Result};
67-
use $crate::client_sync::error::Error;
68-
69-
/// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
70-
pub struct Client {
71-
inner: jsonrpc::client::Client,
72-
}
73-
74-
impl fmt::Debug for Client {
75-
fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result {
76-
write!(
77-
f,
78-
"corepc_client::client_sync::{}::Client({:?})", $version, self.inner
79-
)
80-
}
81-
}
82-
83-
impl Client {
84-
/// Creates a client to a bitcoind JSON-RPC server without authentication.
85-
pub fn new(url: &str) -> Self {
86-
let transport = jsonrpc::http::bitreq_http::Builder::new()
87-
.url(url)
88-
.expect("jsonrpc v0.19, this function does not error")
89-
.timeout(std::time::Duration::from_secs(60))
90-
.build();
91-
let inner = jsonrpc::client::Client::with_transport(transport);
92-
93-
Self { inner }
94-
}
95-
96-
/// Creates a client to a bitcoind JSON-RPC server with authentication.
97-
pub fn new_with_auth(url: &str, auth: Auth) -> Result<Self> {
98-
if matches!(auth, Auth::None) {
99-
return Err(Error::MissingUserPassword);
100-
}
101-
let (user, pass) = auth.get_user_pass()?;
102-
103-
let transport = jsonrpc::http::bitreq_http::Builder::new()
104-
.url(url)
105-
.expect("jsonrpc v0.19, this function does not error")
106-
.timeout(std::time::Duration::from_secs(60))
107-
.basic_auth(user.unwrap(), pass)
108-
.build();
109-
let inner = jsonrpc::client::Client::with_transport(transport);
47+
/// Client implements an async JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
48+
pub struct Client {
49+
inner: jsonrpc::client_async::Client,
50+
}
11051

111-
Ok(Self { inner })
112-
}
52+
impl fmt::Debug for Client {
53+
fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result {
54+
write!(f, "corepc_client::client_async::Client({:?})", self.inner)
55+
}
56+
}
11357

114-
/// Call an RPC `method` with given `args` list.
115-
pub fn call<T: for<'a> serde::de::Deserialize<'a>>(
116-
&self,
117-
method: &str,
118-
args: &[serde_json::Value],
119-
) -> Result<T> {
120-
let raw = serde_json::value::to_raw_value(args)?;
121-
let req = self.inner.build_request(&method, Some(&*raw));
122-
if log::log_enabled!(log::Level::Debug) {
123-
log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args));
124-
}
58+
impl Client {
59+
/// Creates a client to a bitcoind JSON-RPC server without authentication.
60+
pub fn new(url: &str) -> Self {
61+
let transport = jsonrpc::bitreq_http_async::Builder::new()
62+
.url(url)
63+
.expect("this function does not error")
64+
.timeout(std::time::Duration::from_secs(60))
65+
.build();
66+
let inner = jsonrpc::client_async::Client::with_transport(transport);
67+
68+
Self { inner }
69+
}
12570

126-
let resp = self.inner.send_request(req).map_err(Error::from);
127-
log_response(method, &resp);
128-
Ok(resp?.result()?)
129-
}
71+
/// Creates a client to a bitcoind JSON-RPC server with authentication.
72+
pub fn new_with_auth(url: &str, auth: Auth) -> Result<Self> {
73+
if matches!(auth, Auth::None) {
74+
return Err(Error::MissingUserPassword);
13075
}
76+
let (user, pass) = auth.get_user_pass()?;
77+
let user = user.ok_or(Error::MissingUserPassword)?;
78+
let transport = jsonrpc::bitreq_http_async::Builder::new()
79+
.url(url)
80+
.expect("this function does not error")
81+
.timeout(std::time::Duration::from_secs(60))
82+
.basic_auth(user, pass)
83+
.build();
84+
let inner = jsonrpc::client_async::Client::with_transport(transport);
85+
86+
Ok(Self { inner })
13187
}
132-
}
13388

134-
/// Implements the `check_expected_server_version()` on `Client`.
135-
///
136-
/// Requires `Client` to be in scope and implement `server_version()`.
137-
/// See and/or use `impl_client_v17__getnetworkinfo`.
138-
///
139-
/// # Parameters
140-
///
141-
/// - `$expected_versions`: An vector of expected server versions e.g., `[230100, 230200]`.
142-
#[macro_export]
143-
macro_rules! impl_client_check_expected_server_version {
144-
($expected_versions:expr) => {
145-
impl Client {
146-
/// Checks that the JSON-RPC endpoint is for a `bitcoind` instance with the expected version.
147-
pub fn check_expected_server_version(&self) -> Result<()> {
148-
let server_version = self.server_version()?;
149-
if !$expected_versions.contains(&server_version) {
150-
return Err($crate::client_sync::error::UnexpectedServerVersionError {
151-
got: server_version,
152-
expected: $expected_versions.to_vec(),
153-
})?;
154-
}
155-
Ok(())
156-
}
89+
/// Call an RPC `method` with given `args` list.
90+
pub async fn call<T: for<'a> serde::de::Deserialize<'a>>(
91+
&self,
92+
method: &str,
93+
args: &[serde_json::Value],
94+
) -> Result<T> {
95+
let raw = serde_json::value::to_raw_value(args)?;
96+
let req = self.inner.build_request(method, Some(&*raw));
97+
if log::log_enabled!(log::Level::Debug) {
98+
log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args));
15799
}
158-
};
100+
101+
let resp = self.inner.send_request(req).await.map_err(Error::from);
102+
log_response(method, &resp);
103+
Ok(resp?.result()?)
104+
}
159105
}

client/src/client_async/rpcs.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! RPC set used by BDK.
4+
//! All functions return the version nonspecific, strongly typed types.
5+
6+
use bitcoin::{block, Block, BlockHash, Transaction, Txid};
7+
use serde_json::value::RawValue;
8+
9+
use crate::client_async::{into_json, Client, Result};
10+
use crate::types::model::{GetBlockFilter, GetBlockHeaderVerbose, GetBlockVerboseOne};
11+
12+
impl Client {
13+
/// Gets a block by blockhash.
14+
pub async fn get_block(&self, hash: &BlockHash) -> Result<Block> {
15+
let json: crate::types::v25::GetBlockVerboseZero =
16+
self.call("getblock", &[into_json(hash)?, into_json(0)?]).await?;
17+
Ok(json.into_model()?.0)
18+
}
19+
20+
/// Gets the block count.
21+
pub async fn get_block_count(&self) -> Result<u64> {
22+
let json: crate::types::v25::GetBlockCount = self.call("getblockcount", &[]).await?;
23+
Ok(json.into_model().0)
24+
}
25+
26+
/// Gets the block hash for a height.
27+
pub async fn get_block_hash(&self, height: u32) -> Result<BlockHash> {
28+
let json: crate::types::v25::GetBlockHash =
29+
self.call("getblockhash", &[into_json(height)?]).await?;
30+
Ok(json.into_model()?.0)
31+
}
32+
33+
/// Gets the hash of the chain tip.
34+
pub async fn get_best_block_hash(&self) -> Result<BlockHash> {
35+
let json: crate::types::v25::GetBestBlockHash = self.call("getbestblockhash", &[]).await?;
36+
Ok(json.into_model()?.0)
37+
}
38+
39+
/// Gets the block header by blockhash.
40+
pub async fn get_block_header(&self, hash: &BlockHash) -> Result<block::Header> {
41+
let json: crate::types::v25::GetBlockHeader =
42+
self.call("getblockheader", &[into_json(hash)?, into_json(false)?]).await?;
43+
Ok(json.into_model()?.0)
44+
}
45+
46+
/// Gets the block header with verbose output.
47+
pub async fn get_block_header_verbose(
48+
&self,
49+
hash: &BlockHash,
50+
) -> Result<GetBlockHeaderVerbose> {
51+
let raw: Box<RawValue> =
52+
self.call("getblockheader", &[into_json(hash)?, into_json(true)?]).await?;
53+
54+
if let Ok(json) =
55+
serde_json::from_str::<crate::types::v29::GetBlockHeaderVerbose>(raw.get())
56+
{
57+
Ok(json.into_model()?)
58+
} else {
59+
let json: crate::types::v25::GetBlockHeaderVerbose = serde_json::from_str(raw.get())?;
60+
Ok(json.into_model()?)
61+
}
62+
}
63+
64+
/// Gets a block by blockhash with verbose set to 1.
65+
pub async fn get_block_verbose(&self, hash: &BlockHash) -> Result<GetBlockVerboseOne> {
66+
let raw: Box<RawValue> = self.call("getblock", &[into_json(hash)?, into_json(1)?]).await?;
67+
68+
if let Ok(json) = serde_json::from_str::<crate::types::v29::GetBlockVerboseOne>(raw.get()) {
69+
Ok(json.into_model()?)
70+
} else {
71+
let json: crate::types::v25::GetBlockVerboseOne = serde_json::from_str(raw.get())?;
72+
Ok(json.into_model()?)
73+
}
74+
}
75+
76+
/// Gets the block filter for a blockhash.
77+
pub async fn get_block_filter(&self, hash: &BlockHash) -> Result<GetBlockFilter> {
78+
let json: crate::types::v25::GetBlockFilter =
79+
self.call("getblockfilter", &[into_json(hash)?]).await?;
80+
Ok(json.into_model()?)
81+
}
82+
83+
/// Gets the transaction IDs currently in the mempool.
84+
pub async fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
85+
let json: crate::types::v25::GetRawMempool = self.call("getrawmempool", &[]).await?;
86+
Ok(json.into_model()?.0)
87+
}
88+
89+
/// Gets the raw transaction by txid.
90+
pub async fn get_raw_transaction(&self, txid: &Txid) -> Result<Transaction> {
91+
let json: crate::types::v25::GetRawTransaction =
92+
self.call("getrawtransaction", &[into_json(txid)?]).await?;
93+
Ok(json.into_model()?.0)
94+
}
95+
96+
/// Returns the version integer reported by the server (e.g. `250200` for v25.2.0).
97+
pub async fn server_version(&self) -> Result<usize> {
98+
// Use a minimal type to read only the `version` field; the shape of other fields
99+
// (e.g. `warnings` changed from String to Vec<String> at v28) differs across the
100+
// supported version range.
101+
#[derive(serde::Deserialize)]
102+
struct NetworkVersion {
103+
version: usize,
104+
}
105+
let json: NetworkVersion = self.call("getnetworkinfo", &[]).await?;
106+
Ok(json.version)
107+
}
108+
}

client/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ pub extern crate types;
1212
#[macro_use]
1313
pub mod client_sync;
1414

15+
#[cfg(feature = "client-async")]
16+
pub mod client_async;
17+
1518
/// Helper to log an RPC response.
1619
#[cfg(any(feature = "client-sync", feature = "client-async"))]
1720
pub(crate) fn log_response<E: std::fmt::Debug>(

integration_test/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,12 @@ TODO = [] # This is a dirty hack while writing the tests.
5959

6060
[dependencies]
6161
bitcoin = { version = "0.32.0", default-features = false, features = ["std", "serde"] }
62+
corepc-client = { version = "0.15.0", path = "../client", default-features = false, features = ["client-async"] }
6263
env_logger = "0.9.0"
6364
bitcoind = { package = "bitcoind", version = "0.40.0", path = "../bitcoind", default-features = false }
6465
rand = "0.8.5"
6566
# Just so we can enable the feature.
6667
types = { package = "corepc-types", version = "0.14.0", path = "../types", features = ["serde-deny-unknown-fields"] }
6768

6869
[dev-dependencies]
70+
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

0 commit comments

Comments
 (0)