|
| 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