Skip to main content

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;
19pub mod v30;
20
21use std::fs::File;
22use std::io::{BufRead, BufReader};
23use std::path::PathBuf;
24
25pub 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.
30pub 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)]
34pub enum Auth {
35    None,
36    UserPass(String, String),
37    CookieFile(PathBuf),
38}
39
40impl 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]
60macro_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]
141macro_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`.
160fn into_json<T>(val: T) -> Result<serde_json::Value>
161where
162    T: serde::ser::Serialize,
163{
164    Ok(serde_json::to_value(val)?)
165}
166
167/// Helper to log an RPC response.
168fn 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}