bitcoind_async_client/client/
mod.rs

1use std::{
2    fmt,
3    sync::{
4        atomic::{AtomicUsize, Ordering},
5        Arc,
6    },
7    time::Duration,
8};
9
10use crate::error::{BitcoinRpcError, ClientError};
11use base64::{engine::general_purpose, Engine};
12use reqwest::{
13    header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE},
14    Client as ReqwestClient,
15};
16use serde::{de, Deserialize, Serialize};
17use serde_json::{json, value::Value};
18use tokio::time::sleep;
19use tracing::*;
20
21#[cfg(feature = "29_0")]
22pub mod v29;
23
24/// This is an alias for the result type returned by the [`Client`].
25pub type ClientResult<T> = Result<T, ClientError>;
26
27/// The maximum number of retries for a request.
28const DEFAULT_MAX_RETRIES: u8 = 3;
29
30/// The maximum number of retries for a request.
31const DEFAULT_RETRY_INTERVAL_MS: u64 = 1_000;
32
33/// Custom implementation to convert a value to a `Value` type.
34pub fn to_value<T>(value: T) -> ClientResult<Value>
35where
36    T: Serialize,
37{
38    serde_json::to_value(value)
39        .map_err(|e| ClientError::Param(format!("Error creating value: {e}")))
40}
41
42/// An `async` client for interacting with a `bitcoind` instance.
43#[derive(Debug, Clone)]
44pub struct Client {
45    /// The URL of the `bitcoind` instance.
46    url: String,
47
48    /// The underlying `async` HTTP client.
49    client: ReqwestClient,
50
51    /// The ID of the current request.
52    ///
53    /// # Implementation Details
54    ///
55    /// Using an [`Arc`] so that [`Client`] is [`Clone`].
56    id: Arc<AtomicUsize>,
57
58    /// The maximum number of retries for a request.
59    max_retries: u8,
60
61    /// Interval between retries for a request in ms.
62    retry_interval: u64,
63}
64
65/// Response returned by the `bitcoind` RPC server.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67struct Response<R> {
68    pub result: Option<R>,
69    pub error: Option<BitcoinRpcError>,
70    pub id: u64,
71}
72
73impl Client {
74    /// Creates a new [`Client`] with the given URL, username, and password.
75    pub fn new(
76        url: String,
77        username: String,
78        password: String,
79        max_retries: Option<u8>,
80        retry_interval: Option<u64>,
81    ) -> ClientResult<Self> {
82        if username.is_empty() || password.is_empty() {
83            return Err(ClientError::MissingUserPassword);
84        }
85
86        let user_pw = general_purpose::STANDARD.encode(format!("{username}:{password}"));
87        let authorization = format!("Basic {user_pw}")
88            .parse()
89            .map_err(|_| ClientError::Other("Error parsing header".to_string()))?;
90
91        let content_type = "application/json"
92            .parse()
93            .map_err(|_| ClientError::Other("Error parsing header".to_string()))?;
94        let headers =
95            HeaderMap::from_iter([(AUTHORIZATION, authorization), (CONTENT_TYPE, content_type)]);
96
97        trace!(headers = ?headers);
98
99        let client = ReqwestClient::builder()
100            .default_headers(headers)
101            .build()
102            .map_err(|e| ClientError::Other(format!("Could not create client: {e}")))?;
103
104        let id = Arc::new(AtomicUsize::new(0));
105
106        let max_retries = max_retries.unwrap_or(DEFAULT_MAX_RETRIES);
107        let retry_interval = retry_interval.unwrap_or(DEFAULT_RETRY_INTERVAL_MS);
108
109        trace!(url = %url, "Created bitcoin client");
110
111        Ok(Self {
112            url,
113            client,
114            id,
115            max_retries,
116            retry_interval,
117        })
118    }
119
120    fn next_id(&self) -> usize {
121        self.id.fetch_add(1, Ordering::AcqRel)
122    }
123
124    async fn call<T: de::DeserializeOwned + fmt::Debug>(
125        &self,
126        method: &str,
127        params: &[Value],
128    ) -> ClientResult<T> {
129        let mut retries = 0;
130        loop {
131            trace!(%method, ?params, %retries, "Calling bitcoin client");
132
133            let id = self.next_id();
134
135            let response = self
136                .client
137                .post(&self.url)
138                .json(&json!({
139                    "jsonrpc": "1.0",
140                    "id": id,
141                    "method": method,
142                    "params": params
143                }))
144                .send()
145                .await;
146            trace!(?response, "Response received");
147            match response {
148                Ok(resp) => {
149                    // Check HTTP status code first before parsing body
150                    let resp = match resp.error_for_status() {
151                        Err(e) if e.is_status() => {
152                            if let Some(status) = e.status() {
153                                let reason =
154                                    status.canonical_reason().unwrap_or("Unknown").to_string();
155                                return Err(ClientError::Status(status.as_u16(), reason));
156                            } else {
157                                return Err(ClientError::Other(e.to_string()));
158                            }
159                        }
160                        Err(e) => {
161                            return Err(ClientError::Other(e.to_string()));
162                        }
163                        Ok(resp) => resp,
164                    };
165
166                    let raw_response = resp
167                        .text()
168                        .await
169                        .map_err(|e| ClientError::Parse(e.to_string()))?;
170                    trace!(%raw_response, "Raw response received");
171                    let data: Response<T> = serde_json::from_str(&raw_response)
172                        .map_err(|e| ClientError::Parse(e.to_string()))?;
173                    if let Some(err) = data.error {
174                        return Err(ClientError::Server(err.code, err.message));
175                    }
176                    return data
177                        .result
178                        .ok_or_else(|| ClientError::Other("Empty data received".to_string()));
179                }
180                Err(err) => {
181                    warn!(err = %err, "Error calling bitcoin client");
182
183                    if err.is_body() {
184                        // Body error is unrecoverable
185                        return Err(ClientError::Body(err.to_string()));
186                    } else if err.is_status() {
187                        // Status error is unrecoverable
188                        let e = match err.status() {
189                            Some(code) => ClientError::Status(code.as_u16(), err.to_string()),
190                            _ => ClientError::Other(err.to_string()),
191                        };
192                        return Err(e);
193                    } else if err.is_decode() {
194                        // Error decoding response, might be recoverable
195                        let e = ClientError::MalformedResponse(err.to_string());
196                        warn!(%e, "decoding error, retrying...");
197                    } else if err.is_connect() {
198                        // Connection error, might be recoverable
199                        let e = ClientError::Connection(err.to_string());
200                        warn!(%e, "connection error, retrying...");
201                    } else if err.is_timeout() {
202                        // Timeout error, might be recoverable
203                        let e = ClientError::Timeout;
204                        warn!(%e, "timeout error, retrying...");
205                    } else if err.is_request() {
206                        // General request error, might be recoverable
207                        let e = ClientError::Request(err.to_string());
208                        warn!(%e, "request error, retrying...");
209                    } else if err.is_builder() {
210                        // Request builder error is unrecoverable
211                        return Err(ClientError::ReqBuilder(err.to_string()));
212                    } else if err.is_redirect() {
213                        // Redirect error is unrecoverable
214                        return Err(ClientError::HttpRedirect(err.to_string()));
215                    } else {
216                        // Unknown error is unrecoverable
217                        return Err(ClientError::Other("Unknown error".to_string()));
218                    }
219                }
220            }
221            retries += 1;
222            if retries >= self.max_retries {
223                return Err(ClientError::MaxRetriesExceeded(self.max_retries));
224            }
225            sleep(Duration::from_millis(self.retry_interval)).await;
226        }
227    }
228}