bitcoind_async_client/client/
mod.rs1use 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
24pub type ClientResult<T> = Result<T, ClientError>;
26
27const DEFAULT_MAX_RETRIES: u8 = 3;
29
30const DEFAULT_RETRY_INTERVAL_MS: u64 = 1_000;
32
33pub 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#[derive(Debug, Clone)]
44pub struct Client {
45 url: String,
47
48 client: ReqwestClient,
50
51 id: Arc<AtomicUsize>,
57
58 max_retries: u8,
60
61 retry_interval: u64,
63}
64
65#[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 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 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 return Err(ClientError::Body(err.to_string()));
186 } else if err.is_status() {
187 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 let e = ClientError::MalformedResponse(err.to_string());
196 warn!(%e, "decoding error, retrying...");
197 } else if err.is_connect() {
198 let e = ClientError::Connection(err.to_string());
200 warn!(%e, "connection error, retrying...");
201 } else if err.is_timeout() {
202 let e = ClientError::Timeout;
204 warn!(%e, "timeout error, retrying...");
205 } else if err.is_request() {
206 let e = ClientError::Request(err.to_string());
208 warn!(%e, "request error, retrying...");
209 } else if err.is_builder() {
210 return Err(ClientError::ReqBuilder(err.to_string()));
212 } else if err.is_redirect() {
213 return Err(ClientError::HttpRedirect(err.to_string()));
215 } else {
216 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}