Skip to content

Commit c9cd46d

Browse files
committed
Copy client_sync to client_async
Create a new folder for the upcoming async client and copy in the existing client_sync code. Code copy only to make the next patch easier to review.
1 parent d949c57 commit c9cd46d

2 files changed

Lines changed: 302 additions & 0 deletions

File tree

client/src/client_async/error.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
use std::{error, fmt, io};
4+
5+
use bitcoin::hex;
6+
7+
/// The error type for errors produced in this library.
8+
#[derive(Debug)]
9+
pub enum Error {
10+
JsonRpc(jsonrpc::error::Error),
11+
HexToArray(hex::HexToArrayError),
12+
HexToBytes(hex::HexToBytesError),
13+
Json(serde_json::error::Error),
14+
BitcoinSerialization(bitcoin::consensus::encode::FromHexError),
15+
Io(io::Error),
16+
InvalidCookieFile,
17+
/// The JSON result had an unexpected structure.
18+
UnexpectedStructure,
19+
/// The daemon returned an error string.
20+
Returned(String),
21+
/// The server version did not match what was expected.
22+
ServerVersion(UnexpectedServerVersionError),
23+
/// Missing user/password.
24+
MissingUserPassword,
25+
}
26+
27+
impl From<jsonrpc::error::Error> for Error {
28+
fn from(e: jsonrpc::error::Error) -> Error { Error::JsonRpc(e) }
29+
}
30+
31+
impl From<hex::HexToArrayError> for Error {
32+
fn from(e: hex::HexToArrayError) -> Self { Self::HexToArray(e) }
33+
}
34+
35+
impl From<hex::HexToBytesError> for Error {
36+
fn from(e: hex::HexToBytesError) -> Self { Self::HexToBytes(e) }
37+
}
38+
39+
impl From<serde_json::error::Error> for Error {
40+
fn from(e: serde_json::error::Error) -> Error { Error::Json(e) }
41+
}
42+
43+
impl From<bitcoin::consensus::encode::FromHexError> for Error {
44+
fn from(e: bitcoin::consensus::encode::FromHexError) -> Error { Error::BitcoinSerialization(e) }
45+
}
46+
47+
impl From<io::Error> for Error {
48+
fn from(e: io::Error) -> Error { Error::Io(e) }
49+
}
50+
51+
impl fmt::Display for Error {
52+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
53+
use Error::*;
54+
55+
match *self {
56+
JsonRpc(ref e) => write!(f, "JSON-RPC error: {}", e),
57+
HexToArray(ref e) => write!(f, "hex to array decode error: {}", e),
58+
HexToBytes(ref e) => write!(f, "hex to bytes decode error: {}", e),
59+
Json(ref e) => write!(f, "JSON error: {}", e),
60+
BitcoinSerialization(ref e) => write!(f, "Bitcoin serialization error: {}", e),
61+
Io(ref e) => write!(f, "I/O error: {}", e),
62+
InvalidCookieFile => write!(f, "invalid cookie file"),
63+
UnexpectedStructure => write!(f, "the JSON result had an unexpected structure"),
64+
Returned(ref s) => write!(f, "the daemon returned an error string: {}", s),
65+
ServerVersion(ref e) => write!(f, "server version: {}", e),
66+
MissingUserPassword => write!(f, "missing user and/or password"),
67+
}
68+
}
69+
}
70+
71+
impl error::Error for Error {
72+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
73+
use Error::*;
74+
75+
match *self {
76+
JsonRpc(ref e) => Some(e),
77+
HexToArray(ref e) => Some(e),
78+
HexToBytes(ref e) => Some(e),
79+
Json(ref e) => Some(e),
80+
BitcoinSerialization(ref e) => Some(e),
81+
Io(ref e) => Some(e),
82+
ServerVersion(ref e) => Some(e),
83+
InvalidCookieFile | UnexpectedStructure | Returned(_) | MissingUserPassword => None,
84+
}
85+
}
86+
}
87+
88+
/// Error returned when RPC client expects a different version than bitcoind reports.
89+
#[derive(Debug, Clone, PartialEq, Eq)]
90+
pub struct UnexpectedServerVersionError {
91+
/// Version from server.
92+
pub got: usize,
93+
/// Expected server version.
94+
pub expected: Vec<usize>,
95+
}
96+
97+
impl fmt::Display for UnexpectedServerVersionError {
98+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
99+
let mut expected = String::new();
100+
for version in &self.expected {
101+
let v = format!(" {} ", version);
102+
expected.push_str(&v);
103+
}
104+
write!(f, "unexpected bitcoind version, got: {} expected one of: {}", self.got, expected)
105+
}
106+
}
107+
108+
impl error::Error for UnexpectedServerVersionError {}
109+
110+
impl From<UnexpectedServerVersionError> for Error {
111+
fn from(e: UnexpectedServerVersionError) -> Self { Self::ServerVersion(e) }
112+
}

