Skip to content

Commit 8f59c84

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 e8d30c6 commit 8f59c84

2 files changed

Lines changed: 271 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: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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+
pub mod v31;
21+
22+
use std::fs::File;
23+
use std::io::{BufRead, BufReader};
24+
use std::path::PathBuf;
25+
26+
pub use crate::client_sync::error::Error;
27+
pub(crate) use crate::{into_json, log_response};
28+
29+
/// Crate-specific Result type.
30+
///
31+
/// Shorthand for `std::result::Result` with our crate-specific [`Error`] type.
32+
pub type Result<T> = std::result::Result<T, Error>;
33+
34+
/// The different authentication methods for the client.
35+
#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
36+
pub enum Auth {
37+
None,
38+
UserPass(String, String),
39+
CookieFile(PathBuf),
40+
}
41+
42+
impl Auth {
43+
/// Convert into the arguments that jsonrpc::Client needs.
44+
pub fn get_user_pass(self) -> Result<(Option<String>, Option<String>)> {
45+
match self {
46+
Auth::None => Ok((None, None)),
47+
Auth::UserPass(u, p) => Ok((Some(u), Some(p))),
48+
Auth::CookieFile(path) => {
49+
let line = BufReader::new(File::open(path)?)
50+
.lines()
51+
.next()
52+
.ok_or(Error::InvalidCookieFile)??;
53+
let colon = line.find(':').ok_or(Error::InvalidCookieFile)?;
54+
Ok((Some(line[..colon].into()), Some(line[colon + 1..].into())))
55+
}
56+
}
57+
}
58+
}
59+
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);
110+
111+
Ok(Self { inner })
112+
}
113+
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+
}
125+
126+
let resp = self.inner.send_request(req).map_err(Error::from);
127+
log_response(method, &resp);
128+
Ok(resp?.result()?)
129+
}
130+
}
131+
}
132+
}
133+
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+
}
157+
}
158+
};
159+
}

0 commit comments

Comments
 (0)