corepc_client/client_sync/
mod.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! JSON-RPC clients for testing against specific versions of Bitcoin Core.
4
5mod error;
6pub mod v17;
7pub mod v18;
8pub mod v19;
9pub mod v20;
10pub mod v21;
11pub mod v22;
12pub mod v23;
13pub mod v24;
14pub mod v25;
15pub mod v26;
16pub mod v27;
17pub mod v28;
18pub mod v29;
19
20use std::fs::File;
21use std::io::{BufRead, BufReader};
22use std::path::PathBuf;
23
24pub use crate::client_sync::error::Error;
25
26/// Crate-specific Result type.
27///
28/// Shorthand for `std::result::Result` with our crate-specific [`Error`] type.
29pub type Result<T> = std::result::Result<T, Error>;
30
31/// The different authentication methods for the client.
32#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
33pub enum Auth {
34    None,
35    UserPass(String, String),
36    CookieFile(PathBuf),
37}
38
39impl Auth {
40    /// Convert into the arguments that jsonrpc::Client needs.
41    pub fn get_user_pass(self) -> Result<(Option<String>, Option<String>)> {
42        match self {
43            Auth::None => Ok((None, None)),
44            Auth::UserPass(u, p) => Ok((Some(u), Some(p))),
45            Auth::CookieFile(path) => {
46                let line = BufReader::new(File::open(path)?)
47                    .lines()
48                    .next()
49                    .ok_or(Error::InvalidCookieFile)??;
50                let colon = line.find(':').ok_or(Error::InvalidCookieFile)?;
51                Ok((Some(line[..colon].into()), Some(line[colon + 1..].into())))
52            }
53        }
54    }
55}
56
57/// Defines a `jsonrpc::Client` using `minreq`.
58#[macro_export]
59macro_rules! define_jsonrpc_minreq_client {
60    ($version:literal) => {
61        use std::fmt;
62
63        use $crate::client_sync::{log_response, Auth, Result};
64        use $crate::client_sync::error::Error;
65
66        /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
67        pub struct Client {
68            inner: jsonrpc::client::Client,
69        }
70
71        impl fmt::Debug for Client {
72            fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result {
73                write!(
74                    f,
75                    "corepc_client::client_sync::{}::Client({:?})", $version, self.inner
76                )
77            }
78        }
79
80        impl Client {
81            /// Creates a client to a bitcoind JSON-RPC server without authentication.
82            pub fn new(url: &str) -> Self {
83                let transport = jsonrpc::http::minreq_http::Builder::new()
84                    .url(url)
85                    .expect("jsonrpc v0.18, this function does not error")
86                    .build();
87                let inner = jsonrpc::client::Client::with_transport(transport);
88
89                Self { inner }
90            }
91
92            /// Creates a client to a bitcoind JSON-RPC server with authentication.
93            pub fn new_with_auth(url: &str, auth: Auth) -> Result<Self> {
94                if matches!(auth, Auth::None) {
95                    return Err(Error::MissingUserPassword);
96                }
97                let (user, pass) = auth.get_user_pass()?;
98
99                let transport = jsonrpc::http::minreq_http::Builder::new()
100                    .url(url)
101                    .expect("jsonrpc v0.18, this function does not error")
102                    .basic_auth(user.unwrap(), pass)
103                    .build();
104                let inner = jsonrpc::client::Client::with_transport(transport);
105
106                Ok(Self { inner })
107            }
108
109            /// Call an RPC `method` with given `args` list.
110            pub fn call<T: for<'a> serde::de::Deserialize<'a>>(
111                &self,
112                method: &str,
113                args: &[serde_json::Value],
114            ) -> Result<T> {
115                let raw = serde_json::value::to_raw_value(args)?;
116                let req = self.inner.build_request(&method, Some(&*raw));
117                if log::log_enabled!(log::Level::Debug) {
118                    log::debug!(target: "corepc", "request: {} {}", method, serde_json::Value::from(args));
119                }
120
121                let resp = self.inner.send_request(req).map_err(Error::from);
122                log_response(method, &resp);
123                Ok(resp?.result()?)
124            }
125        }
126    }
127}
128
129/// Implements the `check_expected_server_version()` on `Client`.
130///
131/// Requires `Client` to be in scope and implement `server_version()`.
132/// See and/or use `impl_client_v17__getnetworkinfo`.
133///
134/// # Parameters
135///
136/// - `$expected_versions`: An vector of expected server versions e.g., `[230100, 230200]`.
137#[macro_export]
138macro_rules! impl_client_check_expected_server_version {
139    ($expected_versions:expr) => {
140        impl Client {
141            /// Checks that the JSON-RPC endpoint is for a `bitcoind` instance with the expected version.
142            pub fn check_expected_server_version(&self) -> Result<()> {
143                let server_version = self.server_version()?;
144                if !$expected_versions.contains(&server_version) {
145                    return Err($crate::client_sync::error::UnexpectedServerVersionError {
146                        got: server_version,
147                        expected: $expected_versions.to_vec(),
148                    })?;
149                }
150                Ok(())
151            }
152        }
153    };
154}
155
156/// Shorthand for converting a variable into a `serde_json::Value`.
157fn into_json<T>(val: T) -> Result<serde_json::Value>
158where
159    T: serde::ser::Serialize,
160{
161    Ok(serde_json::to_value(val)?)
162}
163
164/// Shorthand for converting an `Option` into an `Option<serde_json::Value>`.
165#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
166fn opt_into_json<T>(opt: Option<T>) -> Result<serde_json::Value>
167where
168    T: serde::ser::Serialize,
169{
170    match opt {
171        Some(val) => Ok(into_json(val)?),
172        None => Ok(serde_json::Value::Null),
173    }
174}
175
176/// Shorthand for `serde_json::Value::Null`.
177#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
178fn null() -> serde_json::Value { serde_json::Value::Null }
179
180/// Shorthand for an empty `serde_json::Value` array.
181#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
182fn empty_arr() -> serde_json::Value { serde_json::Value::Array(vec![]) }
183
184/// Shorthand for an empty `serde_json` object.
185#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
186fn empty_obj() -> serde_json::Value { serde_json::Value::Object(Default::default()) }
187
188/// Convert a possible-null result into an `Option`.
189#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
190fn opt_result<T: for<'a> serde::de::Deserialize<'a>>(
191    result: serde_json::Value,
192) -> Result<Option<T>> {
193    if result == serde_json::Value::Null {
194        Ok(None)
195    } else {
196        Ok(serde_json::from_value(result)?)
197    }
198}
199
200/// Helper to log an RPC response.
201fn log_response(method: &str, resp: &Result<jsonrpc::Response>) {
202    use log::Level::{Debug, Trace, Warn};
203
204    if log::log_enabled!(Warn) || log::log_enabled!(Debug) || log::log_enabled!(Trace) {
205        match resp {
206            Err(ref e) =>
207                if log::log_enabled!(Debug) {
208                    log::debug!(target: "corepc", "error: {}: {:?}", method, e);
209                },
210            Ok(ref resp) =>
211                if let Some(ref e) = resp.error {
212                    if log::log_enabled!(Debug) {
213                        log::debug!(target: "corepc", "response error for {}: {:?}", method, e);
214                    }
215                } else if log::log_enabled!(Trace) {
216                    let def =
217                        serde_json::value::to_raw_value(&serde_json::value::Value::Null).unwrap();
218                    let result = resp.result.as_ref().unwrap_or(&def);
219                    log::trace!(target: "corepc", "response for {}: {}", method, result);
220                },
221        }
222    }
223}