client/src/client_async/mod.rs

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// SPDX-License-Identifier: CC0-1.0
2+
3+
//! JSON-RPC clients for testing against specific versions of Bitcoin Core.
4+
5+
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+
21+
use std::fs::File;
22+
use std::io::{BufRead, BufReader};
23+
use std::path::PathBuf;
24+
25+
pub use crate::client_sync::error::Error;
26+
27+
/// Crate-specific Result type.
28+
///
29+
/// Shorthand for `std::result::Result` with our crate-specific [`Error`] type.
30+
pub type Result<T> = std::result::Result<T, Error>;
31+
32+
/// The different authentication methods for the client.
33+
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
34+
pub enum Auth {
35+
None,
36+
UserPass(String, String),
37+
CookieFile(PathBuf),
38+
}
39+
40+
impl Auth {
41+
/// Convert into the arguments that jsonrpc::Client needs.
42+
pub fn get_user_pass(self) -> Result<(Option<String>, Option<String>)> {
43+
match self {
44+
Auth::None => Ok((None, None)),
45+
Auth::UserPass(u, p) => Ok((Some(u), Some(p))),
46+
Auth::CookieFile(path) => {
47+
let line = BufReader::new(File::open(path)?)
48+
.lines()
49+
.next()
50+
.ok_or(Error::InvalidCookieFile)??;
51+
let colon = line.find(':').ok_or(Error::InvalidCookieFile)?;
52+
Ok((Some(line[..colon].into()), Some(line[colon + 1..].into())))
53+
}
54+
}
55+
}
56+
}
57+
58+
/// Defines a `jsonrpc::Client` using `bitreq`.
59+
#[macro_export]
60+
macro_rules! define_jsonrpc_bitreq_client {
61+
($version:literal) => {
62+
use std::fmt;
63+
64+
use $crate::client_sync::{log_response, Auth, Result};
65+
use $crate::client_sync::error::Error;
66+
67+
/// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
68+
pub struct Client {
69+
inner: jsonrpc::client::Client,
70+
}
71+
72+
impl fmt::Debug for Client {
73+
fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result {
74+
write!(
75+
f,
76+
"corepc_client::client_sync::{}::Client({:?})", $version, self.inner
77+
)
78+
}
79+
}
80+
81+
impl Client {
82+
/// Creates a client to a bitcoind JSON-RPC server without authentication.
83+
pub fn new(url: &str) -> Self {
84+
let transport = jsonrpc::http::bitreq_http::Builder::new()
85+
.url(url)
86+
.expect("jsonrpc v0.19, this function does not error")
87+
.timeout(std::time::Duration::from_secs(60))
88+
.build();
89+
let inner = jsonrpc::client::Client::with_transport(transport);
90+
91+
Self { inner }
92+
}
93+
94+
/// Creates a client to a bitcoind JSON-RPC server with authentication.
95+
pub fn new_with_auth(url: &str, auth: Auth) -> Result<Self> {
96+
if matches!(auth, Auth::None) {
97+
return Err(Error::MissingUserPassword);
98+
}
99+
let (user, pass) = auth.get_user_pass()?;
100+
101+
let transport = jsonrpc::http::bitreq_http::Builder::new()
102+
.url(url)
103+
.expect("jsonrpc v0.19, this function does not error")
104+
.timeout(std::time::Duration::from_secs(60))
105+
.basic_auth(user.unwrap(), pass)
106+
.build();
107+
let inner = jsonrpc::client::Client::with_transport(transport);
108+
109+
Ok(Self { inner })
110+
}
111+
112+
/// Call an RPC `method` with given `args` list.
113+
pub fn call<T: for<'a> serde::de::Deserialize<'a>>(
114+
&self,
115+
method: &str,
116+
args: &[serde_json::Value],
117+
) -> Result<T> {
118+
let raw = serde_json::value::to_raw_value(args)?;
119+
let req = self.inner.build_request(&method, Some(&*raw));
120+
if log::log_enabled!(log::Level::Debug) {
121+
log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args));
122+
}
123+
124+
let resp = self.inner.send_request(req).map_err(Error::from);
125+
log_response(method, &resp);
126+
Ok(resp?.result()?)
127+
}
128+
}
129+
}
130+
}
131+
132+
/// Implements the `check_expected_server_version()` on `Client`.
133+
///
134+
/// Requires `Client` to be in scope and implement `server_version()`.
135+
/// See and/or use `impl_client_v17__getnetworkinfo`.
136+
///
137+
/// # Parameters
138+
///
139+
/// - `$expected_versions`: An vector of expected server versions e.g., `[230100, 230200]`.
140+
#[macro_export]
141+
macro_rules! impl_client_check_expected_server_version {
142+
($expected_versions:expr) => {
143+
impl Client {
144+
/// Checks that the JSON-RPC endpoint is for a `bitcoind` instance with the expected version.
145+
pub fn check_expected_server_version(&self) -> Result<()> {
146+
let server_version = self.server_version()?;
147+
if !$expected_versions.contains(&server_version) {
148+
return Err($crate::client_sync::error::UnexpectedServerVersionError {
149+
got: server_version,
150+
expected: $expected_versions.to_vec(),
151+
})?;
152+
}
153+
Ok(())
154+
}
155+
}
156+
};
157+
}
158+
159+
/// Shorthand for converting a variable into a `serde_json::Value`.
160+
fn into_json<T>(val: T) -> Result<serde_json::Value>
161+
where
162+
T: serde::ser::Serialize,
163+
{
164+
Ok(serde_json::to_value(val)?)
165+
}
166+
167+
/// Helper to log an RPC response.
168+
fn log_response(method: &str, resp: &Result<jsonrpc::Response>) {
169+
use log::Level::{Debug, Trace, Warn};
170+
171+
if log::log_enabled!(Warn) || log::log_enabled!(Debug) || log::log_enabled!(Trace) {
172+
match resp {
173+
Err(ref e) =>
174+
if log::log_enabled!(Debug) {
175+
log::debug!(target: "corepc", "error: {}: {:?}", method, e);
176+
},
177+
Ok(ref resp) =>
178+
if let Some(ref e) = resp.error {
179+
if log::log_enabled!(Debug) {
180+
log::debug!(target: "corepc", "response error for {}: {:?}", method, e);
181+
}
182+
} else if log::log_enabled!(Trace) {
183+
let def =
184+
serde_json::value::to_raw_value(&serde_json::value::Value::Null).unwrap();
185+
let result = resp.result.as_ref().unwrap_or(&def);
186+
log::trace!(target: "corepc", "response for {}: {}", method, result);
187+
},
188+
}
189+
}
190+
}

0 commit comments

Comments
 (0)