bitcoind_json_rpc_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;
18
19use std::fs::File;
20use std::io::{BufRead, BufReader};
21use std::path::PathBuf;
22
23pub use crate::client_sync::error::Error;
24
25/// Crate-specific Result type.
26///
27/// Shorthand for `std::result::Result` with our crate-specific [`Error`] type.
28pub type Result<T> = std::result::Result<T, Error>;
29
30/// The different authentication methods for the client.
31#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)]
32pub enum Auth {
33    None,
34    UserPass(String, String),
35    CookieFile(PathBuf),
36}
37
38impl Auth {
39    /// Convert into the arguments that jsonrpc::Client needs.
40    pub fn get_user_pass(self) -> Result<(Option<String>, Option<String>)> {
41        match self {
42            Auth::None => Ok((None, None)),
43            Auth::UserPass(u, p) => Ok((Some(u), Some(p))),
44            Auth::CookieFile(path) => {
45                let line = BufReader::new(File::open(path)?)
46                    .lines()
47                    .next()
48                    .ok_or(Error::InvalidCookieFile)??;
49                let colon = line.find(':').ok_or(Error::InvalidCookieFile)?;
50                Ok((Some(line[..colon].into()), Some(line[colon + 1..].into())))
51            }
52        }
53    }
54}
55
56/// Defines a `jsonrpc::Client` using `minreq`.
57#[macro_export]
58macro_rules! define_jsonrpc_minreq_client {
59    ($version:literal) => {
60        use std::fmt;
61
62        use $crate::client_sync::{log_response, Auth, Result};
63        use $crate::client_sync::error::Error;
64
65        /// Client implements a JSON-RPC client for the Bitcoin Core daemon or compatible APIs.
66        pub struct Client {
67            inner: jsonrpc::client::Client,
68        }
69
70        impl fmt::Debug for Client {
71            fn fmt(&self, f: &mut fmt::Formatter) -> core::fmt::Result {
72                write!(
73                    f,
74                    "bitcoind-json-rpc::client_sync::{}::Client({:?})", $version, self.inner
75                )
76            }
77        }
78
79        impl Client {
80            /// Creates a client to a bitcoind JSON-RPC server without authentication.
81            pub fn new(url: &str) -> Self {
82                let transport = jsonrpc::http::minreq_http::Builder::new()
83                    .url(url)
84                    .expect("jsonrpc v0.18, this function does not error")
85                    .build();
86                let inner = jsonrpc::client::Client::with_transport(transport);
87
88                Self { inner }
89            }
90
91            /// Creates a client to a bitcoind JSON-RPC server with authentication.
92            pub fn new_with_auth(url: &str, auth: Auth) -> Result<Self> {
93                if matches!(auth, Auth::None) {
94                    return Err(Error::MissingUserPassword);
95                }
96                let (user, pass) = auth.get_user_pass()?;
97
98                let transport = jsonrpc::http::minreq_http::Builder::new()
99                    .url(url)
100                    .expect("jsonrpc v0.18, this function does not error")
101                    .basic_auth(user.unwrap(), pass)
102                    .build();
103                let inner = jsonrpc::client::Client::with_transport(transport);
104
105                Ok(Self { inner })
106            }
107
108            /// Call an RPC `method` with given `args` list.
109            pub fn call<T: for<'a> serde::de::Deserialize<'a>>(
110                &self,
111                method: &str,
112                args: &[serde_json::Value],
113            ) -> Result<T> {
114                let raw = serde_json::value::to_raw_value(args)?;
115                let req = self.inner.build_request(&method, Some(&*raw));
116                if log::log_enabled!(log::Level::Debug) {
117                    log::debug!(target: "bitcoind-json-rpc", "request: {} {}", method, serde_json::Value::from(args));
118                }
119
120                let resp = self.inner.send_request(req).map_err(Error::from);
121                log_response(method, &resp);
122                Ok(resp?.result()?)
123            }
124        }
125    }
126}
127
128/// Implements the `check_expected_server_version()` on `Client`.
129///
130/// Requires `Client` to be in scope and implement `server_version()`.
131/// See and/or use `impl_client_v17__getnetworkinfo`.
132///
133/// # Parameters
134///
135/// - `$expected_versions`: An vector of expected server versions e.g., `[230100, 230200]`.
136#[macro_export]
137macro_rules! impl_client_check_expected_server_version {
138    ($expected_versions:expr) => {
139        impl Client {
140            /// Checks that the JSON-RPC endpoint is for a `bitcoind` instance with the expected version.
141            pub fn check_expected_server_version(&self) -> Result<()> {
142                let server_version = self.server_version()?;
143                if !$expected_versions.contains(&server_version) {
144                    return Err($crate::client_sync::error::UnexpectedServerVersionError {
145                        got: server_version,
146                        expected: $expected_versions.to_vec(),
147                    })?;
148                }
149                Ok(())
150            }
151        }
152    };
153}
154
155/// Shorthand for converting a variable into a `serde_json::Value`.
156fn into_json<T>(val: T) -> Result<serde_json::Value>
157where
158    T: serde::ser::Serialize,
159{
160    Ok(serde_json::to_value(val)?)
161}
162
163/// Shorthand for converting an `Option` into an `Option<serde_json::Value>`.
164#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
165fn opt_into_json<T>(opt: Option<T>) -> Result<serde_json::Value>
166where
167    T: serde::ser::Serialize,
168{
169    match opt {
170        Some(val) => Ok(into_json(val)?),
171        None => Ok(serde_json::Value::Null),
172    }
173}
174
175/// Shorthand for `serde_json::Value::Null`.
176#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
177fn null() -> serde_json::Value { serde_json::Value::Null }
178
179/// Shorthand for an empty `serde_json::Value` array.
180#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
181fn empty_arr() -> serde_json::Value { serde_json::Value::Array(vec![]) }
182
183/// Shorthand for an empty `serde_json` object.
184#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
185fn empty_obj() -> serde_json::Value { serde_json::Value::Object(Default::default()) }
186
187/// Handle default values in the argument list.
188///
189/// Substitute `Value::Null`s with corresponding values from `defaults` table, except when they are
190/// trailing, in which case just skip them altogether in returned list.
191///
192/// Note, that `defaults` corresponds to the last elements of `args`.
193///
194/// ```norust
195/// arg1 arg2 arg3 arg4
196///           def1 def2
197/// ```
198///
199/// Elements of `args` without corresponding `defaults` value, won't be substituted, because they
200/// are required.
201#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
202fn handle_defaults<'a>(
203    args: &'a mut [serde_json::Value],
204    defaults: &[serde_json::Value],
205) -> &'a [serde_json::Value] {
206    assert!(args.len() >= defaults.len());
207
208    // Pass over the optional arguments in backwards order, filling in defaults after the first
209    // non-null optional argument has been observed.
210    let mut first_non_null_optional_idx = None;
211    for i in 0..defaults.len() {
212        let args_i = args.len() - 1 - i;
213        let defaults_i = defaults.len() - 1 - i;
214        if args[args_i] == serde_json::Value::Null {
215            if first_non_null_optional_idx.is_some() {
216                if defaults[defaults_i] == serde_json::Value::Null {
217                    panic!("Missing `default` for argument idx {}", args_i);
218                }
219                args[args_i] = defaults[defaults_i].clone();
220            }
221        } else if first_non_null_optional_idx.is_none() {
222            first_non_null_optional_idx = Some(args_i);
223        }
224    }
225
226    let required_num = args.len() - defaults.len();
227
228    if let Some(i) = first_non_null_optional_idx {
229        &args[..i + 1]
230    } else {
231        &args[..required_num]
232    }
233}
234
235/// Convert a possible-null result into an `Option`.
236#[allow(dead_code)] // TODO: Remove this if unused still when we are done.
237fn opt_result<T: for<'a> serde::de::Deserialize<'a>>(
238    result: serde_json::Value,
239) -> Result<Option<T>> {
240    if result == serde_json::Value::Null {
241        Ok(None)
242    } else {
243        Ok(serde_json::from_value(result)?)
244    }
245}
246
247/// Helper to log an RPC response.
248fn log_response(method: &str, resp: &Result<jsonrpc::Response>) {
249    use log::Level::{Debug, Trace, Warn};
250
251    if log::log_enabled!(Warn) || log::log_enabled!(Debug) || log::log_enabled!(Trace) {
252        match resp {
253            Err(ref e) =>
254                if log::log_enabled!(Debug) {
255                    log::debug!(target: "bitcoind-json-rpc", "error: {}: {:?}", method, e);
256                },
257            Ok(ref resp) =>
258                if let Some(ref e) = resp.error {
259                    if log::log_enabled!(Debug) {
260                        log::debug!(target: "bitcoind-json-rpc", "response error for {}: {:?}", method, e);
261                    }
262                } else if log::log_enabled!(Trace) {
263                    let def =
264                        serde_json::value::to_raw_value(&serde_json::value::Value::Null).unwrap();
265                    let result = resp.result.as_ref().unwrap_or(&def);
266                    log::trace!(target: "bitcoind-json-rpc", "response for {}: {}", method, result);
267                },
268        }
269    }
270}